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, Sheet: Fix infinite focus loop (#649) #1442

Merged
merged 2 commits into from Mar 2, 2022

Conversation

leoschmitz
Copy link
Contributor

@leoschmitz leoschmitz commented Mar 26, 2021

The TrapFocusBehavior component listens to all document focus events. If
a target of a FocusEvent does not match any children of the trapped
component, the event is blocked and a new one is dispatched for the
first child of the trapped component. This works well for a single trap.

The infinite loop occurs when there is more than one TrapFocusBehaviour
trying to trap the focus for its first child. Calling .focus() for it
triggers a new FocusEvent, which is then captured by all Trap components,
calling .focus() again and filling all call stack up.

The solution is to check whether the event has been triggered by a trap
component or not. If it has, then let the event run its course. Since
the first child is actually the event target, and not the trap
component, Element.closest is used to identify if the target has a trap
ancestor.

In short,

  • 'trap-focus' name added to identify TrapFocusBehavior components
  • Element.closest() used to identify trapped components
  • class component migrated to hook

Impact: both Modal and Sheet use TrapFocusBehavior.

Test Plan

@leoschmitz leoschmitz requested a review from a team as a code owner March 26, 2021 23:04
@netlify
Copy link

netlify bot commented Mar 26, 2021

✔️ Deploy Preview for gestalt ready!

🔨 Explore the source changes: f4be74b

🔍 Inspect the deploy log: https://app.netlify.com/sites/gestalt/deploys/621fd81ec6c49200073e428b

😎 Browse the preview: https://deploy-preview-1442--gestalt.netlify.app

@dangerismycat
Copy link
Contributor

Thanks for fixing this bug! We're reviewing this PR internally and we'll get back to you soon. 🙂

@dangerismycat
Copy link
Contributor

Hey @leoschmitz

Thanks again for the PR. It looks like the stack overflow bug has been fixed! However, it also seems that focus is not being trapped anymore, which you can repro by opening a modal and using the keyboard to tab through the interactive elements. Focus should be confined to the open Modal, but as you can see in this video, we're able to tab through the rest of the elements as well.
https://user-images.githubusercontent.com/12059539/112914281-70708c00-90b0-11eb-9c2e-911099389eb2.mp4

@leoschmitz
Copy link
Contributor Author

Yes, @dangerismycat, you're right.

I guess not only blocking the event propagation, but re-trapping it, is necessary after all. One solution I can think of right now is that if we are able to identify that the EventTarget is a TrapFocusBehavior component, we should not call .focus(), but let the EventTarget do its course. So multiple trap components competing would eventually stop at the last registered listener, which would be the topmost Modal. I'm not sure about this last part though, so I'll try it out.

Anyways, I'll try do deal with both Trap components competing for the event differently and get back to you.

@dangerismycat
Copy link
Contributor

Sounds good. Thanks again for your work on this!

The TrapFocusBehavior component listens to all document focus events. If
a target of a FocusEvent does not match any children of the trapped
component, the event is blocked and a new one is dispatched for the
first child of the trapped component. This works well for a single trap.

The infinite loop occurs when there is more than one TrapFocusBehaviour
trying to trap the focus for its first child. Calling .focus() for it
triggers a new FocusEvent, which is then captured by all Trap components,
calling .focus() again and filling all call stack up.

The solution is to check whether the event has been triggered by a trap
component or not. If it has, then let the event run its course. Since
the first child is actually the event target, and not the trap
component, Element.closest is used to identify if the target has a trap
ancestor.

In short,

* 'trap-focus' name added to identify TrapFocusBehavior components
* Element.closest() used to identify trapped components
* class component migrated to hook

Impact: both Modal and Sheet use TrapFocusBehavior.
@leoschmitz
Copy link
Contributor Author

Hey @dangerismycat,

It appears that identifying the event target as a child of the Trap component solves it. Problem is, since the focus goes to the first child of the trap component, I had to search the ancestors up to see if the event came from a Trap component. I did this with the help of a new 'trap-focus' attribute in the TrapFocusBehavior div.

I don't know if adding a name there is acceptable. What do you think?

@dangerismycat
Copy link
Contributor

That seems to do the trick! The name seems fine to me, though I'm interested in opinions from @christianvuerings or @jennyscript.

@dangerismycat
Copy link
Contributor

@leoschmitz Thanks again for this work, and our apologies for the lack of action on it. We're taking this issue off the back burner and addressing it again soon — I may end up re-opening and merging this PR, or perhaps adapting it if needed. 🙂

@dangerismycat
Copy link
Contributor

@leoschmitz I finally had time to look through this, and it looks great! Seems to work perfectly to me. I'm re-opening this PR and will merge it soon. Thank you again for this work, and many apologies for the extended delays! 🙏

@dangerismycat dangerismycat reopened this Mar 2, 2022
@dangerismycat dangerismycat changed the title Modal: Fix infinite focus loop (#649) Modal, Sheet: Fix infinite focus loop (#649) Mar 2, 2022
@dangerismycat dangerismycat added the minor release Minor release label Mar 2, 2022
@dangerismycat dangerismycat merged commit 2682852 into pinterest:master Mar 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
minor release Minor release
Projects
None yet
3 participants