Skip to content

Commit 0daf77b

Browse files
committed
Allow fragment refs to attempt focus/focusLast on nested host children
1 parent e5a8de8 commit 0daf77b

File tree

5 files changed

+88
-23
lines changed

5 files changed

+88
-23
lines changed

fixtures/dom/src/components/fixtures/fragment-refs/FocusCase.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,18 @@ export default function FocusCase() {
4343
</Fixture.Controls>
4444
<div className="highlight-focused-children" style={{display: 'flex'}}>
4545
<Fragment ref={fragmentRef}>
46-
<div style={{outline: '1px solid black'}}>Unfocusable div</div>
47-
<button>Button 1</button>
46+
<div style={{outline: '1px solid black'}}>
47+
<p>Unfocusable div</p>
48+
</div>
49+
<div style={{outline: '1px solid black'}}>
50+
<p>Unfocusable div with nested focusable button</p>
51+
<button>Button 1</button>
52+
</div>
4853
<button>Button 2</button>
4954
<input type="text" placeholder="Input field" />
50-
<div style={{outline: '1px solid black'}}>Unfocusable div</div>
55+
<div style={{outline: '1px solid black'}}>
56+
<p>Unfocusable div</p>
57+
</div>
5158
</Fragment>
5259
</div>
5360
</Fixture>

fixtures/dom/src/style.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@ tbody tr:nth-child(even) {
365365
background-color: green;
366366
}
367367

368+
.highlight-focused-children * {
369+
margin-left: 10px;
370+
}
371+
368372
.highlight-focused-children *:focus {
369373
outline: 2px solid green;
370374
}

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
getFragmentParentHostFiber,
6969
getNextSiblingHostFiber,
7070
getInstanceFromHostFiber,
71+
traverseFragmentInstanceDeeply,
7172
} from 'react-reconciler/src/ReactFiberTreeReflection';
7273

7374
export {detachDeletedInstance};
@@ -2698,7 +2699,7 @@ FragmentInstance.prototype.focus = function (
26982699
this: FragmentInstanceType,
26992700
focusOptions?: FocusOptions,
27002701
): void {
2701-
traverseFragmentInstance(
2702+
traverseFragmentInstanceDeeply(
27022703
this._fragmentFiber,
27032704
setFocusOnFiberIfFocusable,
27042705
focusOptions,
@@ -2717,7 +2718,11 @@ FragmentInstance.prototype.focusLast = function (
27172718
focusOptions?: FocusOptions,
27182719
): void {
27192720
const children: Array<Fiber> = [];
2720-
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
2721+
traverseFragmentInstanceDeeply(
2722+
this._fragmentFiber,
2723+
collectChildren,
2724+
children,
2725+
);
27212726
for (let i = children.length - 1; i >= 0; i--) {
27222727
const child = children[i];
27232728
if (setFocusOnFiberIfFocusable(child, focusOptions)) {

packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,32 @@ describe('FragmentRefs', () => {
145145
document.activeElement.blur();
146146
});
147147

148+
// @gate enableFragmentRefs
149+
it('focuses deeply nested focusable children, depth first', async () => {
150+
const fragmentRef = React.createRef();
151+
const root = ReactDOMClient.createRoot(container);
152+
153+
function Test() {
154+
return (
155+
<Fragment ref={fragmentRef}>
156+
<div id="child-a">
157+
<div tabIndex={0} id="grandchild-a">
158+
<a id="greatgrandchild-a" href="/" />
159+
</div>
160+
</div>
161+
<a id="child-b" href="/" />
162+
</Fragment>
163+
);
164+
}
165+
await act(() => {
166+
root.render(<Test />);
167+
});
168+
await act(() => {
169+
fragmentRef.current.focus();
170+
});
171+
expect(document.activeElement.id).toEqual('grandchild-a');
172+
});
173+
148174
// @gate enableFragmentRefs
149175
it('preserves document order when adding and removing children', async () => {
150176
const fragmentRef = React.createRef();
@@ -228,6 +254,34 @@ describe('FragmentRefs', () => {
228254
expect(document.activeElement.id).toEqual('child-c');
229255
document.activeElement.blur();
230256
});
257+
258+
// @gate enableFragmentRefs
259+
it('focuses deeply nested focusable children, depth first', async () => {
260+
const fragmentRef = React.createRef();
261+
const root = ReactDOMClient.createRoot(container);
262+
263+
function Test() {
264+
return (
265+
<Fragment ref={fragmentRef}>
266+
<div id="child-a" href="/">
267+
<a id="grandchild-a" href="/" />
268+
<a id="grandchild-b" href="/" />
269+
</div>
270+
<div tabIndex={0} id="child-b">
271+
<a id="grandchild-a" href="/" />
272+
<a id="grandchild-b" href="/" />
273+
</div>
274+
</Fragment>
275+
);
276+
}
277+
await act(() => {
278+
root.render(<Test />);
279+
});
280+
await act(() => {
281+
fragmentRef.current.focusLast();
282+
});
283+
expect(document.activeElement.id).toEqual('grandchild-b');
284+
});
231285
});
232286

233287
describe('blur()', () => {

packages/react-reconciler/src/ReactFiberTreeReflection.js

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,16 @@ export function traverseFragmentInstance<A, B, C>(
354354
traverseVisibleHostChildren(fragmentFiber.child, false, fn, a, b, c);
355355
}
356356

357+
export function traverseFragmentInstanceDeeply<A, B, C>(
358+
fragmentFiber: Fiber,
359+
fn: (Fiber, A, B, C) => boolean,
360+
a: A,
361+
b: B,
362+
c: C,
363+
): void {
364+
traverseVisibleHostChildren(fragmentFiber.child, true, fn, a, b, c);
365+
}
366+
357367
function traverseVisibleHostChildren<A, B, C>(
358368
child: Fiber | null,
359369
searchWithinHosts: boolean,
@@ -363,31 +373,16 @@ function traverseVisibleHostChildren<A, B, C>(
363373
c: C,
364374
): boolean {
365375
while (child !== null) {
366-
if (child.tag === HostComponent) {
367-
if (fn(child, a, b, c)) {
368-
return true;
369-
}
370-
if (searchWithinHosts) {
371-
if (
372-
traverseVisibleHostChildren(
373-
child.child,
374-
searchWithinHosts,
375-
fn,
376-
a,
377-
b,
378-
c,
379-
)
380-
) {
381-
return true;
382-
}
383-
}
376+
if (child.tag === HostComponent && fn(child, a, b, c)) {
377+
return true;
384378
} else if (
385379
child.tag === OffscreenComponent &&
386380
child.memoizedState !== null
387381
) {
388382
// Skip hidden subtrees
389383
} else {
390384
if (
385+
(searchWithinHosts || child.tag !== HostComponent) &&
391386
traverseVisibleHostChildren(child.child, searchWithinHosts, fn, a, b, c)
392387
) {
393388
return true;

0 commit comments

Comments
 (0)