Skip to content

Commit fe51ced

Browse files
committed
Allow fragment refs to attempt focus/focusLast on nested host children
1 parent 88b9767 commit fe51ced

File tree

5 files changed

+106
-12
lines changed

5 files changed

+106
-12
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
@@ -60,6 +60,7 @@ import {
6060
} from './ReactDOMComponentTree';
6161
import {
6262
traverseFragmentInstance,
63+
traverseFragmentInstanceDeeply,
6364
getFragmentParentHostInstance,
6465
} from 'react-reconciler/src/ReactFiberTreeReflection';
6566

@@ -2610,7 +2611,7 @@ FragmentInstance.prototype.focus = function (
26102611
this: FragmentInstanceType,
26112612
focusOptions?: FocusOptions,
26122613
): void {
2613-
traverseFragmentInstance(
2614+
traverseFragmentInstanceDeeply(
26142615
this._fragmentFiber,
26152616
setFocusIfFocusable,
26162617
focusOptions,
@@ -2622,7 +2623,11 @@ FragmentInstance.prototype.focusLast = function (
26222623
focusOptions?: FocusOptions,
26232624
): void {
26242625
const children: Array<Instance> = [];
2625-
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
2626+
traverseFragmentInstanceDeeply(
2627+
this._fragmentFiber,
2628+
collectChildren,
2629+
children,
2630+
);
26262631
for (let i = children.length - 1; i >= 0; i--) {
26272632
const child = children[i];
26282633
if (setFocusIfFocusable(child, focusOptions)) {

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,33 @@ describe('FragmentRefs', () => {
140140
document.activeElement.blur();
141141
});
142142

143+
// @gate enableFragmentRefs
144+
it('focuses deeply nested focusable children, depth first', async () => {
145+
const fragmentRef = React.createRef();
146+
const root = ReactDOMClient.createRoot(container);
147+
const childRef = React.createRef();
148+
149+
function Test() {
150+
return (
151+
<Fragment ref={fragmentRef}>
152+
<div id="child-a">
153+
<div tabIndex={0} id="grandchild-a">
154+
<a id="greatgrandchild-a" href="/"></a>
155+
</div>
156+
</div>
157+
<a id="child-b" href="/"></a>
158+
</Fragment>
159+
);
160+
}
161+
await act(() => {
162+
root.render(<Test />);
163+
});
164+
await act(() => {
165+
fragmentRef.current.focus();
166+
});
167+
expect(document.activeElement.id).toEqual('grandchild-a');
168+
});
169+
143170
// @gate enableFragmentRefs
144171
it('preserves document order when adding and removing children', async () => {
145172
const fragmentRef = React.createRef();
@@ -223,6 +250,35 @@ describe('FragmentRefs', () => {
223250
expect(document.activeElement.id).toEqual('child-c');
224251
document.activeElement.blur();
225252
});
253+
254+
// @gate enableFragmentRefs
255+
it('focuses deeply nested focusable children, depth first', async () => {
256+
const fragmentRef = React.createRef();
257+
const root = ReactDOMClient.createRoot(container);
258+
const childRef = React.createRef();
259+
260+
function Test() {
261+
return (
262+
<Fragment ref={fragmentRef}>
263+
<div id="child-a" href="/">
264+
<a id="grandchild-a" href="/"></a>
265+
<a id="grandchild-b" href="/"></a>
266+
</div>
267+
<div tabIndex={0} id="child-b">
268+
<a id="grandchild-a" href="/"></a>
269+
<a id="grandchild-b" href="/"></a>
270+
</div>
271+
</Fragment>
272+
);
273+
}
274+
await act(() => {
275+
root.render(<Test />);
276+
});
277+
await act(() => {
278+
fragmentRef.current.focusLast();
279+
});
280+
expect(document.activeElement.id).toEqual('grandchild-b');
281+
});
226282
});
227283

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

packages/react-reconciler/src/ReactFiberTreeReflection.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,31 +352,53 @@ export function traverseFragmentInstance<A, B, C>(
352352
b: B,
353353
c: C,
354354
): void {
355-
traverseFragmentInstanceChildren(fragmentFiber.child, fn, a, b, c);
355+
traverseFragmentInstanceChildren(fragmentFiber.child, false, fn, a, b, c);
356+
}
357+
358+
export function traverseFragmentInstanceDeeply<A, B, C>(
359+
fragmentFiber: Fiber,
360+
fn: (Instance, A, B, C) => boolean,
361+
a: A,
362+
b: B,
363+
c: C,
364+
): void {
365+
traverseFragmentInstanceChildren(fragmentFiber.child, true, fn, a, b, c);
356366
}
357367

358368
function traverseFragmentInstanceChildren<A, B, C>(
359369
child: Fiber | null,
370+
searchWithinHosts: boolean,
360371
fn: (Instance, A, B, C) => boolean,
361372
a: A,
362373
b: B,
363374
c: C,
364-
): void {
375+
): boolean {
365376
while (child !== null) {
366-
if (child.tag === HostComponent) {
367-
if (fn(child.stateNode, a, b, c)) {
368-
return;
369-
}
377+
if (child.tag === HostComponent && fn(child.stateNode, a, b, c)) {
378+
return true;
370379
} else if (
371380
child.tag === OffscreenComponent &&
372381
child.memoizedState !== null
373382
) {
374383
// Skip hidden subtrees
375384
} else {
376-
traverseFragmentInstanceChildren(child.child, fn, a, b, c);
385+
if (
386+
(searchWithinHosts || child.tag !== HostComponent) &&
387+
traverseFragmentInstanceChildren(
388+
child.child,
389+
searchWithinHosts,
390+
fn,
391+
a,
392+
b,
393+
c,
394+
)
395+
) {
396+
return true;
397+
}
377398
}
378399
child = child.sibling;
379400
}
401+
return false;
380402
}
381403

382404
export function getFragmentParentHostInstance(fiber: Fiber): null | Instance {

0 commit comments

Comments
 (0)