Fix focus set for delegated and already focused elements#688
Conversation
I found two focus bugs when working on documentation for Fragment Refs. 1) If an element delegates focus handling, it will return false from setFocusIfFocusable even though a focus event has occured on a different element. The fix for this is a document level event listener rather than only listening on the current element. For example, if you have a form with multiple nested label>inputs. Calling focus on the label will focus its input but not fire an event on the label. setFocusIfFocusable returns false and you end up continuing to attempt focus down the form tree. 2) If an element is already focused, setFocusIfFocusable will return false. The fix for this is checking the document's activeElement with an early return. In the same form example, if the first input is already focused and you call fragmentInstance.focus() at the form level, the second input would end up getting focused since the focus event on the first is not triggered.
Greptile SummaryThis PR fixes two related bugs in
Confidence Score: 4/5
Important Files Changed
Last reviewed commit: ea8b0fb |
| <a id="sibling-link" href="/"> | ||
| Link | ||
| </a> | ||
| </Fragment> | ||
| ); | ||
| } | ||
|
|
||
| await act(() => { | ||
| root.render(<Test />); | ||
| }); | ||
|
|
||
| // Focus the nested input manually | ||
| document.getElementById('nested-input').focus(); | ||
| expect(document.activeElement.id).toEqual('nested-input'); | ||
|
|
||
| // Calling fragment.focus() should keep focus on nested-input | ||
| await act(() => { | ||
| fragmentRef.current.focus(); | ||
| }); | ||
| expect(document.activeElement.id).toEqual('nested-input'); | ||
| document.activeElement.blur(); | ||
| }); | ||
|
|
||
| // @gate enableFragmentRefs | ||
| it('focuses the first focusable child in a fieldset', async () => { | ||
| const fragmentRef = React.createRef(); | ||
| const root = ReactDOMClient.createRoot(container); | ||
|
|
||
| function Test() { | ||
| return ( | ||
| <Fragment ref={fragmentRef}> | ||
| <fieldset> |
There was a problem hiding this comment.
Missing direct test for label-delegation fix
Bug fix #1 (the document-level capture listener) is specifically motivated by <label> elements delegating focus to their associated <input>. The new fieldset test exercises the existing depth-first traversal but does not exercise the new code path — a <fieldset> is simply not focusable, so fieldset.focus() fires no focus event at all. The document-level listener only matters when .focus() on element A causes a focus event to fire on a different element B (e.g. label → input).
Consider adding a test like the one below to directly validate the new listener:
// @gate enableFragmentRefs
it('focuses the associated input when calling focus on a label', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRef}>
<label htmlFor="name-input">Name</label>
<input id="name-input" />
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('name-input');
document.activeElement.blur();
});(Note: if JSDOM does not implement <label> focus delegation, a shadow-host with delegatesFocus would be the alternative vehicle for this test.)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
Line: 305-336
Comment:
**Missing direct test for label-delegation fix**
Bug fix #1 (the document-level capture listener) is specifically motivated by `<label>` elements delegating focus to their associated `<input>`. The new fieldset test exercises the existing depth-first traversal but does not exercise the new code path — a `<fieldset>` is simply not focusable, so `fieldset.focus()` fires no focus event at all. The document-level listener only matters when `.focus()` on element A causes a focus event to fire on a **different** element B (e.g. `label → input`).
Consider adding a test like the one below to directly validate the new listener:
```js
// @gate enableFragmentRefs
it('focuses the associated input when calling focus on a label', async () => {
const fragmentRef = React.createRef();
const root = ReactDOMClient.createRoot(container);
function Test() {
return (
<Fragment ref={fragmentRef}>
<label htmlFor="name-input">Name</label>
<input id="name-input" />
</Fragment>
);
}
await act(() => {
root.render(<Test />);
});
await act(() => {
fragmentRef.current.focus();
});
expect(document.activeElement.id).toEqual('name-input');
document.activeElement.blur();
});
```
(Note: if JSDOM does not implement `<label>` focus delegation, a shadow-host with `delegatesFocus` would be the alternative vehicle for this test.)
How can I resolve this? If you propose a fix, please make it concise.|
Upstream PR was closed or merged. Code is synced via branch mirror. |
Mirror of facebook/react#36010
Original author: jackpope
I found two focus bugs when working on documentation for Fragment Refs.
For example, if you have a form with multiple nested label>inputs. Calling focus on the label will focus its input but not fire an event on the label. setFocusIfFocusable returns false and you end up continuing to attempt focus down the form tree.
In the same form example, if the first input is already focused and you call fragmentInstance.focus() at the form level, the second input would end up getting focused since the focus event on the first is not triggered.