When an event is fired in a shadow DOM subtree, it either escapes out of the shadow tree during the capture, target, bubble, or default phases or it is stopped at the shadow boundary.
To help with algorithm description, let's define the direction walking the ancestors of a node as up when traversing from child to parent, and down when traversing from parent to child. In the context of events-related algorithms, all ancestor traversal crosses shadow boundaries.
In the cases where event escapes the shadow tree, the event is retargeted to ensure that the original target-related information on the event object does not reveal information about the shadow DOM subtree beyond the boundary. Event retargeting is a process of computing a relative target for each ancestor of the node at which the event is dispatched.
The following retargeting algorithm must be used to determine relative targets:
The retargeting process must occur prior to dispatch of an event.
At the time of event dispatch, for capturing, target, and bubbling phases, Event
object's target
and currentTarget
must be set to the relative target that was computed for the node along the capturing and bubbling flow.
The event must not be retargeted during the default phase of dispatch.
Upon completion of the event dispatch, the Event
object's target
and currentTarget
are set to the highest ancestor's relative target. Since it is possible for a script to hold on to the Event
object past the scope of event dispatch, this step is necessary to avoid revealing the nodes in shadow DOM subtrees.
Some events have a relatedTarget
property, which holds a node that's not the event's target, but is related to the event. For instance, a mouseover
event's relatedTarget
may hold the node from which the mouse has moved to event's target
. In the case where relatedTarget
is in a shadow DOM subtree, we don't want to leak its actual value outside of this subtree. In cases where both relatedTarget
and target
are part of the same shadow DOM subtree, we don't want the appearance of spurious mouseover
and mouseout
events firing from the same node.
Thus, if an event has a relatedTarget
, its value and extent of event dispatch must to be adjusted. In general:
relatedTarget
and target
are enclosed by a shadow DOM subtree, the event dispatch must be stopped at the shadow boundary of this subtree.relatedTarget
is separated from target
by one or more shadow boundaries, it must be retargeted to the boundary that's closest to target
.The following algorithm must be used:
target
and relatedTarget
.relatedTarget
, and it's a shadow DOM subtree host, make this tree's boundary the lowest common boundary, and go to step 6 in the algorithm.target
and relatedTarget
.
relatedTarget
.relatedTarget
, find a shadow boundary. This is the first divergent boundary.relatedTarget
to the parent of the first divergent boundary.
The focus
, DOMFocusIn
, blur
, and DOMFocusOut
events must be treated in the same way as events with a relatedTarget
, where the corresponding node that is losing focus as a result of target
gaining focus or the node that is gaining focus, and thus causing the blurring of target
acts as the related target.
Mutation events and selectstart
event are always stopped at the nearest shadow boundary.
Suppose we have a user interface for a media controller, built with several components, represented by this shadow-aware DOM tree:
<div id="player">
<shadow-boundary>
<div pseudo="controls">
<button pseudo="play-button">
<input type="range" pseudo="timeline">
<shadow-boundary>
<div pseudo="slider-thumb"></div>
</shadow-boundary>
</input>
<div pseudo="volume-slider-container">
<input type="range" pseudo="volume-slider">
<shadow-boundary>
<div pseudo="slider-thumb"></div>
</shadow-boundary>
</input>
</div>
</div>
</shadow-boundary>
</div>
Let's have a user position their pointing device over the volume slider's thumb (#player::volume-slider::slider-thumb
), thus triggering a mouseover
event on that node. For this event, let's pretend it has no associated relatedTarget
.
Just before the event is dispatched, we perform retargeting:
#player::volume-slider
), and we set its relative target to itself, since the previous ancestor was a shadow boundary.#player::volume-slider-container
), and use previous ancestor's target -- the input element.#player::controls
), and again use input element as relative target per retargeting algorithm.#player
) and set its relative target to itself.<div id="player"> 7
<shadow-boundary> 6
<div pseudo="controls"> 5
<button pseudo="play-button">
<input type="range" pseudo="timeline">
<shadow-boundary>
<div pseudo="slider-thumb"></div>
</shadow-boundary>
</input>
<div pseudo="volume-slider-container"> 4
<input type="range" pseudo="volume-slider"> 3
<shadow-boundary> 2
<div pseudo="slider-thumb"></div> 1
</shadow-boundary>
</input>
</div>
</div>
</shadow-boundary>
</div>
At the end of this process, we should have the following set of ancestors and relative targets:
Ancestor | Relative Target |
---|---|
... |
div#player |
div#player |
div#player |
div#player::controls |
div#player::controls::volume-slider |
div#player::volume-slider-container |
div#player::controls::volume-slider |
div#player::controls::volume-slider |
div#player::controls::volume-slider |
div#player::controls::volume-slider::slider-thumb |
div#player::controls::volume-slider::slider-thumb |
After we dispatch the mouseover
event using these newly computed relative targets, the user decides to move their pointing device over the thumb of the timeline (#player::timeline::slider-thumb
). This triggers both a mouseout
event for the volume slider thumb and the mouseover
event for the timeline thumb.
Let's study how the relatedTarget
value of the volume thumb's mouseout
event is affected and whether event escapes out of the player. For this event, the relatedTarget
is the timeline thumb (#player::timeline::slider-thumb
).
#player::controls
).#player::timeline
). This is the first divergent boundary.relatedTarget
to be the parent of the first divergent boundary, the timeline (#player::timeline
). We are now ready to dispatch this event.<div id="player">
<shadow-boundary> — lowest common boundary
<div pseudo="controls">
<button pseudo="play-button">
<input type="range" pseudo="timeline">
<shadow-boundary> — first divergent boundary
<div pseudo="slider-thumb"></div>
</shadow-boundary>
</input>
<div pseudo="volume-slider-container">
<input type="range" pseudo="volume-slider">
<shadow-boundary>
<div pseudo="slider-thumb"></div>
</shadow-boundary>
</input>
</div>
</div>
</shadow-boundary>
</div>