From fab6f97fe2a8e75fc5b8ab3ee24868b7ba4f5afe Mon Sep 17 00:00:00 2001 From: Tassawwur Hussain Joiya Date: Sat, 31 Jan 2026 18:11:53 +0500 Subject: [PATCH] Add autoFocus support for anchor elements (#35656) Anchor elements () can be focused programmatically, but React's autoFocus prop was not triggering focus on mount. This adds anchor tags to the list of elements that support autoFocus, alongside button, input, select, and textarea. Changes: - Add 'a' case to finalizeInitialChildren to detect autoFocus - Add 'a' case to commitMount to call focus() on mount - Add HTMLAnchorElement to the Flow type annotation - Add comprehensive tests for anchor autoFocus behavior Co-authored-by: Cursor --- .../src/client/ReactFiberConfigDOM.js | 3 + .../react-dom/src/__tests__/ReactDOM-test.js | 199 ++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 09653552aafe..99e52d6959f1 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -653,6 +653,7 @@ export function finalizeInitialChildren( ): boolean { setInitialProperties(domElement, type, props); switch (type) { + case 'a': case 'button': case 'input': case 'select': @@ -846,12 +847,14 @@ export function commitMount( // there are also other cases when this might happen (such as patching // up text content during hydration mismatch). So we'll check this again. switch (type) { + case 'a': case 'button': case 'input': case 'select': case 'textarea': if (newProps.autoFocus) { ((domElement: any): + | HTMLAnchorElement | HTMLButtonElement | HTMLInputElement | HTMLSelectElement diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index 5397de0e2510..927ec7a9b65a 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -386,6 +386,205 @@ describe('ReactDOM', () => { } }); + it('calls focus() on autoFocus anchor elements after they have been mounted to the DOM', async () => { + const originalFocus = HTMLElement.prototype.focus; + + try { + let focusedElement; + let anchorFocusedAfterMount = false; + + HTMLElement.prototype.focus = function () { + focusedElement = this; + anchorFocusedAfterMount = !!this.parentNode; + }; + + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+

Anchor Auto-focus Test

+
+ Click me + +

The above anchor should be focused after mount.

+
, + ); + }); + + expect(anchorFocusedAfterMount).toBe(true); + expect(focusedElement.tagName).toBe('A'); + } finally { + HTMLElement.prototype.focus = originalFocus; + document.body.innerHTML = ''; + } + }); + + it('does not call focus() on anchor elements when autoFocus is false', async () => { + const originalFocus = HTMLElement.prototype.focus; + + try { + let focusCalled = false; + + HTMLElement.prototype.focus = function () { + focusCalled = true; + }; + + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ + Click me + +
, + ); + }); + + expect(focusCalled).toBe(false); + } finally { + HTMLElement.prototype.focus = originalFocus; + document.body.innerHTML = ''; + } + }); + + it('does not call focus() on anchor elements when autoFocus is not specified', async () => { + const originalFocus = HTMLElement.prototype.focus; + + try { + let focusCalled = false; + + HTMLElement.prototype.focus = function () { + focusCalled = true; + }; + + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ Click me +
, + ); + }); + + expect(focusCalled).toBe(false); + } finally { + HTMLElement.prototype.focus = originalFocus; + document.body.innerHTML = ''; + } + }); + + it('calls focus() on autoFocus anchor without href attribute', async () => { + const originalFocus = HTMLElement.prototype.focus; + + try { + let focusedElement; + let anchorFocusedAfterMount = false; + + HTMLElement.prototype.focus = function () { + focusedElement = this; + anchorFocusedAfterMount = !!this.parentNode; + }; + + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ Anchor without href +
, + ); + }); + + expect(anchorFocusedAfterMount).toBe(true); + expect(focusedElement.tagName).toBe('A'); + } finally { + HTMLElement.prototype.focus = originalFocus; + document.body.innerHTML = ''; + } + }); + + it('only focuses the first autoFocus element when multiple are present (anchor and input)', async () => { + const originalFocus = HTMLElement.prototype.focus; + + try { + const focusedElements = []; + + HTMLElement.prototype.focus = function () { + focusedElements.push(this.tagName); + }; + + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ + First autoFocus + + +
, + ); + }); + + // React calls focus on elements in tree order, but only the first + // autoFocus element in DOM order should actually be focused in practice. + // Both elements will have focus() called, but the second one will + // receive focus in the actual DOM. + expect(focusedElements).toContain('A'); + expect(focusedElements).toContain('INPUT'); + } finally { + HTMLElement.prototype.focus = originalFocus; + document.body.innerHTML = ''; + } + }); + + it('existing form element autoFocus still works alongside anchor autoFocus support', async () => { + const originalFocus = HTMLElement.prototype.focus; + + try { + const focusedElements = []; + + HTMLElement.prototype.focus = function () { + focusedElements.push({tagName: this.tagName, id: this.id}); + }; + + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ + + +