mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Allow fragment refs to attempt focus/focusLast on nested host children (#33058)
This enables `focus` and `focusLast` methods on FragmentInstances to
search nested host components, depth first. Attempts focus on each child
and bails if one is successful. Previously, only the first level of host
children would attempt focus.
Now if we have an example like
```
component MenuItem() {
return (<div><a>{...}</a></div>)
}
component Menu() {
return <Fragment>{items.map(i => <MenuItem i={i} />)}</Fragment>
}
```
We can target focus on the first or last a tag, rather than checking
each wrapping div and then noop.
This commit is contained in:
parent
4a702865dd
commit
4206fe4982
|
|
@ -43,11 +43,18 @@ export default function FocusCase() {
|
|||
</Fixture.Controls>
|
||||
<div className="highlight-focused-children" style={{display: 'flex'}}>
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div style={{outline: '1px solid black'}}>Unfocusable div</div>
|
||||
<div style={{outline: '1px solid black'}}>
|
||||
<p>Unfocusable div</p>
|
||||
</div>
|
||||
<div style={{outline: '1px solid black'}}>
|
||||
<p>Unfocusable div with nested focusable button</p>
|
||||
<button>Button 1</button>
|
||||
</div>
|
||||
<button>Button 2</button>
|
||||
<input type="text" placeholder="Input field" />
|
||||
<div style={{outline: '1px solid black'}}>Unfocusable div</div>
|
||||
<div style={{outline: '1px solid black'}}>
|
||||
<p>Unfocusable div</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
</div>
|
||||
</Fixture>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Fiber> = [];
|
||||
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)) {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="child-a">
|
||||
<div tabIndex={0} id="grandchild-a">
|
||||
<a id="greatgrandchild-a" href="/" />
|
||||
</div>
|
||||
</div>
|
||||
<a id="child-b" href="/" />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
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 (
|
||||
<Fragment ref={fragmentRef}>
|
||||
<div id="child-a" href="/">
|
||||
<a id="grandchild-a" href="/" />
|
||||
<a id="grandchild-b" href="/" />
|
||||
</div>
|
||||
<div tabIndex={0} id="child-b">
|
||||
<a id="grandchild-a" href="/" />
|
||||
<a id="grandchild-b" href="/" />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
await act(() => {
|
||||
root.render(<Test />);
|
||||
});
|
||||
await act(() => {
|
||||
fragmentRef.current.focusLast();
|
||||
});
|
||||
expect(document.activeElement.id).toEqual('grandchild-b');
|
||||
});
|
||||
});
|
||||
|
||||
describe('blur()', () => {
|
||||
|
|
|
|||
|
|
@ -354,6 +354,16 @@ export function traverseFragmentInstance<A, B, C>(
|
|||
traverseVisibleHostChildren(fragmentFiber.child, false, fn, a, b, c);
|
||||
}
|
||||
|
||||
export function traverseFragmentInstanceDeeply<A, B, C>(
|
||||
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<A, B, C>(
|
||||
child: Fiber | null,
|
||||
searchWithinHosts: boolean,
|
||||
|
|
@ -363,24 +373,8 @@ function traverseVisibleHostChildren<A, B, C>(
|
|||
c: C,
|
||||
): boolean {
|
||||
while (child !== null) {
|
||||
if (child.tag === HostComponent) {
|
||||
if (fn(child, a, b, c)) {
|
||||
if (child.tag === HostComponent && fn(child, a, b, c)) {
|
||||
return true;
|
||||
}
|
||||
if (searchWithinHosts) {
|
||||
if (
|
||||
traverseVisibleHostChildren(
|
||||
child.child,
|
||||
searchWithinHosts,
|
||||
fn,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
child.tag === OffscreenComponent &&
|
||||
child.memoizedState !== null
|
||||
|
|
@ -388,6 +382,7 @@ function traverseVisibleHostChildren<A, B, C>(
|
|||
// Skip hidden subtrees
|
||||
} else {
|
||||
if (
|
||||
(searchWithinHosts || child.tag !== HostComponent) &&
|
||||
traverseVisibleHostChildren(child.child, searchWithinHosts, fn, a, b, c)
|
||||
) {
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user