-
Notifications
You must be signed in to change notification settings - Fork 48k
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
Determine event target in Shadow DOM correctly #12163
Determine event target in Shadow DOM correctly #12163
Conversation
0406615
to
d02aa87
Compare
Cool! Also waiting for this fix... Thx @marionebl ! |
the callback still does not invoke in web component |
@marionebl Great fix! Solves all my problems with injecting a complete react client inside of the shadowRoot. Can we have some sort of time estimation would love to see this merged in! |
@MRDNZ I am not a react contributor, so I can't promise when / if this gets merged at all, sorry. |
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 change looks fine to me, i'm not super familar with the shadow-dom but this is minimal enough i think it's ok?
let React; | ||
let ReactDOM; | ||
|
||
describe('getEventTarget', () => { |
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'm not sure this test suite tests much? I suppose it doesn't hurt but it doesn't really cover this bug, or the svg case. I'm not sure if it makes sense to add.
@@ -17,6 +17,12 @@ import {TEXT_NODE} from '../shared/HTMLNodeType'; | |||
function getEventTarget(nativeEvent) { | |||
let target = nativeEvent.target || window; | |||
|
|||
// If composed / inside open shadow-dom use first item of composed path #9242 | |||
if (nativeEvent.composed) { | |||
const path = nativeEvent.composedPath(); |
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 add an additional safety check here that composedPath
exists. Who knows what the compatibility matrix will look like here...
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.
typeof nativeEvent.composedPath === "function"
should do the trick?
|
||
return ( | ||
<FixtureSet title="Shadow DOM" description=""> | ||
<TestCase title="Event listeners in shadow-dom" relatedIssues="4963"> |
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.
Does this test case fail without this PR? My understanding is the event would still fire, but the target
would be wrong, the TestCase doesn't assert anything about the target, only that the event was seen.
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 test case fails without the other changes in this PR
@jquense , @marionebl is there anything i can help you guys with to help speed up the process of getting this merged in? |
@jquense Adressed your comments, thanks for the review! |
Anything else I can do to help this move along? |
Sorry if this is a silly question... Would it be possible instead to expose Event.composed and Event.composedPath() on the Shadow DOM purposefully retargets events so as not to violate encapsulation. Instead, if |
To be clear The function changed in this PR is only used internally by React. It’s not a public API. But it determines what React users will see as a |
ah I see.
Does it also inform React about which handler to call? For example, if I render a React component inside of the shadow DOM, like so:
Normally the DOM click event would be retargeted to If that's the case, would it be possible for React to use the element at I realize this might be a weird idea, but my thinking is that React, the library, has a very specific need to reach inside of shadow boundaries because of how it does event delegation. But if I'm just using React to write a component, then I shouldn't need to know about the encapsulated internals of another component. And if, for whatever reason, I do need to get at those internals, I can still use |
In other words, React would use |
The event Synthetic event target will be the DOM node associated with the component that's used here, so in the case it will be whatever This looks good to me, @nhunzaker or @aweary care to add another sign off? |
Ok that confirms what I was curious about, thank you @jquense. I guess I still have concerns that by giving the developer the internal element as SyntheticEvent.target it's bypassing shadow DOM's encapsulation. Those elements are supposed to be private, implementation details which is why the event is retargeted to the shadow host. That's a key point of shadow DOM. If it's possible to set the target back to the shadow host after React has triggered handlers that would feel a bit better to me, though I realize it may be more work |
Maybe I can give some examples to explain my thinking. I realized you may want two things here...
In this scenario, it's totally fine for
In this scenario, Apologies if I'm totally misunderstanding 😬 I hope these examples explain my thinking a bit better. |
@jquense Would this not be a breaking change? Technically we're changing what |
@robdodson I think I get your point. I did not consider scenario 2 when implementing this.
@gaearon In scenario 2 (shadow-dom inside React tree) this would be a breaking change pointing Could we solve this by decoupling the passed I haven't dug deep enough into the |
In this case starting to execute them can also be a breaking change. |
I'm excited for this to be merged but it seems that |
My understanding tho is that this is how the Shadow DOM works, it makes the distinction between "closed" and "open", we are talking about "fixing" the case where the tree is open, but not doing what would natively happen.
@gaearon I think this falls in the gray area. I'm inclined to say it's not, since it's a fix to the existing behavior to bring it inline with how the native DOM events work. My (limited) understand of the shadow DOM here is this is definitely a fix for the case demonstrated in the Fixture, it's odd that the I guess i have a few outstanding questions @marionebl
Maybe my questions are a bit off, I suppose it probably doesn't make any sense for a component like this to accept |
<Box /> | ||
</Shadow> | ||
</TestCase> | ||
</FixtureSet> |
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.
we should probably also include a closed
version that ensures documents/asserts the behavior in that case as well
Open shadow DOM still retargets the event to the shadow host to preserve encapsulation. It's just a bit less strict than closed shadow DOM—where you can't access the internal path at all. Open shadow DOM example So for a closed shadow root, this approach won't work because
Even with open shadow roots, the goal is to avoid component consumers from knowing about/relying on the implementation details of a particular widget, which is why the retargeting happens. As @marionebl mentioned, if it's possible to decouple the synthetic event target that developers see, from the target used by React to call handlers, that could be a workable solution. |
Ahhhhh ok, thanks for the rundown @robdodson that is very helpful.
This is possible but i'm not sure there is one good place to do this...What happens is
ReactDOM starts with a native event and derives the appropriate component instances to use in order to call the handlers. It also uses the target here to pass through to event plugins which (generally) pass it through to the Synthetic event. We could decouple that logic in at the line i linked above, the trick is to get the instance related to the composedPath so listeners are called, but pass through the original target to plugins so that is used as the main target. I'm not sure tho if that will just work, my hunch is it's gonna get messy and require some check of this logic in a few places which wouldn't be ideal. The other concern here is that |
Closing as this naive fix won't do it. |
The case in this example seems to be working in React 17 RC: We'd appreciate filing any issues if something is very wrong before we cut React 17 stable. |
Motivation
Fixes an issue with events not being delegated correctly when originating from an element inside a Shadow DOM root as described in #9242.
Included changes
Events
that are.composed
as per MDNgetEventTarget
fixtures/dom
, asattachShadow
is not supported injsdom
yet