Skip to content
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

[FocusTrap] Correctly focus on the correct element on initial focus #37672

Open
wants to merge 20 commits into
base: master
Choose a base branch
from

Conversation

Greg-NetDuma
Copy link

@Greg-NetDuma Greg-NetDuma commented Jun 22, 2023

with a fallback to the root component.

The Problem:
Dialog has a DOM layout something like this:

<modal>
   <backdrop>
  <focusTrap start />
   <dialog tabindex="-1" role="presentation">
        <dialogPaper role="dialog">
        </dialogPaper>
   </dialog>
  <focusTrap end />
</modal>

Modal applies tabindex to dialog for accessibility reasons (see link for possible explanation) and FocusTrap automatically focuses on it's direct child (Dialog). But Dialog has an incorrect role set in Dialog.js for this specific case.

According to the guidelines the modal should focus on either:

  • The first tabbable element
  • The element that makes most sense
  • On the first element specified by tabindex={-1}

The Solution
Make sure FocusTrap immediately focuses on the first tabbable element including elements with tabIndex={-1} set. Regular tabbing should ignore them. I also removed Modal injecting tabindex to the root child forcefully (the only place we seem to use FocusTrap)

This means

  • Modal doesn't need to forcefully apply tabindex="-1" to it's child
  • FocusTrap will still apply tabindex="-1" to its direct child if no better candidate was found (and complain, currently)
  • By adding tabIndex={-1} to the element that actually has "role="dialog" in Dialog will be announced correctly by screenreaders.
  • By adding a new prop to Dialog where users can disable adding tabIndex={-1} to the content they can properly control the focus order including the first focus, where they can just choose to set tabIndex={-1} on another element inside the dialog or just let FocusTrap use the implicit/explicit tabindex order.

Alternate Solution
We could just reverse the how roles and aria-props applied from DialogPaper to Dialog instead of all of this. We still can't implement something that controls the initial focus, but it is much simpler.


This also fixes #42989


@mui-bot
Copy link

mui-bot commented Jun 22, 2023

Netlify deploy preview

https://deploy-preview-37672--material-ui.netlify.app/

Bundle size report

Details of bundle changes (Toolpad)
Details of bundle changes

Generated by 🚫 dangerJS against 3b7de07

@Greg-NetDuma Greg-NetDuma changed the title [FocusTrap] On open focus on the first component that is tabbable including tabIndex={-1} [FocusTrap] Correctly focus on the correct element on initial focus Jun 22, 2023
@Greg-NetDuma
Copy link
Author

Greg-NetDuma commented Jun 22, 2023

Test Cases

are not written. Tests are numbered and bullet points note "expected input" -> "expected output"

FocusTrap

  1. On open check the current focus
    • 2 tabbable elements inside -> First one is the document.activeElement
    • 2 tabbable elements plus one more set to tabindex="-1" inside -> the one containing tabindex="-1" is is the document.activeElement
    • no tabbale elements inside -> the root element is the document.activeElement and tabindex="-1" is automatically set on it. FocusTrap will post console.error

Dialog

  1. On Dialog open -> DialogPaper should be the document.activeElement with tabindex="-1" on it.
  2. On Dialog open and disableInitialContentFocus={true} -> FocusTrap test 1. test cases

@Greg-NetDuma
Copy link
Author

Please help me find a better name for disableInitialContentFocus. Its for disabling focusing on DialogPaper element on opening, this causes something else that is tabbable/set to tabindex=1 to be focused inside the dialog.

@Greg-NetDuma
Copy link
Author

Greg-NetDuma commented Jun 22, 2023

How this affects other Components

In general tests are failing because now that Modal doesn't set tabIndex on it's child there are modals without any focusable elements inside them (in tests).

The question is should I fix the tests so if you use the modal you are expected to do that manually or should I allow FocusTrap to gracefully handle it instead? Probably the former.

Menu and MenuList

https://github.com/Greg-NetDuma/material-ui/blob/0f44030a4020ea0b5c93eefc8c90d799b7ce4f22/packages/mui-material/src/Menu/Menu.test.js#L203

Menu looks like this:

<Menu (modal)>
  <FocusTrap start />
  <MenuList />
  <FocusTrap end />
</Menu (modal)>

Tests are failing because if you set autoFocus={false} it somehow expects the focus to be on the Modal outside FocusTrap.

@zannager zannager added the component: FocusTrap The React component. label Jun 22, 2023
@Greg-NetDuma
Copy link
Author

For whoever review this, I appeased the CI gods as much as I can, but I can't seem to generate the docs for some reason.

Also it's not possible for me spend more time on this, if someone can take over to write the new tests based on the test cases I've written and generate the docs I would appreciate that.

@mj12albert
Copy link
Member

mj12albert commented Jul 6, 2023

@Greg-NetDuma Thanks for working on this 💜 I've pushed a commit to generate the docs

Would you mind giving me a TLDR of the state of the tests before I dig into this? 🙏

@Greg-NetDuma
Copy link
Author

@mj12albert All existing tests are fixed and passing. (Most of the changes are to make sure all test Modals used have tabbable elements inside now that Modal doesn't mess with tabIndexes - FocusTrap will still inject tabIndex={-1} if it can't find anything with an error)

None of the new tests in #37672 (comment) are written, these are all the cases that I can think of for the changes in this PR.

They need to be implemented. I tried to write the description for them as simple as possible.

@mj12albert mj12albert self-assigned this Jul 10, 2023
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Jul 18, 2023
@ZeeshanTamboli
Copy link
Member

Any updates on this PR?

@Greg-NetDuma
Copy link
Author

@ZeeshanTamboli

Also it's not possible for me spend more time on this, if someone can take over to write the new tests based on the test cases I've written and generate the docs I would appreciate that.

This is the current state of this PR. I'm using this fix in production for the past 6 months and it also passed an accessibility audit as well.

@ZeeshanTamboli
Copy link
Member

ZeeshanTamboli commented Dec 25, 2023

@mj12albert Since you assigned this to yourself, are you planning to work on it by adding the new tests and reviewing the implementation?

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Sep 17, 2024
@Gr3q
Copy link

Gr3q commented Sep 18, 2024

I added all the tests needed and implemented the fix across all your duplicated codebase.

I still can't generate the docs properly because pnpm docs:api still just wipes the docs for me. ll fix as many other CI steps as I can. I still don't know why your argos check is failing.

Will I need to wait another year? It looks like the interval for the accessibility PRs.

@ZeeshanTamboli
Copy link
Member

@michaldudak @DiegoAndai Can you take a look?

@Gr3q
Copy link

Gr3q commented Sep 19, 2024

I'll add 2 more tests to FocusTrap today involving expected behaviour when tabbing inside.

  1. if tabindex=-1 was set on a implicitly tabbable (interactive) element initial focus starts there and it can be focused when tabbing through
  2. if tabindex=-1 was set on a non-interactive element initial focus starts there and it cannot be focused when tabbing through

As far as I remember that's the expected behaviour but I'll re-read the guidelines and check how it behaves now.

Edit: it seems there are some more test failures because of the changes (or maybe not?) I'll have a look at them too.

@michaldudak
Copy link
Member

I'm seeing some typecheck failures as well. I'll try to figure out what's going on.

@Gr3q
Copy link

Gr3q commented Sep 19, 2024

While I was trying to write the test I realized that I'm trying to test is just native browser behaviour. So I only implemented/amended tests for things FocusTrap actually does, like where to it goes next where it reaches it's sentinels.

Initially I thought my implementation logic was wrong but I can see now I implemented it just like https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex#sect2 says.

I'll look at the failure in Select next.

@Gr3q
Copy link

Gr3q commented Sep 19, 2024

I looked at it (/mui-base/**/Select.test.tsx#1257), I'm not sure how to fix it. You might need to revert changes to the lockfile because most of the problems seem to come from that, but probably you know better than me.

@ZeeshanTamboli
Copy link
Member

Should this pointed against master branch?

@michaldudak
Copy link
Member

I believe so. cc @DiegoAndai

@Gr3q
Copy link

Gr3q commented Sep 20, 2024

Is master going to be v6 or is it still going into v5?

If I have to do a backport after it's merged into master I rather open a new PR for it so I don't have to do the work again (for v5)

@ZeeshanTamboli
Copy link
Member

Is master going to be v6 or is it still going into v5?

master is already on v6, the latest stable version of Material UI. If we want this in v5, we could cherrypick it to the v5.x branch, though I’m unsure if or when future v5 releases will happen.

@ZeeshanTamboli
Copy link
Member

@ZeeshanTamboli based on this should I remove all changes inside the mui-base directory?

Yes.

@Gr3q
Copy link

Gr3q commented Sep 24, 2024

I did except the test cases, they fail without those changes.

PS. still can't generate the documentation

@ZeeshanTamboli
Copy link
Member

PS. still can't generate the documentation

Generated.

Copy link
Member

@ZeeshanTamboli ZeeshanTamboli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also fixes #42989 bug

Copy link
Member

@ZeeshanTamboli ZeeshanTamboli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just reverse the how roles and aria-props applied from DialogPaper to Dialog instead of all of this. We still can't implement something that controls the initial focus, but it is much simpler.

This would be a breaking change.

@ZeeshanTamboli ZeeshanTamboli added package: material-ui Specific to @mui/material bug 🐛 Something doesn't work labels Oct 2, 2024
Copy link
Member

@ZeeshanTamboli ZeeshanTamboli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial review

packages/mui-material/src/Drawer/Drawer.js Outdated Show resolved Hide resolved
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Oct 22, 2024
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Oct 26, 2024
Copy link
Member

@ZeeshanTamboli ZeeshanTamboli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Greg-NetDuma I pushed some changes here: commit 1 and commit 2. The PR looks good to me, but I'd like others to review it — cc @DiegoAndai @aarongarciah. I think this should be released in a minor Material UI version once merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something doesn't work component: FocusTrap The React component. package: material-ui Specific to @mui/material
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[material-ui][Dialog] Keyboard scrolling doesn't work in fullscreen mode
7 participants