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 0000000000000..f9c77014a94d2 --- /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 0000000000000..fdd99ab126d7b --- /dev/null +++ b/packages/react-components/react-dialog/library/src/utils/useFocusFirstElement.test.ts @@ -0,0 +1,78 @@ +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'; +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.RefObject).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.RefObject).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 ff1a4c7837f30..ca140bed0239c 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,8 @@ export function useFocusFirstElement( if (!open) { return; } - const element = dialogRef.current && findFirstFocusable(dialogRef.current); + const autoFocusElement = dialogRef.current?.querySelector('[autofocus]'); + const element = autoFocusElement || (dialogRef.current && findFirstFocusable(dialogRef.current)); if (element) { element.focus(); } else {