1. boundless ancestor -- need better term.

Event Retargeting

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:

  1. Set the relative target of the node at which event is fired to the node itself.
  2. For each ancestor walking up starting with the node's parent, compute its relative target:

    1. If the node is a shadow boundary, skip it
    2. If the previous node was a shadow boundary, make the node its own relative target.
    3. Otherwise, use previous node's relative target.

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.

Related Target Events

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:

  1. If relatedTarget and target are enclosed by a shadow DOM subtree, the event dispatch must be stopped at the shadow boundary of this subtree.
  2. If 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:

  1. Find the lowest common ancestor of target and relatedTarget.
  2. If there is none, skip to step 7 in the algorithm.
  3. If the lowest common ancestor is 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.
  4. Looking at the ancestors of the lowest common ancestor, starting at its parent and walking up, find a shadow boundary. This is the lowest common boundary, the boundary of a shadow DOM subtree that encloses both target and relatedTarget.
  5. If the lowest common boundary is not found, skip to step 7 in the algorithm.
  6. Limit event dispatch to the descendants of the lowest common boundary.
  7. If the lowest common boundary is still undetermined at this point, set it to be the farthest ancestor of the relatedTarget.
  8. Starting from lowest common boundary and walking down ancestors of the relatedTarget, find a shadow boundary. This is the first divergent boundary.
  9. If first divergent boundary is not found, stop.
  10. Set relatedTarget to the parent of the first divergent boundary.

Focus Events

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.

Events That Are Always Stopped

Mutation events and selectstart event are always stopped at the nearest shadow boundary.

Example

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:

  1. We set the relative target of the thumb node to itself.
  2. Walking up, we find a shadow boundary and skip it.
  3. Next up is an input (#player::volume-slider), and we set its relative target to itself, since the previous ancestor was a shadow boundary.
  4. We then walk up to the containing div (#player::volume-slider-container), and use previous ancestor's target -- the input element.
  5. Walking up one more, we reach the controls panel (#player::controls), and again use input element as relative target per retargeting algorithm.
  6. Next, we reach another shadow boundary and skip it.
  7. Finally we walk up to the player element (#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).

  1. We find lowest common ancestor of timeline thumb and volume slider thumb: the controls panel (#player::controls).
  2. Walking up, we look for a shadow boundary, and find one just above. This is the lowest common boundary.
  3. Given that the lowest common boundary exists, we limit event dispatch to the descendants of this boundary.
  4. Starting from the lowest common boundary, we walk down the ancestors of the timeline thumb, looking for a shadow boundary. We find it just below the timeline (#player::timeline). This is the first divergent boundary.
  5. We adjust 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>