From f703696b1733ca0213a7132b0043d3a6372da2f6 Mon Sep 17 00:00:00 2001 From: Ethan Standel Date: Wed, 10 Aug 2022 08:16:05 -0400 Subject: [PATCH] [base] Removes invisible tabbable elements from `TrapFocus` component (#33543) --- .../trap-focus/ContainedToggleTrappedFocus.js | 33 +++++++++++++++++++ .../ContainedToggleTrappedFocus.tsx | 33 +++++++++++++++++++ .../ContainedToggleTrappedFocus.tsx.preview | 14 ++++++++ .../base/components/trap-focus/trap-focus.md | 7 ++++ package.json | 1 + packages/mui-base/src/TrapFocus/TrapFocus.js | 11 +++++-- .../mui-base/src/TrapFocus/TrapFocus.test.js | 31 +++++++++++++++++ yarn.lock | 5 +++ 8 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.js create mode 100644 docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx create mode 100644 docs/data/base/components/trap-focus/ContainedToggleTrappedFocus.tsx.preview 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 ba92d417bef06c..dd19e5c70525ea 100644 --- a/docs/data/base/components/trap-focus/trap-focus.md +++ b/docs/data/base/components/trap-focus/trap-focus.md @@ -83,3 +83,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 79702f70b3e88c..3e259e3391a3a7 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@rollup/plugin-replace": "^4.0.0", "@testing-library/dom": "^8.16.1", "@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 c7e6f20a206c21..73241dea2671a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3167,6 +3167,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"