New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[base] Removes invisible tabbable elements from TrapFocus
component
#33543
Conversation
TrapFocus
component (fixes #33380)TrapFocus
component
@@ -283,6 +283,28 @@ describe('<TrapFocus />', () => { | |||
expect(screen.getByTestId('root')).toHaveFocus(); | |||
}); | |||
|
|||
it('does not allow sentinelStart or sentinelEnd to be tabbable until open={true}', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of testing the tabindex
value, I would suggest testing the behavior, as the implementation may change in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mnajdova done! However, to add real tabbability testing, I needed to add @testing-library/user-event
which has an awkward naming conflict with test/utils/userEvent.ts
and I don't know if you have a preferred solution for that situation.
cc @michaldudak would be great to have a second opinion on the changes. @samuelsycamore for reviewing the copywriting. |
I haven't thought of such a use case before, but these changes look OK to me. |
When I tried to resync the |
@michaldudak I really like the idea of the prop being |
No, not at all, this should be a separate PR that we can handle on our own. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solid addition! I just have a couple suggestions for grammar and style in the docs.
|
||
## 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 which is entire separete from the element which would open the modal. However, there are use cases where you may want to have a toggle button for the `open` prop of the `TrapFocus` component which is stored inside that component. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The most common use case for the `TrapFocus` component is to maintain focus within a [`Modal`](/base/react-modal/) component which is entire separete from the element which would open the modal. However, there are use cases where you may want to have a toggle button for the `open` prop of the `TrapFocus` component which is stored inside that component. | |
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, except for one thing. I think it should be "Modal
" and not "modal" because directly above this section, the Portal component is referenced in this exact same way. 😄
Same deal as before. Next time I won't open a PR off a fork from |
Requesting a re-review from @samuelsycamore & @mnajdova ! 😄 Is that how I should do that? I legitimately don't know. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really great first contribution to MUI @EthanStandel 👌 I look forward for more of these 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@EthanStandel Thanks for working on this. Should we close #33380? I assume it's fixed.
@@ -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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAIK @eps1lon has been pushing toward not using this plugin, I would be in favor of doing such as well. The logic that implements userEvent.tab();
looks brittle, I don't think that we should trust it for tests. Instead, we could continue to use the real browser tab as the existing tests are doing in:
material-ui/test/e2e/index.test.ts
Line 118 in 8d6bae2
describe('<TrapFocus />', () => { |
How about we do a follow-up to remove it?
import userEvent from '@testing-library/user-event'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't we already have Playwright tests for TrapFocus or any other more involved user-interaction tests? I would still recomend using that approach over using user-event
. Especiallly for a component library. Unless you have good reason for using JSDOM + replaying events in which case I'd be curious to hear why you'd prefer JSDOm + replaying events over browser tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be fine with moving the meaning of this test to Playwright and removing the new dependency. If I'm to be honest, I didn't notice the e2e tests initially 😅
import Stack from '@mui/material/Stack'; | ||
import TrapFocus from '@mui/base/TrapFocus'; | ||
|
||
export default function BasicTrapFocus() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name doesn't match
export default function BasicTrapFocus() { | |
export default function ContainedToggleTrappedFocus() { |
<Box | ||
sx={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
flexDirection: 'column', | ||
}} | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary, we can simplify the demo without
<Box | |
sx={{ | |
display: 'flex', | |
alignItems: 'center', | |
flexDirection: 'column', | |
}} | |
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going for consistency with the other demos
e.g. DisableEnforceFocus, LazyTrapFocus, PortalTrapFocus
Would you suggest that they all be changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nevermind I went to do that and then immediately saw why it's necessary in those cases and not this one!
<Stack> | ||
<Stack alignItems="center"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would have been simpler like this, we don't need two Stack.
<Stack> | |
<Stack alignItems="center"> | |
<Stack alignItems="center" spacing={2}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense!
Problem description
Fix #33380
Solution
Setting
tabIndex={open ? 0 : -1}
onsentinelStart
andsentinelEnd
elements so that they're only tabbable when theTrapFocus
open
prop istrue
. Thus removing the awkward experience where there are invisible tabbable elements.Preview: https://deploy-preview-33543--material-ui.netlify.app/base/react-trap-focus/