diff --git a/.changeset/thirty-pets-impress.md b/.changeset/thirty-pets-impress.md new file mode 100644 index 00000000000..abb4f39f643 --- /dev/null +++ b/.changeset/thirty-pets-impress.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Adds dependencies to `Dialog` focus trap to ensure focus trap is reset when content within changes diff --git a/package-lock.json b/package-lock.json index 5fbb3a366ea..b3891397e82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -292,7 +292,7 @@ "name": "example-app-router", "version": "0.0.0", "dependencies": { - "@primer/react": "37.0.0-rc.1", + "@primer/react": "37.0.0-rc.3", "next": "^14.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -311,7 +311,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { - "@primer/react": "37.0.0-rc.1", + "@primer/react": "37.0.0-rc.3", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.11.0", @@ -329,7 +329,7 @@ "name": "example-consumer-test", "version": "0.0.0", "dependencies": { - "@primer/react": "37.0.0-rc.1", + "@primer/react": "37.0.0-rc.3", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.19", "@types/styled-components": "^5.1.11", @@ -7616,13 +7616,9 @@ } }, "node_modules/@primer/behaviors": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.7.0.tgz", - "integrity": "sha512-C0yY6XqYaqmGANX+ALF259hGGaG2i70tDjzMR7YyahW6Iwv8a7znaQK58o2AVtlwxo6CC6Vn/ZJU0Ea1djiu2w==", - "license": "MIT", - "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.18.0" - } + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.7.2.tgz", + "integrity": "sha512-I5dGgtzV9n1ZX3J1KHkLVWjvEbzanstaXFTDr/+tdn4E2GAA/NUHfTLMq6i5+Pt4P/p/paLI50EgExElENzCYQ==" }, "node_modules/@primer/component-metadata": { "version": "0.5.1", @@ -62800,7 +62796,7 @@ }, "packages/react": { "name": "@primer/react", - "version": "37.0.0-rc.1", + "version": "37.0.0-rc.3", "license": "MIT", "dependencies": { "@github/combobox-nav": "^2.1.5", @@ -62810,7 +62806,7 @@ "@github/tab-container-element": "^4.8.0", "@lit-labs/react": "1.2.1", "@oddbird/popover-polyfill": "^0.3.1", - "@primer/behaviors": "^1.7.0", + "@primer/behaviors": "^1.7.2", "@primer/live-region-element": "^0.7.0", "@primer/octicons-react": "^19.9.0", "@primer/primitives": "^7.16.0", diff --git a/packages/react/package.json b/packages/react/package.json index 7bd59410ab3..0e956bf9271 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -95,7 +95,7 @@ "@github/tab-container-element": "^4.8.0", "@lit-labs/react": "1.2.1", "@oddbird/popover-polyfill": "^0.3.1", - "@primer/behaviors": "^1.7.0", + "@primer/behaviors": "^1.7.2", "@primer/live-region-element": "^0.7.0", "@primer/octicons-react": "^19.9.0", "@primer/primitives": "^7.16.0", diff --git a/packages/react/src/Dialog/Dialog.features.stories.tsx b/packages/react/src/Dialog/Dialog.features.stories.tsx index 7f694ce9908..264fa3440e6 100644 --- a/packages/react/src/Dialog/Dialog.features.stories.tsx +++ b/packages/react/src/Dialog/Dialog.features.stories.tsx @@ -143,7 +143,7 @@ export const StressTest = ({width, height, subtitle}: DialogStoryProps) => { footerButtons={[ ...manyButtons, {buttonType: 'danger', content: 'Delete the universe', onClick: onDialogClose}, - {buttonType: 'primary', content: 'Proceed', onClick: openSecondDialog, autoFocus: true}, + {buttonType: 'primary', content: 'Proceed', onClick: openSecondDialog}, ]} > {lipsum} @@ -164,6 +164,10 @@ export const ReproMultistepDialogWithConditionalFooter = ({width, height}: Dialo const onDialogClose = useCallback(() => setIsOpen(false), []) const [step, setStep] = React.useState(1) + const [inputText, setInputText] = React.useState('') + + const dialogRef = useRef(null) + const renderFooterConditionally = () => { if (step === 1) return null @@ -174,6 +178,14 @@ export const ReproMultistepDialogWithConditionalFooter = ({width, height}: Dialo ) } + React.useEffect(() => { + // focus the close button when the step changes + const focusTarget = dialogRef.current?.querySelector('button[aria-label="Close"]') as HTMLButtonElement + if (step === 2) { + focusTarget.focus() + } + }, [step]) + return ( <> @@ -185,6 +197,7 @@ export const ReproMultistepDialogWithConditionalFooter = ({width, height}: Dialo renderFooter={renderFooterConditionally} onClose={onDialogClose} footerButtons={[{buttonType: 'primary', content: 'Proceed'}]} + ref={dialogRef} > {step === 1 ? ( @@ -196,12 +209,17 @@ export const ReproMultistepDialogWithConditionalFooter = ({width, height}: Dialo ) : ( -

+

- + setInputText(event.target.value)} + /> -

+
)} )} @@ -330,3 +348,56 @@ export const NewIssues = () => { ) } + +export const RetainsFocusTrapWithDynamicContent = () => { + const [isOpen, setIsOpen] = useState(false) + const [secondOpen, setSecondOpen] = useState(false) + const [expandContent, setExpandContent] = useState(false) + const [changeBodyContent, setChangeBodyContent] = useState(false) + + const buttonRef = useRef(null) + const onDialogClose = useCallback(() => setIsOpen(false), []) + const onSecondDialogClose = useCallback(() => setSecondOpen(false), []) + const openSecondDialog = useCallback(() => setSecondOpen(true), []) + + const renderFooterConditionally = () => { + if (!changeBodyContent) return null + + return ( + + + + ) + } + + return ( + <> + + {isOpen && ( + + + + + {expandContent && ( + + {lipsum} + + + + )} + {secondOpen && ( + + Hello world + + )} + + )} + + ) +} diff --git a/packages/react/src/Dialog/Dialog.stories.tsx b/packages/react/src/Dialog/Dialog.stories.tsx index 2cbba5354e1..ac6f1b1e58e 100644 --- a/packages/react/src/Dialog/Dialog.stories.tsx +++ b/packages/react/src/Dialog/Dialog.stories.tsx @@ -77,7 +77,7 @@ export const Default = () => { footerButtons={[ {buttonType: 'default', content: 'Open Second Dialog', onClick: openSecondDialog}, {buttonType: 'danger', content: 'Delete the universe', onClick: onDialogClose}, - {buttonType: 'primary', content: 'Proceed', onClick: openSecondDialog, autoFocus: true}, + {buttonType: 'primary', content: 'Proceed', onClick: openSecondDialog}, ]} > {lipsum} @@ -119,7 +119,7 @@ export const Playground = ( footerButtons={[ {buttonType: 'default', content: 'Open Second Dialog', onClick: openSecondDialog}, {buttonType: 'danger', content: 'Delete the universe', onClick: onDialogClose}, - {buttonType: 'primary', content: 'Proceed', onClick: openSecondDialog, autoFocus: true}, + {buttonType: 'primary', content: 'Proceed', onClick: openSecondDialog}, ]} > {lipsum}