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

[TrapFocus] Fix portal support #21610

Merged
merged 24 commits into from
Jul 2, 2020
Merged

Conversation

mnajdova
Copy link
Member

@mnajdova mnajdova commented Jun 28, 2020

This PR enables portals support inside TrapFocus.

What is the problem?

When there are portals rendered inside the TrapFocus, they are rendered outside of the that elements dom tree. This creates the following problem: by nature the TrapFocus traps the focus whenever the focus is not inside it's dom tree. That means if the focus goes inside the portal, it is "stealed" by the TrapFocus and it's put on the root element (an example of this can be find in #15694)

Solution

The solution to this problem is, we need to somehow figure out if the focusing event is happening inside the React tree, not just the dom tree. I have experiment with different approaches of how this can be solved, but settled on the event propagation, as the most stable one (there was a different one that uses React's internal, you can find the link in the end of the PR description).

What we need to do is, add an onFocus event on the children of the TrapFocus and set some internal ref to the last target that was focused, so that we can check that it is indeed the element focused when the contain method is invoked - it is a dom listener on the document for the focus event. This is necessary because we may run up to some inconsistency, when some element outside of the TrapFocus' React tree is focused.

Fixes #15694

For people interested in an alternative that uses React's internals: d8c75e5.

@mui-pr-bot
Copy link

mui-pr-bot commented Jun 28, 2020

Details of bundle changes

Generated by 🚫 dangerJS against 3eead64

@oliviertassinari oliviertassinari added the component: FocusTrap The React component. label Jun 28, 2020
@oliviertassinari oliviertassinari changed the title Bug/portal dialog trapfocus [TrapFocus] Fix portal support Jun 28, 2020
@mnajdova mnajdova marked this pull request as ready for review June 28, 2020 11:49
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.

The logic looks solid, I couldn't find anything better.

mnajdova and others added 5 commits June 28, 2020 20:22
Co-authored-by: Olivier Tassinari <olivier.tassinari@gmail.com>
….test.js

Co-authored-by: Olivier Tassinari <olivier.tassinari@gmail.com>
Co-authored-by: Olivier Tassinari <olivier.tassinari@gmail.com>
@oliviertassinari oliviertassinari requested a review from eps1lon June 28, 2020 21:51
@oliviertassinari
Copy link
Member

I think that it would be great to check that the changes solve this downstream issue mui/material-ui-pickers#1852.
We can do such by using the picker reproduction and replacing the used version by the one Codesandbox-CI publishes :). @dmtrKovalenko Could you check that out? :)

Copy link
Member

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

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

Not easy to hold different trees in your head where focus events propagate differently. I think it's important for knowledge sharing especially to go into more details for this change.

@@ -73,7 +75,7 @@ function Unstable_TrapFocus(props) {
rootRef.current.focus();
}

const contain = () => {
const contain = (event) => {
Copy link
Member

Choose a reason for hiding this comment

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

Is this a native DOM event or a synthetic React event? As long as we're untyped I'd suggest using *event for react events and *nativeEvent for native DOM events to make it clear what we're dealing with.

Copy link
Member

Choose a reason for hiding this comment

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

From past experience, such distinction is also useful for using event.key vs nativeEvent.keyCode in regard to how React polyfill IE 11.

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed to nativeEvent 👍

@@ -85,6 +87,18 @@ function Unstable_TrapFocus(props) {
}

if (rootRef.current && !rootRef.current.contains(doc.activeElement)) {
// if the focus event is different than the last syntheticEvent from the children, reset
Copy link
Member

Choose a reason for hiding this comment

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

Having a hard time understanding this branch. Could you explain with JSX and an event order when we reach this branch? Ideally point to an existing test or add a new one for this branch.

Copy link
Member Author

Choose a reason for hiding this comment

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

The condition is trying to capture whether between the time we saved the last focused event target and this invocation of the function, it has changed. We have two scenarios we need to check:

  1. the focus event was invoked with different target (that is the first part of the condition)
  2. the element that was focused was removed and the interval for the function run out (that is the second part of the condition)

The 2 condition is captured already in the test where an element is removed and the focus moves to body, I will add a test for the other use-case (it may be hard to make sure which exact of these two condition will be caught because the function is invoked in an interval, but let me try to deal with it)

Does this clarifies the use case? I will also try to update the comment to better reflect this.

Copy link
Member Author

Choose a reason for hiding this comment

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

On this note, I would like to test whether just the second part of the condition will be enough, as the interval would anyway invoke this function, but it may have a bit of delay, so it may not be the best idea..

Copy link
Member Author

Choose a reason for hiding this comment

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

The newly added trest together with the previous covers all use casees I believe - focusing an dom outisde the react tree, as well focusing nodes inside portal

@mnajdova
Copy link
Member Author

mnajdova commented Jul 1, 2020

Not easy to hold different trees in your head where focus events propagate differently. I think it's important for knowledge sharing especially to go into more details for this change.

@eps1lon I've updated the PR description, hopefully this clarifies things. Let me know what do you think

@mnajdova mnajdova requested a review from eps1lon July 1, 2020 20:00
Copy link
Member

@joshwooding joshwooding left a comment

Choose a reason for hiding this comment

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

Really clean!

@oliviertassinari
Copy link
Member

I think that it would be great to check that the changes solve this downstream issue mui/material-ui-pickers#1852.

Tested in mui/material-ui-pickers#1852 (comment), we are good 👌

@mnajdova mnajdova merged commit b50793d into mui:next Jul 2, 2020
This was referenced Jul 4, 2020
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.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[TrapFocus] Steals focus from nested portal
5 participants