diff --git a/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js b/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js
index 888107c9e07c7..baff30895c0e0 100644
--- a/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js
+++ b/fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js
@@ -43,11 +43,18 @@ export default function FocusCase() {
- Unfocusable div
-
+
+
+
Unfocusable div with nested focusable button
+
+
- Unfocusable div
+
diff --git a/fixtures/dom/src/style.css b/fixtures/dom/src/style.css
index 66fda7afe0cac..e507014d6829d 100644
--- a/fixtures/dom/src/style.css
+++ b/fixtures/dom/src/style.css
@@ -365,6 +365,10 @@ tbody tr:nth-child(even) {
background-color: green;
}
+.highlight-focused-children * {
+ margin-left: 10px;
+}
+
.highlight-focused-children *:focus {
outline: 2px solid green;
}
diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 8584b644eff9d..1eabf7a19a90b 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -68,6 +68,7 @@ import {
getFragmentParentHostFiber,
getNextSiblingHostFiber,
getInstanceFromHostFiber,
+ traverseFragmentInstanceDeeply,
} from 'react-reconciler/src/ReactFiberTreeReflection';
export {detachDeletedInstance};
@@ -2698,7 +2699,7 @@ FragmentInstance.prototype.focus = function (
this: FragmentInstanceType,
focusOptions?: FocusOptions,
): void {
- traverseFragmentInstance(
+ traverseFragmentInstanceDeeply(
this._fragmentFiber,
setFocusOnFiberIfFocusable,
focusOptions,
@@ -2717,7 +2718,11 @@ FragmentInstance.prototype.focusLast = function (
focusOptions?: FocusOptions,
): void {
const children: Array = [];
- traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
+ traverseFragmentInstanceDeeply(
+ this._fragmentFiber,
+ collectChildren,
+ children,
+ );
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (setFocusOnFiberIfFocusable(child, focusOptions)) {
diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
index 50447e1eac677..6eb1fb3a0f346 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
@@ -145,6 +145,32 @@ describe('FragmentRefs', () => {
document.activeElement.blur();
});
+ // @gate enableFragmentRefs
+ it('focuses deeply nested focusable children, depth first', async () => {
+ const fragmentRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+
+ function Test() {
+ return (
+
+
+
+
+ );
+ }
+ await act(() => {
+ root.render();
+ });
+ await act(() => {
+ fragmentRef.current.focus();
+ });
+ expect(document.activeElement.id).toEqual('grandchild-a');
+ });
+
// @gate enableFragmentRefs
it('preserves document order when adding and removing children', async () => {
const fragmentRef = React.createRef();
@@ -228,6 +254,34 @@ describe('FragmentRefs', () => {
expect(document.activeElement.id).toEqual('child-c');
document.activeElement.blur();
});
+
+ // @gate enableFragmentRefs
+ it('focuses deeply nested focusable children, depth first', async () => {
+ const fragmentRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+
+ function Test() {
+ return (
+
+
+
+
+ );
+ }
+ await act(() => {
+ root.render();
+ });
+ await act(() => {
+ fragmentRef.current.focusLast();
+ });
+ expect(document.activeElement.id).toEqual('grandchild-b');
+ });
});
describe('blur()', () => {
diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js
index d032d3247e475..45da707dfea23 100644
--- a/packages/react-reconciler/src/ReactFiberTreeReflection.js
+++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js
@@ -354,6 +354,16 @@ export function traverseFragmentInstance(
traverseVisibleHostChildren(fragmentFiber.child, false, fn, a, b, c);
}
+export function traverseFragmentInstanceDeeply(
+ fragmentFiber: Fiber,
+ fn: (Fiber, A, B, C) => boolean,
+ a: A,
+ b: B,
+ c: C,
+): void {
+ traverseVisibleHostChildren(fragmentFiber.child, true, fn, a, b, c);
+}
+
function traverseVisibleHostChildren(
child: Fiber | null,
searchWithinHosts: boolean,
@@ -363,24 +373,8 @@ function traverseVisibleHostChildren(
c: C,
): boolean {
while (child !== null) {
- if (child.tag === HostComponent) {
- if (fn(child, a, b, c)) {
- return true;
- }
- if (searchWithinHosts) {
- if (
- traverseVisibleHostChildren(
- child.child,
- searchWithinHosts,
- fn,
- a,
- b,
- c,
- )
- ) {
- return true;
- }
- }
+ if (child.tag === HostComponent && fn(child, a, b, c)) {
+ return true;
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
@@ -388,6 +382,7 @@ function traverseVisibleHostChildren(
// Skip hidden subtrees
} else {
if (
+ (searchWithinHosts || child.tag !== HostComponent) &&
traverseVisibleHostChildren(child.child, searchWithinHosts, fn, a, b, c)
) {
return true;