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

[Modal] Fix focus not being contained #16585

Merged
merged 8 commits into from
Jul 20, 2019

Conversation

eps1lon
Copy link
Member

@eps1lon eps1lon commented Jul 12, 2019

Not sure yet if the test is properly set up but I have codesandbox as a proof of concept.

The issue was that DOM events were confused with React events. In React every event bubbles. In the DOM that's not necessarily the case. Specifically focus does not bubble. Focusout does.

Should close #16584 if done

@eps1lon eps1lon added bug 🐛 Something doesn't work accessibility a11y component: modal This is the name of the generic UI component, not the React module! labels Jul 12, 2019
@mui-pr-bot
Copy link

mui-pr-bot commented Jul 12, 2019

Details of bundle changes.

Comparing: 3b6c822...e4d192c

bundle parsed diff gzip diff prev parsed current parsed prev gzip current gzip
@material-ui/core +0.02% 🔺 +0.03% 🔺 327,255 327,310 90,349 90,374
@material-ui/core/Paper 0.00% 0.00% 68,477 68,477 20,410 20,410
@material-ui/core/Paper.esm 0.00% 0.00% 61,761 61,761 19,177 19,177
@material-ui/core/Popper 0.00% 0.00% 28,896 28,896 10,394 10,394
@material-ui/core/Textarea 0.00% 0.00% 5,534 5,534 2,369 2,369
@material-ui/core/TrapFocus +1.47% 🔺 +1.59% 🔺 3,753 3,808 1,577 1,602
@material-ui/core/styles/createMuiTheme 0.00% 0.00% 16,156 16,156 5,816 5,816
@material-ui/core/useMediaQuery 0.00% 0.00% 3,098 3,098 1,310 1,310
@material-ui/lab 0.00% 0.00% 141,699 141,699 43,813 43,813
@material-ui/styles 0.00% 0.00% 51,886 51,886 15,380 15,380
@material-ui/system 0.00% 0.00% 15,576 15,576 4,445 4,445
Button 0.00% 0.00% 79,711 79,711 24,358 24,358
Modal +0.38% 🔺 +0.47% 🔺 14,548 14,603 5,102 5,126
Portal 0.00% 0.00% 3,471 3,471 1,568 1,568
Rating 0.00% 0.00% 70,267 70,267 22,068 22,068
Slider 0.00% 0.00% 75,096 75,096 23,311 23,311
colorManipulator 0.00% 0.00% 3,904 3,904 1,543 1,543
docs.landing 0.00% 0.00% 54,338 54,338 13,762 13,762
docs.main +0.01% 🔺 +0.01% 🔺 647,615 647,670 204,253 204,278
packages/material-ui/build/umd/material-ui.production.min.js +0.02% 🔺 +0.03% 🔺 299,670 299,725 86,116 86,141

Generated by 🚫 dangerJS against e4d192c

@eps1lon eps1lon marked this pull request as ready for review July 12, 2019 11:03
@@ -97,11 +97,11 @@ function TrapFocus(props) {
}
};

doc.addEventListener('focus', contain, true);
Copy link
Member Author

Choose a reason for hiding this comment

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

Huh the 3rd was capture, right? Do these events go through the capture phase?

@eps1lon eps1lon force-pushed the fix/FocusTrap/containment branch from 11a0c2b to 17eca2d Compare July 12, 2019 11:12
@eps1lon eps1lon force-pushed the fix/FocusTrap/containment branch from 17eca2d to 288672a Compare July 12, 2019 11:21
Copy link
Member

@oliviertassinari oliviertassinari left a comment

Choose a reason for hiding this comment

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

  • It solves the related issue on Chrome. It doesn't solve the problem on Firefox.
  • Removing the ignoreNextEnforceFocus logic breaks the focus ring. Shift + tab doesn't circle anymore.

@oliviertassinari
Copy link
Member

Hum, I'm out of idea for supporting Firefox (aside from setInternval…). I'm wondering if Firefox doesn't have an active bug on this matter: https://bugzilla.mozilla.org/show_bug.cgi?id=559561.

@eps1lon
Copy link
Member Author

eps1lon commented Jul 12, 2019

Yeah I'll give it a spin tomorrow but I'm not spending too much time on it considering there's active work on this issue in the react core.

@oliviertassinari
Copy link
Member

@eps1lon 💯 for giving up if it takes more than x hours. Opportunity cost, opportunity cost! :)

@oliviertassinari
Copy link
Member

oliviertassinari commented Jul 15, 2019

It seems that the fix only works on Chrome. I had no success in Edge and Firefox to find any event to leverage. I have tried to look at how the popular open-source project handles the problem: none seems to solve it, e.g. reactjs/react-modal#486.

We have a few options, by order of personal preference:

@oliviertassinari
Copy link
Member

oliviertassinari commented Jul 16, 2019

Oh, just realized that the test on browserstack reports the same observation than I did, I should have checked first 🤦‍♂️. I have spent 15 minutes trying the interval approach out. Pushing to see the report of the CI. I'm really happy about your new test @eps1lon 👍. Let me know what you think.

@oliviertassinari oliviertassinari force-pushed the fix/FocusTrap/containment branch from 0db1dac to 095af44 Compare July 16, 2019 08:59
@@ -100,7 +100,13 @@ function TrapFocus(props) {
doc.addEventListener('focus', contain, true);
doc.addEventListener('keydown', loopFocus, true);

const interval = setInterval(() => {
contain();
}, 500);
Copy link
Member Author

Choose a reason for hiding this comment

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

500ms is really long. Shouldn't this be 50?

Copy link
Member

@oliviertassinari oliviertassinari Jul 16, 2019

Choose a reason for hiding this comment

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

I don't know. The value should be large enough not to put much pressure on the main thread but be small enough not to disturb user interactions. The average reaction time for a human is around 700ms, e.g. for a car accident. Maybe the value could be defined with a heuristic: pick the reproduction example, start from 50ms, increase the value up until we can no longer make the escape keypress, consequent to an enter keypress, work reliably?

Copy link
Member Author

@eps1lon eps1lon Jul 16, 2019

Choose a reason for hiding this comment

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

100ms is the magic number when it comes to noticing time passed. If 400ms puts already pressure on the main thread your app has other issues.

contain is currently expensive? Maybe just check if the focus is on the body (which is the case if it is lost like in the reported issue) and only then run it. This should allow us to reduce it to 50ms which is ~3 frames. That seems alot less likely to be noticeable.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, sure, that makes sense to me. No matter what we pick, the cost of changing it is low. Can it have an impact of people perception if the see something that ticks every 50ms when debugging a perf issue on their app? Or, can it have an impact on the requestidlecallback API? I have no idea. The future will tell us 🤷‍♂️. +1 for trying 50ms.

@eps1lon
Copy link
Member Author

eps1lon commented Jul 17, 2019

For future reference: While working please prefer to branch of the work of others. If you need CI open a separate PR to this repository. If it's more about discussing implementation details a PR to the fork repository is more appropriate.

If I want to continue my work I now have to checkout previous versions of my work or hard reset your work. I'm going open a new PR with my previous work this time but I'd hope we make a collaborative effort in the future.

@eps1lon
Copy link
Member Author

eps1lon commented Jul 17, 2019

Confirmed bug in Firefox: The issue is that in Firefox no focus related events are fired when the focused area stops being a focused area. The whatwg spec defines how the browser should behave but does not explicitly mention any events (chrome and IE11 do fire blur events):

Focus fixup rule: When the designated focused area of the document is removed from that Document in some way (e.g. it stops being a focusable area, it is removed from the DOM, it becomes expressly inert, etc.), designate the Document's viewport to be the new focused area of the document.

-- https://html.spec.whatwg.org/multipage/interaction.html#focus-fixup-rule

Given that I would stick with the "containOnBlur" handler which handles spec compliant browsers immediately and add a polling based fallback with the note that it should be removed once firefox fixes that bug and we no longer support versions with that bug.

@oliviertassinari
Copy link
Member

Do you confirm that Edge and Safari have the same bug?

@oliviertassinari
Copy link
Member

While working please prefer to branch of the work of others. If you need CI open a separate PR to this repository. If it's more about discussing implementation details a PR to the fork repository is more appropriate.

I think that collaboration should happen, ideal, under a single centralized thread. This is what we do with comments. I wish we could do the same with code changes. Its work best when different paths can be explored, one at the time. However, there is one important limitation in practice with GitHub: asynchronicity. We can't push at the same time under the same branch while we can comment at the same time. I think that it should be encouraged when:

  1. Somebody focus has moved somewhere else or if I can help make us move faster without creating code conflict or friction
  2. The effort doesn't need an owner, e.g. the problem needs to be hammered out or it's a discussion and committing help moving the thread (silence = not important, we move forward)
  3. To avoid an inefficient review process that can be short-circuited

I'm aware you dislike when somebody pushes on your remote (you mentioned it at multiple occasion in the past), I will keep reducing my threshold for doing it with you (hopefully, never do it). Thanks for the feedback.

If I want to continue my work I now have to checkout previous versions of my work or hard reset your work. I'm going open a new PR with my previous work this time but I'd hope we make a collaborative effort in the future.

You can hard reset the path I have explored. It's fine, I don't mind at all. GitHub keeps track of the push forces now. It won't be lost. As for syncing the local environment with remote, when I have this problem, I personally use this approach. I don't know if there is a simpler one?

gpr -n eps1lon:fix/FocusTrap/containment
  1. I commit my changes
git push origin HEAD:fix/FocusTrap/containment

@eps1lon
Copy link
Member Author

eps1lon commented Jul 17, 2019

I know how to handle git and get my changes back. If you only care about your workload then keep on pushing over my implementations and I will put in the extra effort to work on my branches that you don't want to put in. Glad that we could finally talk open about it.

If you want to explore different paths you can look into branches in git and how to track different remotes with them. Git offers tracking of changes not only sequentially with commits but also in parallel with branches.

@eps1lon
Copy link
Member Author

eps1lon commented Jul 17, 2019

Do you confirm that Edge and Safari have the same bug?

Can't even get Safari to tab into the dialog so I'll leave that alone. Edge has the same issue as firefox but will at least be fixed once they are chromium based.

Guess we stick with the polling based approach. MutationObserver might do as well but I honestly don't want to put too much benchmarking effort into this so 50ms polling rate sounds fine.

@oliviertassinari
Copy link
Member

The interval still runs at the end of the suite, it sounds wrong. I believe one of the tests in our suite doesn't clean up correctly. 🔍 Looking into which test leak.

@oliviertassinari
Copy link
Member

Found it:

diff --git a/packages/material-ui/src/Popover/Popover.test.js b/packages/material-ui/src/Popover/Popover.test.js
index 92f112dd1..3593fdc34 100644
--- a/packages/material-ui/src/Popover/Popover.test.js
+++ b/packages/material-ui/src/Popover/Popover.test.js
@@ -609,6 +609,7 @@ describe('<Popover />', () => {
     let wrapper;

     before(() => {
+      clock = useFakeTimers();
       innerHeightContainer = window.innerHeight;
       const mockedAnchor = document.createElement('div');
       stub(mockedAnchor, 'getBoundingClientRect').callsFake(() => ({
@@ -629,8 +630,6 @@ describe('<Popover />', () => {
         </Popover>,
       );
       element = handleEntering.args[0][0];
-
-      clock = useFakeTimers();
     });

     after(() => {

@eps1lon The strategy proposed in #16585 (comment) sounds great to me 👍. How do you want to move forward?

@oliviertassinari
Copy link
Member

@eps1lon I will assume that you have assigned me to the pull request so I push it one step further. I'm on it :).

@oliviertassinari
Copy link
Member

oliviertassinari commented Jul 19, 2019

Ok, it should be good to go 🚢. I have restricted the changes to the addition of the interval to support Firefox, Edge and Safari. The other alternatives were breaking the focus ring in some way. Given that the long term plan is to get rid of this component, better not invest too much time on it.

@koshkarik
Copy link

@oliviertassinari could you please apply this fix also for 3rd mui version?

@eps1lon eps1lon deleted the fix/FocusTrap/containment branch July 22, 2019 15:16
@oliviertassinari
Copy link
Member

Sorry, we have no plan for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accessibility a11y bug 🐛 Something doesn't work component: FocusTrap The React component. component: modal This is the name of the generic UI component, not the React module!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Modal] focus-trap fails when a focused element in the modal unmounts
4 participants