From f83661555206f517d87f0759477e31984b8efa36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:00:49 +0000 Subject: [PATCH 1/4] Initial plan From 0a9325c524cf5a3c81b36aee034ca6d4530ee7b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:06:21 +0000 Subject: [PATCH 2/4] fix: prefer autofocus element over findFirstFocusable in Dialog/Drawer (#35749) Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/32dab34c-d281-4bef-af30-8c263a2a57d6 Co-authored-by: tudorpopams <97875118+tudorpopams@users.noreply.github.com> --- ...-890df3cd-7a3d-4215-a779-d73397bba5f0.json | 7 ++ .../src/utils/useFocusFirstElement.test.ts | 77 +++++++++++++++++++ .../library/src/utils/useFocusFirstElement.ts | 4 +- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 change/@fluentui-react-dialog-890df3cd-7a3d-4215-a779-d73397bba5f0.json create mode 100644 packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts diff --git a/change/@fluentui-react-dialog-890df3cd-7a3d-4215-a779-d73397bba5f0.json b/change/@fluentui-react-dialog-890df3cd-7a3d-4215-a779-d73397bba5f0.json new file mode 100644 index 00000000000000..f9c77014a94d24 --- /dev/null +++ b/change/@fluentui-react-dialog-890df3cd-7a3d-4215-a779-d73397bba5f0.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: prefer autofocus element over first focusable in Dialog/Drawer (#35749)", + "packageName": "@fluentui/react-dialog", + "email": "198982749+Copilot@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts new file mode 100644 index 00000000000000..6773b82a432675 --- /dev/null +++ b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts @@ -0,0 +1,77 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useFocusFinders } from '@fluentui/react-tabster'; +import { useFluent_unstable } from '@fluentui/react-shared-contexts'; +import { useFocusFirstElement } from './useFocusFirstElement'; + +jest.mock('@fluentui/react-tabster'); +jest.mock('@fluentui/react-shared-contexts'); + +const mockFindFirstFocusable = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); + (useFocusFinders as jest.Mock).mockReturnValue({ findFirstFocusable: mockFindFirstFocusable }); + (useFluent_unstable as jest.Mock).mockReturnValue({ targetDocument: document }); +}); + +describe('useFocusFirstElement', () => { + it('focuses the first focusable element when dialog opens and no autofocus element is present', () => { + const container = document.createElement('div'); + const firstButton = document.createElement('button'); + container.appendChild(firstButton); + document.body.appendChild(container); + + mockFindFirstFocusable.mockReturnValue(firstButton); + const focusSpy = jest.spyOn(firstButton, 'focus'); + + const { result, rerender } = renderHook(({ open }) => useFocusFirstElement(open, 'modal'), { + initialProps: { open: false }, + }); + + // Attach the ref to the container + (result.current as React.MutableRefObject).current = container; + + act(() => { + rerender({ open: true }); + }); + + expect(mockFindFirstFocusable).toHaveBeenCalledWith(container); + expect(focusSpy).toHaveBeenCalledTimes(1); + + document.body.removeChild(container); + }); + + it('focuses the autofocus element instead of the first focusable element when autofocus element is present', () => { + const container = document.createElement('div'); + const firstButton = document.createElement('button'); + const autoFocusInput = document.createElement('input'); + autoFocusInput.setAttribute('autofocus', ''); + + container.appendChild(firstButton); + container.appendChild(autoFocusInput); + document.body.appendChild(container); + + mockFindFirstFocusable.mockReturnValue(firstButton); + const firstButtonFocusSpy = jest.spyOn(firstButton, 'focus'); + const autoFocusInputFocusSpy = jest.spyOn(autoFocusInput, 'focus'); + + const { result, rerender } = renderHook(({ open }) => useFocusFirstElement(open, 'modal'), { + initialProps: { open: false }, + }); + + // Attach the ref to the container + (result.current as React.MutableRefObject).current = container; + + act(() => { + rerender({ open: true }); + }); + + // findFirstFocusable should not be called since autofocus element was found + expect(mockFindFirstFocusable).not.toHaveBeenCalled(); + // The autofocus element should be focused, not the first button + expect(autoFocusInputFocusSpy).toHaveBeenCalledTimes(1); + expect(firstButtonFocusSpy).not.toHaveBeenCalled(); + + document.body.removeChild(container); + }); +}); diff --git a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts index ff1a4c7837f30e..3ca58a064f1c1f 100644 --- a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts +++ b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts @@ -21,7 +21,9 @@ export function useFocusFirstElement( if (!open) { return; } - const element = dialogRef.current && findFirstFocusable(dialogRef.current); + const autoFocusElement = + dialogRef.current && (dialogRef.current.querySelector('[autofocus]') ?? undefined); + const element = autoFocusElement || (dialogRef.current && findFirstFocusable(dialogRef.current)); if (element) { element.focus(); } else { From add920412b6f0ce14ed9b3138db7e677ff799764 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:07:12 +0000 Subject: [PATCH 3/4] fix: simplify autofocus element lookup using optional chaining Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/32dab34c-d281-4bef-af30-8c263a2a57d6 Co-authored-by: tudorpopams <97875118+tudorpopams@users.noreply.github.com> --- .../react-dialog/library/src/utils/useFocusFirstElement.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts index 3ca58a064f1c1f..ca140bed0239c4 100644 --- a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts +++ b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.ts @@ -21,8 +21,7 @@ export function useFocusFirstElement( if (!open) { return; } - const autoFocusElement = - dialogRef.current && (dialogRef.current.querySelector('[autofocus]') ?? undefined); + const autoFocusElement = dialogRef.current?.querySelector('[autofocus]'); const element = autoFocusElement || (dialogRef.current && findFirstFocusable(dialogRef.current)); if (element) { element.focus(); From c91b1815ab66f2ad55579bbf8d1414a211805878 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:15:35 +0000 Subject: [PATCH 4/4] fix: resolve lint errors in useFocusFirstElement.test.ts Agent-Logs-Url: https://github.com/microsoft/fluentui/sessions/c48f37af-fb63-4e6c-8a5e-f1f3c5437885 Co-authored-by: tudorpopams <97875118+tudorpopams@users.noreply.github.com> --- .../library/src/utils/useFocusFirstElement.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts index 6773b82a432675..fdd99ab126d7b8 100644 --- a/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts +++ b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts @@ -1,3 +1,4 @@ +import type * as React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { useFocusFinders } from '@fluentui/react-tabster'; import { useFluent_unstable } from '@fluentui/react-shared-contexts'; @@ -29,7 +30,7 @@ describe('useFocusFirstElement', () => { }); // Attach the ref to the container - (result.current as React.MutableRefObject).current = container; + (result.current as React.RefObject).current = container; act(() => { rerender({ open: true }); @@ -60,7 +61,7 @@ describe('useFocusFirstElement', () => { }); // Attach the ref to the container - (result.current as React.MutableRefObject).current = container; + (result.current as React.RefObject).current = container; act(() => { rerender({ open: true });