diff --git a/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.js b/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.js new file mode 100644 index 00000000000000..0fb77c38f0aeac --- /dev/null +++ b/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import TrapFocus from '@mui/base/TrapFocus'; + +export default function BasicTrapFocus() { + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + {open && ( + + )} + + + + ); +} diff --git a/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx b/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx new file mode 100644 index 00000000000000..0fb77c38f0aeac --- /dev/null +++ b/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import TrapFocus from '@mui/base/TrapFocus'; + +export default function BasicTrapFocus() { + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + {open && ( + + )} + + + + ); +} diff --git a/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx.preview b/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx.preview new file mode 100644 index 00000000000000..d748ddf123628c --- /dev/null +++ b/docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx.preview @@ -0,0 +1,14 @@ + + + + + + {open && ( + + )} + + \ No newline at end of file diff --git a/docs/data/base/components/trap-focus/trap-focus.md b/docs/data/base/components/trap-focus/trap-focus.md index 627624e3bde9ef..c35e010bfbcf92 100644 --- a/docs/data/base/components/trap-focus/trap-focus.md +++ b/docs/data/base/components/trap-focus/trap-focus.md @@ -84,3 +84,10 @@ When auto focus is disabled—as in the demo below—the component only traps th The following demo uses the [`Portal`](/base/react-portal/) component to render a subset of the `TrapFocus` children into a new "subtree" outside of the current DOM hierarchy, so they are no longer part of the focus loop: {{"demo": "PortalTrapFocus.js"}} + +### Using a toggle inside the trap + +The most common use case for the `TrapFocus` component is to maintain focus within a [modal](/base/react-modal/) component that is entirely separate from the element that opens the modal. +But you can also create a toggle button for the `open` prop of the `TrapFocus` component that is stored inside of the component itself, as shown in the following demo: + +{{"demo": "ContainedToggleTrappedFocus.js"}} diff --git a/package.json b/package.json index 287c842a12ebd2..662c044ddaee6d 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@rollup/plugin-replace": "^4.0.0", "@testing-library/dom": "^8.16.0", "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^14.3.0", "@types/chai": "^4.3.1", "@types/chai-dom": "^0.0.13", "@types/enzyme": "^3.10.12", diff --git a/packages/mui-base/src/TrapFocus/TrapFocus.js b/packages/mui-base/src/TrapFocus/TrapFocus.js index 30b20e59e13f53..09a8f72f77c80a 100644 --- a/packages/mui-base/src/TrapFocus/TrapFocus.js +++ b/packages/mui-base/src/TrapFocus/TrapFocus.js @@ -325,13 +325,18 @@ function TrapFocus(props) { return (
{React.cloneElement(children, { ref: handleRef, onFocus })} -
+
); } diff --git a/packages/mui-base/src/TrapFocus/TrapFocus.test.js b/packages/mui-base/src/TrapFocus/TrapFocus.test.js index d65f3951bab254..26324e14f53c67 100644 --- a/packages/mui-base/src/TrapFocus/TrapFocus.test.js +++ b/packages/mui-base/src/TrapFocus/TrapFocus.test.js @@ -4,6 +4,7 @@ import { expect } from 'chai'; import { act, createRenderer, screen } from 'test/utils'; import TrapFocus from '@mui/base/TrapFocus'; import Portal from '@mui/base/Portal'; +import userEvent from '@testing-library/user-event'; describe('', () => { const { clock, render } = createRenderer(); @@ -283,6 +284,36 @@ describe('', () => { expect(screen.getByTestId('root')).toHaveFocus(); }); + it('does not create any tabbable elements when open={false}', () => { + function Test(props) { + return ( +
+ + +
+ +
+
+ +
+ ); + } + + render(); + + expect(screen.getByTestId('initial-focus')).toHaveFocus(); + act(() => { + userEvent.tab(); + }); + expect(screen.getByTestId('inside-focus')).toHaveFocus(); + act(() => { + userEvent.tab(); + }); + expect(screen.getByTestId('end-focus')).toHaveFocus(); + }); + describe('interval', () => { clock.withFakeTimers(); diff --git a/yarn.lock b/yarn.lock index 5f8a80b7c88c7a..4e937e6619651b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3163,6 +3163,11 @@ "@testing-library/dom" "^8.5.0" "@types/react-dom" "^18.0.0" +"@testing-library/user-event@^14.3.0": + version "14.3.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.3.0.tgz#0a6750b94b40e4739706d41e8efc2ccf64d2aad9" + integrity sha512-P02xtBBa8yMaLhK8CzJCIns8rqwnF6FxhR9zs810flHOBXUYCFjLd8Io1rQrAkQRWEmW2PGdZIEdMxf/KLsqFA== + "@theme-ui/color-modes@0.14.7": version "0.14.7" resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.14.7.tgz#6f93bf4d0890ffe3386df311663eaee9bea40796"