event.target shouldn't be retargeted as the event bubbles into a slot
https://bugs.webkit.org/show_bug.cgi?id=157369

Reviewed by Antti Koivisto.

Source/WebCore:

When an event bubbles up from an assigned node to its assigned slot, we shouldn't be adjusting
event.target to point to the slot. Since a shadow tree should have access to nodes outside
the shadow tree, event.target is accessible inside the shadow tree of such a slot.

New behavior matches the behavior of Google Chrome Canary as well as the shadow DOM specification:
http://w3c.github.io/webcomponents/spec/shadow/#dfn-retargeting-algorithm

Test: fast/shadow-dom/event-inside-slotted-node.html

* dom/Event.cpp:
(WebCore::Event::deepPath):
* dom/EventContext.h:
(WebCore::EventContext::isUnreachableNode): Use Node::isUnclosedNode instead of isReachable.
(WebCore::EventContext::isReachable): Deleted.
* dom/EventPath.cpp:
(WebCore::EventPath::EventPath): Don't set the target to the slot when entering a slot. Also moved
the code to adjust the target as we exit a shadow tree to the end of the outer loop for clarity.
(WebCore::isUnclosedNodeOf): Deleted. Renamed to Node::isUnclosedNode.
(WebCore::EventPath::setRelatedTarget):
(WebCore::EventPath::computePathUnclosedToTarget): Renamed from computePathDisclosedToTarget.
(WebCore::moveOutOfAllShadowRoots): Extracted from RelatedNodeRetargeter::RelatedNodeRetargeter.
(WebCore::RelatedNodeRetargeter::RelatedNodeRetargeter): Fixed a bug that we were exiting early
without setting m_hasDifferentTreeRoot true when target and relatedNode are disconnected from
a document.
(WebCore::RelatedNodeRetargeter::currentNode):
(WebCore::RelatedNodeRetargeter::checkConsistency): Updated to match the spec with one exception.
We don't use null as the adjusted related target when the (original) related target and the target
are in two distinct disconnected trees since such a behavior is not Web compatible. This spec bug is
tracked by https://github.com/w3c/webcomponents/issues/494
* dom/EventPath.h:
(WebCore::EventPath::eventTargetRespectingTargetRules): Returns Node* instead of EventTarget* since
we need a Node in RelatedNodeRetargeter::checkConsistency.
* dom/Node.cpp:
(WebCore::Node::isUnclosedNode): Moved from RelatedNodeRetargeter.cpp
* dom/Node.h:

LayoutTests:

Updated test cases to expect the target to be not adjusted to a slot element when the event path
enters one as this didn't match the spec or the behavior of Google Chrome Canary. Both WebKit and
Chrome passes the test with this change.

* fast/shadow-dom/event-inside-slotted-node.html:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@200464 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 6ae685f..9d0fc43 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,16 @@
+2016-05-05  Ryosuke Niwa  <rniwa@webkit.org>
+
+        event.target shouldn't be retargeted as the event bubbles into a slot
+        https://bugs.webkit.org/show_bug.cgi?id=157369
+
+        Reviewed by Antti Koivisto.
+
+        Updated test cases to expect the target to be not adjusted to a slot element when the event path
+        enters one as this didn't match the spec or the behavior of Google Chrome Canary. Both WebKit and
+        Chrome passes the test with this change.
+
+        * fast/shadow-dom/event-inside-slotted-node.html:
+
 2016-05-04  Alex Christensen  <achristensen@webkit.org>
 
         Blocked redirected main resource requests need descriptive errors
diff --git a/LayoutTests/fast/shadow-dom/event-inside-slotted-node.html b/LayoutTests/fast/shadow-dom/event-inside-slotted-node.html
index 0c60eb7..a4a2a52 100644
--- a/LayoutTests/fast/shadow-dom/event-inside-slotted-node.html
+++ b/LayoutTests/fast/shadow-dom/event-inside-slotted-node.html
@@ -80,9 +80,9 @@
                 assert_equals(log.length, 6, 'EventPath must contain [target, target parent, slot, slot parent, shadow root, shadow host]');
                 assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target');
                 assert_array_equals(log[1], [shadow.targetParent, shadow.target], 'EventPath[1] must be the parent of the target');
-                assert_array_equals(log[2], [shadow.slot, shadow.slot], 'EventPath[2] must be the slot');
-                assert_array_equals(log[3], [shadow.slotParent, shadow.slot], 'EventPath[3] must be the parent of the slot');
-                assert_array_equals(log[4], [shadow.root, shadow.slot], 'EventPath[4] must be the shadow root');
+                assert_array_equals(log[2], [shadow.slot, shadow.target], 'EventPath[2] must be the slot');
+                assert_array_equals(log[3], [shadow.slotParent, shadow.target], 'EventPath[3] must be the parent of the slot');
+                assert_array_equals(log[4], [shadow.root, shadow.target], 'EventPath[4] must be the shadow root');
                 assert_array_equals(log[5], [shadow.host, shadow.target], 'EventPath[5] must be the shadow host');
 
             }, 'Firing an event inside a grand child of a detached ' + mode + ' mode shadow host');
@@ -101,9 +101,9 @@
                 assert_equals(log.length, 9, 'EventPath must contain [target, target parent, slot, slot parent, shadow root, shadow host, body, html, document]');
                 assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target');
                 assert_array_equals(log[1], [shadow.targetParent, shadow.target], 'EventPath[1] must be the parent of the target');
-                assert_array_equals(log[2], [shadow.slot, shadow.slot], 'EventPath[2] must be the slot');
-                assert_array_equals(log[3], [shadow.slotParent, shadow.slot], 'EventPath[3] must be the parent of the slot');
-                assert_array_equals(log[4], [shadow.root, shadow.slot], 'EventPath[4] must be the shadow root');
+                assert_array_equals(log[2], [shadow.slot, shadow.target], 'EventPath[2] must be the slot');
+                assert_array_equals(log[3], [shadow.slotParent, shadow.target], 'EventPath[3] must be the parent of the slot');
+                assert_array_equals(log[4], [shadow.root, shadow.target], 'EventPath[4] must be the shadow root');
                 assert_array_equals(log[5], [shadow.host, shadow.target], 'EventPath[5] must be the shadow host');
                 assert_array_equals(log[6], [document.body, shadow.target], 'EventPath[6] must be the body element');
                 assert_array_equals(log[7], [document.documentElement, shadow.target], 'EventPath[7] must be the html element');
@@ -179,19 +179,19 @@
                 assert_equals(log.length, 15, 'EventPath must contain 15 targets');
 
                 assert_array_equals(log[0], [shadow.target, shadow.target], 'EventPath[0] must be the target');
-                assert_array_equals(log[1], [shadow.lowerSlot, shadow.lowerSlot], 'EventPath[1] must be the slot inside the lower shadow tree');
-                assert_array_equals(log[2], [shadow.lowerSlot.parentNode, shadow.lowerSlot], 'EventPath[2] must be the parent of the slot inside the lower shadow tree');
-                assert_array_equals(log[3], [shadow.innerSlot, shadow.innerSlot], 'EventPath[3] must be the slot inside the shadow tree inside the lower shadow tree');
-                assert_array_equals(log[4], [shadow.innerSlot.parentNode, shadow.innerSlot], 'EventPath[4] must be the child of the inner shadow root');
-                assert_array_equals(log[5], [shadow.innerShadow, shadow.innerSlot], 'EventPath[5] must be the inner shadow root');
-                assert_array_equals(log[6], [shadow.innerShadow.host, shadow.lowerSlot], 'EventPath[6] must be the host of the inner shadow tree');
-                assert_array_equals(log[7], [shadow.lowerShadow.firstChild, shadow.lowerSlot], 'EventPath[7] must be the parent of the inner shadow host');
-                assert_array_equals(log[8], [shadow.lowerShadow, shadow.lowerSlot], 'EventPath[8] must be the lower shadow root');
+                assert_array_equals(log[1], [shadow.lowerSlot, shadow.target], 'EventPath[1] must be the slot inside the lower shadow tree');
+                assert_array_equals(log[2], [shadow.lowerSlot.parentNode, shadow.target], 'EventPath[2] must be the parent of the slot inside the lower shadow tree');
+                assert_array_equals(log[3], [shadow.innerSlot, shadow.target], 'EventPath[3] must be the slot inside the shadow tree inside the lower shadow tree');
+                assert_array_equals(log[4], [shadow.innerSlot.parentNode, shadow.target], 'EventPath[4] must be the child of the inner shadow root');
+                assert_array_equals(log[5], [shadow.innerShadow, shadow.target], 'EventPath[5] must be the inner shadow root');
+                assert_array_equals(log[6], [shadow.innerShadow.host, shadow.target], 'EventPath[6] must be the host of the inner shadow tree');
+                assert_array_equals(log[7], [shadow.lowerShadow.firstChild, shadow.target], 'EventPath[7] must be the parent of the inner shadow host');
+                assert_array_equals(log[8], [shadow.lowerShadow, shadow.target], 'EventPath[8] must be the lower shadow root');
                 assert_array_equals(log[9], [shadow.lowerShadow.host, shadow.target], 'EventPath[9] must be the lower shadow host');
                 assert_array_equals(log[10], [shadow.host.firstChild, shadow.target], 'EventPath[10] must be the parent of the grand parent of the target');
-                assert_array_equals(log[11], [shadow.upperSlot, shadow.upperSlot], 'EventPath[11] must be the slot inside the upper shadow tree');
-                assert_array_equals(log[12], [shadow.upperSlot.parentNode, shadow.upperSlot], 'EventPath[12] must be the parent of the slot inside the upper shadow tree');
-                assert_array_equals(log[13], [shadow.upperShadow, shadow.upperSlot], 'EventPath[13] must be the upper shadow root');
+                assert_array_equals(log[11], [shadow.upperSlot, shadow.target], 'EventPath[11] must be the slot inside the upper shadow tree');
+                assert_array_equals(log[12], [shadow.upperSlot.parentNode, shadow.target], 'EventPath[12] must be the parent of the slot inside the upper shadow tree');
+                assert_array_equals(log[13], [shadow.upperShadow, shadow.target], 'EventPath[13] must be the upper shadow root');
                 assert_array_equals(log[14], [shadow.host, shadow.target], 'EventPath[14] must be the host');
 
             }, 'Firing an event on a node with two ancestors with a detached ' + outerUpperMode + ' and ' + outerLowerMode
@@ -215,7 +215,7 @@
             + a               + em (4)
                                 + inner-host (3) -- (innerShadow; 2)
                                   + span            + i (1)
-                                    + slot            + slot (innerSlot; 0)
+                                    + slot            + slot (innerSlot, target; 0)
         */
 
         function testEventInsideNestedShadowsUnderAnotherShadow(outerUpperMode, outerLowerMode, innerMode) {
@@ -236,9 +236,9 @@
                 assert_array_equals(log[5], [shadow.lowerShadow, shadow.innerShadow.host], 'EventPath[5] must be the lower (but outer) shadow root');
                 assert_array_equals(log[6], [shadow.lowerShadow.host, shadow.lowerShadow.host], 'EventPath[6] must be the lower (but outer) shadow root');
                 assert_array_equals(log[7], [shadow.host.firstChild, shadow.lowerShadow.host], 'EventPath[7] must be the slot inside the upper shadow tree');
-                assert_array_equals(log[8], [shadow.upperSlot, shadow.upperSlot], 'EventPath[8] must be the slot inside the upper shadow tree');
-                assert_array_equals(log[9], [shadow.upperSlot.parentNode, shadow.upperSlot], 'EventPath[9] must be the parent of the slot inside the upper shadow tree');
-                assert_array_equals(log[10], [shadow.upperShadow, shadow.upperSlot], 'EventPath[10] must be the upper shadow root');
+                assert_array_equals(log[8], [shadow.upperSlot, shadow.lowerShadow.host], 'EventPath[8] must be the slot inside the upper shadow tree');
+                assert_array_equals(log[9], [shadow.upperSlot.parentNode, shadow.lowerShadow.host], 'EventPath[9] must be the parent of the slot inside the upper shadow tree');
+                assert_array_equals(log[10], [shadow.upperShadow, shadow.lowerShadow.host], 'EventPath[10] must be the upper shadow root');
                 assert_array_equals(log[11], [shadow.upperShadow.host, shadow.lowerShadow.host], 'EventPath[11] must be the host');
 
             }, 'Firing an event on a node with two ancestors with a detached ' + outerUpperMode + ' and ' + outerLowerMode
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 5f50867..6c4d5f7 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,46 @@
+2016-05-05  Ryosuke Niwa  <rniwa@webkit.org>
+
+        event.target shouldn't be retargeted as the event bubbles into a slot
+        https://bugs.webkit.org/show_bug.cgi?id=157369
+
+        Reviewed by Antti Koivisto.
+
+        When an event bubbles up from an assigned node to its assigned slot, we shouldn't be adjusting
+        event.target to point to the slot. Since a shadow tree should have access to nodes outside
+        the shadow tree, event.target is accessible inside the shadow tree of such a slot.
+
+        New behavior matches the behavior of Google Chrome Canary as well as the shadow DOM specification:
+        http://w3c.github.io/webcomponents/spec/shadow/#dfn-retargeting-algorithm
+
+        Test: fast/shadow-dom/event-inside-slotted-node.html
+
+        * dom/Event.cpp:
+        (WebCore::Event::deepPath):
+        * dom/EventContext.h:
+        (WebCore::EventContext::isUnreachableNode): Use Node::isUnclosedNode instead of isReachable.
+        (WebCore::EventContext::isReachable): Deleted.
+        * dom/EventPath.cpp:
+        (WebCore::EventPath::EventPath): Don't set the target to the slot when entering a slot. Also moved
+        the code to adjust the target as we exit a shadow tree to the end of the outer loop for clarity.
+        (WebCore::isUnclosedNodeOf): Deleted. Renamed to Node::isUnclosedNode.
+        (WebCore::EventPath::setRelatedTarget):
+        (WebCore::EventPath::computePathUnclosedToTarget): Renamed from computePathDisclosedToTarget.
+        (WebCore::moveOutOfAllShadowRoots): Extracted from RelatedNodeRetargeter::RelatedNodeRetargeter.
+        (WebCore::RelatedNodeRetargeter::RelatedNodeRetargeter): Fixed a bug that we were exiting early
+        without setting m_hasDifferentTreeRoot true when target and relatedNode are disconnected from
+        a document.
+        (WebCore::RelatedNodeRetargeter::currentNode):
+        (WebCore::RelatedNodeRetargeter::checkConsistency): Updated to match the spec with one exception.
+        We don't use null as the adjusted related target when the (original) related target and the target
+        are in two distinct disconnected trees since such a behavior is not Web compatible. This spec bug is
+        tracked by https://github.com/w3c/webcomponents/issues/494
+        * dom/EventPath.h:
+        (WebCore::EventPath::eventTargetRespectingTargetRules): Returns Node* instead of EventTarget* since
+        we need a Node in RelatedNodeRetargeter::checkConsistency.
+        * dom/Node.cpp:
+        (WebCore::Node::isUnclosedNode): Moved from RelatedNodeRetargeter.cpp
+        * dom/Node.h:
+
 2016-05-02  Sam Weinig  <sam@webkit.org>
 
         On platforms that support it, use a SecTrustRef as the basis of CertificateInfo instead of a chain of SecCertificateRefs.
diff --git a/Source/WebCore/dom/Event.cpp b/Source/WebCore/dom/Event.cpp
index 949f4ab..7a07c82 100644
--- a/Source/WebCore/dom/Event.cpp
+++ b/Source/WebCore/dom/Event.cpp
@@ -191,7 +191,7 @@
 {
     if (!m_eventPath)
         return Vector<EventTarget*>();
-    return m_eventPath->computePathDisclosedToTarget(*m_target);
+    return m_eventPath->computePathUnclosedToTarget(*m_currentTarget);
 }
 
 void Event::receivedTarget()
diff --git a/Source/WebCore/dom/EventContext.h b/Source/WebCore/dom/EventContext.h
index f4ea8df..4ce8eba 100644
--- a/Source/WebCore/dom/EventContext.h
+++ b/Source/WebCore/dom/EventContext.h
@@ -57,7 +57,6 @@
 protected:
 #if !ASSERT_DISABLED
     bool isUnreachableNode(EventTarget*);
-    bool isReachable(Node*) const;
 #endif
     RefPtr<Node> m_node;
     RefPtr<EventTarget> m_currentTarget;
@@ -134,18 +133,7 @@
 inline bool EventContext::isUnreachableNode(EventTarget* target)
 {
     // FIXME: Checks also for SVG elements.
-    return target && target->toNode() && !target->toNode()->isSVGElement() && !isReachable(target->toNode());
-}
-
-inline bool EventContext::isReachable(Node* target) const
-{
-    ASSERT(target);
-    TreeScope& targetScope = target->treeScope();
-    for (TreeScope* scope = &m_node->treeScope(); scope; scope = scope->parentTreeScope()) {
-        if (scope == &targetScope)
-            return true;
-    }
-    return false;
+    return target && target->toNode() && !target->toNode()->isSVGElement() && !m_node->isUnclosedNode(*target->toNode());
 }
 #endif
 
diff --git a/Source/WebCore/dom/EventPath.cpp b/Source/WebCore/dom/EventPath.cpp
index 51b9f0e..bb9c363 100644
--- a/Source/WebCore/dom/EventPath.cpp
+++ b/Source/WebCore/dom/EventPath.cpp
@@ -60,9 +60,9 @@
 
 class RelatedNodeRetargeter {
 public:
-    RelatedNodeRetargeter(Node& relatedNode, TreeScope& targetTreeScope);
+    RelatedNodeRetargeter(Node& relatedNode, Node& target);
 
-    Node* currentNode(TreeScope& currentTreeScope);
+    Node* currentNode(Node& currentTreeScope);
     void moveToNewTreeScope(TreeScope* previousTreeScope, TreeScope& newTreeScope);
 
 private:
@@ -71,9 +71,9 @@
     void collectTreeScopes();
 
 #if ASSERT_DISABLED
-    void checkConsistency(TreeScope&) { }
+    void checkConsistency(Node&) { }
 #else
-    void checkConsistency(TreeScope& currentTreeScope);
+    void checkConsistency(Node& currentTarget);
 #endif
 
     Node& m_relatedNode;
@@ -86,22 +86,14 @@
 EventPath::EventPath(Node& originalTarget, Event& event)
     : m_event(event)
 {
-#if ENABLE(SHADOW_DOM) || ENABLE(DETAILS_ELEMENT)
-    Vector<EventTarget*, 16> targetStack;
-#endif
-
     bool isMouseOrFocusEvent = event.isMouseEvent() || event.isFocusEvent();
 #if ENABLE(TOUCH_EVENTS)
     bool isTouchEvent = event.isTouchEvent();
 #endif
-    EventTarget* target = nullptr;
-
     Node* node = nodeOrHostIfPseudoElement(&originalTarget);
+    Node* target = eventTargetRespectingTargetRules(*node);
     while (node) {
-        if (!target)
-            target = eventTargetRespectingTargetRules(*node);
-        ContainerNode* parent;
-        for (; node; node = parent) {
+        while (node) {
             EventTarget* currentTarget = eventTargetRespectingTargetRules(*node);
 
             if (isMouseOrFocusEvent)
@@ -116,8 +108,7 @@
             if (is<ShadowRoot>(*node))
                 break;
 
-            parent = node->parentNode();
-
+            ContainerNode* parent = node->parentNode();
             if (!parent)
                 return;
 
@@ -125,30 +116,21 @@
             if (ShadowRoot* shadowRootOfParent = parent->shadowRoot()) {
                 if (auto* assignedSlot = shadowRootOfParent->findAssignedSlot(*node)) {
                     // node is assigned to a slot. Continue dispatching the event at this slot.
-                    targetStack.append(target);
                     parent = assignedSlot;
-                    target = assignedSlot;
                 }
             }
 #endif
             node = parent;
         }
 
+        bool exitingShadowTreeOfTarget = &target->treeScope() == &node->treeScope();
         ShadowRoot& shadowRoot = downcast<ShadowRoot>(*node);
-        // At a shadow root. Continue dispatching the event at the shadow host.
-#if ENABLE(SHADOW_DOM) || ENABLE(DETAILS_ELEMENT)
-        if (!targetStack.isEmpty()) {
-            // Move target back to a descendant of the shadow host if the event did not originate in this shadow tree or its inner shadow trees.
-            target = targetStack.last();
-            targetStack.removeLast();
-            ASSERT(shadowRoot.host()->contains(target->toNode()));
-        } else
-#endif
-            target = nullptr;
-
         if (!shouldEventCrossShadowBoundary(event, shadowRoot, originalTarget))
             return;
         node = shadowRoot.host();
+        if (exitingShadowTreeOfTarget)
+            target = eventTargetRespectingTargetRules(*node);
+
     }
 }
 
@@ -158,7 +140,7 @@
     if (!relatedNode || m_path.isEmpty())
         return;
 
-    RelatedNodeRetargeter retargeter(*relatedNode, downcast<MouseOrFocusEventContext>(*m_path[0]).node()->treeScope());
+    RelatedNodeRetargeter retargeter(*relatedNode, *m_path[0]->node());
 
     bool originIsRelatedTarget = &origin == relatedNode;
     bool relatedTargetScoped = m_event.relatedTargetScoped();
@@ -168,11 +150,12 @@
     for (unsigned contextIndex = 0; contextIndex < originalEventPathSize; contextIndex++) {
         auto& context = downcast<MouseOrFocusEventContext>(*m_path[contextIndex]);
 
-        TreeScope& currentTreeScope = context.node()->treeScope();
+        Node& currentTarget = *context.node();
+        TreeScope& currentTreeScope = currentTarget.treeScope();
         if (UNLIKELY(previousTreeScope && &currentTreeScope != previousTreeScope))
             retargeter.moveToNewTreeScope(previousTreeScope, currentTreeScope);
 
-        Node* currentRelatedNode = retargeter.currentNode(currentTreeScope);
+        Node* currentRelatedNode = retargeter.currentNode(currentTarget);
         if (UNLIKELY(relatedTargetScoped && !originIsRelatedTarget && context.target() == currentRelatedNode)) {
             m_path.shrink(contextIndex);
             break;
@@ -200,14 +183,15 @@
     if (!targetNode)
         return;
 
-    RelatedNodeRetargeter retargeter(*targetNode, m_path[0]->node()->treeScope());
+    RelatedNodeRetargeter retargeter(*targetNode, *m_path[0]->node());
     TreeScope* previousTreeScope = nullptr;
     for (auto& context : m_path) {
-        TreeScope& currentTreeScope = context->node()->treeScope();
+        Node& currentTarget = *context->node();
+        TreeScope& currentTreeScope = currentTarget.treeScope();
         if (UNLIKELY(previousTreeScope && &currentTreeScope != previousTreeScope))
             retargeter.moveToNewTreeScope(previousTreeScope, currentTreeScope);
 
-        Node* currentRelatedNode = retargeter.currentNode(currentTreeScope);
+        Node* currentRelatedNode = retargeter.currentNode(currentTarget);
         downcast<TouchEventContext>(*context).touchList(touchListType)->append(touch.cloneWithNewTarget(currentRelatedNode));
 
         previousTreeScope = &currentTreeScope;
@@ -243,29 +227,7 @@
     return false;
 }
 
-// http://w3c.github.io/webcomponents/spec/shadow/#dfn-unclosed-node
-static bool isUnclosedNodeOf(const Node& a, const Node& b)
-{
-    // Use Vector instead of HashSet since we expect the number of ancestor tree scopes to be small.
-    Vector<TreeScope*, 8> treeScopesOpenToB;
-
-    for (auto* scope = &b.treeScope(); scope; scope = scope->parentTreeScope())
-        treeScopesOpenToB.append(scope);
-
-    for (auto* treeScopeThatCanAccessA = &a.treeScope(); treeScopeThatCanAccessA; treeScopeThatCanAccessA = treeScopeThatCanAccessA->parentTreeScope()) {
-        for (auto* openToB : treeScopesOpenToB) {
-            if (openToB == treeScopeThatCanAccessA)
-                return true;
-        }
-        auto& root = treeScopeThatCanAccessA->rootNode();
-        if (is<ShadowRoot>(root) && downcast<ShadowRoot>(root).type() != ShadowRoot::Type::Open)
-            break;
-    }
-
-    return false;
-}
-
-Vector<EventTarget*> EventPath::computePathDisclosedToTarget(const EventTarget& target) const
+Vector<EventTarget*> EventPath::computePathUnclosedToTarget(const EventTarget& target) const
 {
     Vector<EventTarget*> path;
     const Node* targetNode = const_cast<EventTarget&>(target).toNode();
@@ -274,7 +236,7 @@
 
     for (auto& context : m_path) {
         if (Node* nodeInPath = context->currentTarget()->toNode()) {
-            if (isUnclosedNodeOf(*nodeInPath, *targetNode))
+            if (targetNode->isUnclosedNode(*nodeInPath))
                 path.append(context->currentTarget());
         }
     }
@@ -282,12 +244,21 @@
     return path;
 }
 
-RelatedNodeRetargeter::RelatedNodeRetargeter(Node& relatedNode, TreeScope& targetTreeScope)
+static Node* moveOutOfAllShadowRoots(Node& startingNode)
+{
+    Node* node = &startingNode;
+    while (node->isInShadowTree())
+        node = downcast<ShadowRoot>(node->treeScope().rootNode()).host();
+    return node;
+}
+
+RelatedNodeRetargeter::RelatedNodeRetargeter(Node& relatedNode, Node& target)
     : m_relatedNode(relatedNode)
     , m_retargetedRelatedNode(&relatedNode)
 {
+    auto& targetTreeScope = target.treeScope();
     TreeScope* currentTreeScope = &m_relatedNode.treeScope();
-    if (LIKELY(currentTreeScope == &targetTreeScope))
+    if (LIKELY(currentTreeScope == &targetTreeScope && target.inDocument() && m_relatedNode.inDocument()))
         return;
 
     if (&currentTreeScope->documentScope() != &targetTreeScope.documentScope()) {
@@ -295,10 +266,9 @@
         m_retargetedRelatedNode = nullptr;
         return;
     }
-    if (relatedNode.inDocument() != targetTreeScope.rootNode().inDocument()) {
+    if (relatedNode.inDocument() != target.inDocument()) {
         m_hasDifferentTreeRoot = true;
-        while (m_retargetedRelatedNode->isInShadowTree())
-            m_retargetedRelatedNode = downcast<ShadowRoot>(m_retargetedRelatedNode->treeScope().rootNode()).host();
+        m_retargetedRelatedNode = moveOutOfAllShadowRoots(relatedNode);
         return;
     }
 
@@ -320,13 +290,24 @@
             break;
     }
 
+    bool lowestCommonAncestorIsDocumentScope = i + 1 == m_ancestorTreeScopes.size();
+    if (lowestCommonAncestorIsDocumentScope && !relatedNode.inDocument() && !target.inDocument()) {
+        Node& targetAncestorInDocumentScope = i ? *downcast<ShadowRoot>(m_ancestorTreeScopes[i - 1]->rootNode()).shadowHost() : target;
+        Node& relatedNodeAncestorInDocumentScope = j ? *downcast<ShadowRoot>(targetTreeScopeAncestors[j - 1]->rootNode()).shadowHost() : relatedNode;
+        if (targetAncestorInDocumentScope.rootNode() != relatedNodeAncestorInDocumentScope.rootNode()) {
+            m_hasDifferentTreeRoot = true;
+            m_retargetedRelatedNode = moveOutOfAllShadowRoots(relatedNode);
+            return;
+        }
+    }
+
     m_lowestCommonAncestorIndex = i;
     m_retargetedRelatedNode = nodeInLowestCommonAncestor();
 }
 
-inline Node* RelatedNodeRetargeter::currentNode(TreeScope& currentTreeScope)
+inline Node* RelatedNodeRetargeter::currentNode(Node& currentTarget)
 {
-    checkConsistency(currentTreeScope);
+    checkConsistency(currentTarget);
     return m_retargetedRelatedNode;
 }
 
@@ -381,17 +362,19 @@
 }
 
 #if !ASSERT_DISABLED
-void RelatedNodeRetargeter::checkConsistency(TreeScope& currentTreeScope)
+void RelatedNodeRetargeter::checkConsistency(Node& currentTarget)
 {
-    for (auto* relatedNodeScope = &m_relatedNode.treeScope(); relatedNodeScope; relatedNodeScope = relatedNodeScope->parentTreeScope()) {
-        for (auto* targetScope = &currentTreeScope; targetScope; targetScope = targetScope->parentTreeScope()) {
-            if (targetScope == relatedNodeScope) {
-                ASSERT(&m_retargetedRelatedNode->treeScope() == relatedNodeScope);
-                return;
-            }
+    ASSERT(!m_retargetedRelatedNode || currentTarget.isUnclosedNode(*m_retargetedRelatedNode));
+
+    // http://w3c.github.io/webcomponents/spec/shadow/#dfn-retargeting-algorithm
+    Node& base = currentTarget;
+    for (Node* targetAncestor = &m_relatedNode; targetAncestor; targetAncestor = targetAncestor->parentOrShadowHostNode()) {
+        if (targetAncestor->rootNode()->containsIncludingShadowDOM(&base)) {
+            ASSERT(m_retargetedRelatedNode == targetAncestor);
+            return;
         }
     }
-    ASSERT(!m_retargetedRelatedNode);
+    ASSERT(!m_retargetedRelatedNode || m_hasDifferentTreeRoot);
 }
 #endif
 
diff --git a/Source/WebCore/dom/EventPath.h b/Source/WebCore/dom/EventPath.h
index a977324..a6c2005 100644
--- a/Source/WebCore/dom/EventPath.h
+++ b/Source/WebCore/dom/EventPath.h
@@ -48,9 +48,9 @@
 
     EventContext* lastContextIfExists() { return m_path.isEmpty() ? nullptr : m_path.last().get(); }
 
-    Vector<EventTarget*> computePathDisclosedToTarget(const EventTarget&) const;
+    Vector<EventTarget*> computePathUnclosedToTarget(const EventTarget&) const;
 
-    static EventTarget* eventTargetRespectingTargetRules(Node& referenceNode)
+    static Node* eventTargetRespectingTargetRules(Node& referenceNode)
     {
         if (is<PseudoElement>(referenceNode))
             return downcast<PseudoElement>(referenceNode).hostElement();
diff --git a/Source/WebCore/dom/Node.cpp b/Source/WebCore/dom/Node.cpp
index 415b406..a242776 100644
--- a/Source/WebCore/dom/Node.cpp
+++ b/Source/WebCore/dom/Node.cpp
@@ -1117,6 +1117,28 @@
     return is<ShadowRoot>(root) ? downcast<ShadowRoot>(&root) : nullptr;
 }
 
+// http://w3c.github.io/webcomponents/spec/shadow/#dfn-unclosed-node
+bool Node::isUnclosedNode(const Node& otherNode) const
+{
+    // Use Vector instead of HashSet since we expect the number of ancestor tree scopes to be small.
+    Vector<TreeScope*, 8> ancestorScopesOfThisNode;
+
+    for (auto* scope = &treeScope(); scope; scope = scope->parentTreeScope())
+        ancestorScopesOfThisNode.append(scope);
+
+    for (auto* treeScopeThatCanAccessOtherNode = &otherNode.treeScope(); treeScopeThatCanAccessOtherNode; treeScopeThatCanAccessOtherNode = treeScopeThatCanAccessOtherNode->parentTreeScope()) {
+        for (auto* scope : ancestorScopesOfThisNode) {
+            if (scope == treeScopeThatCanAccessOtherNode)
+                return true; // treeScopeThatCanAccessOtherNode is a shadow-including inclusive ancestor of this node.
+        }
+        auto& root = treeScopeThatCanAccessOtherNode->rootNode();
+        if (is<ShadowRoot>(root) && downcast<ShadowRoot>(root).type() != ShadowRoot::Type::Open)
+            break;
+    }
+
+    return false;
+}
+
 #if ENABLE(SHADOW_DOM)
 HTMLSlotElement* Node::assignedSlot() const
 {
diff --git a/Source/WebCore/dom/Node.h b/Source/WebCore/dom/Node.h
index 3fbb012..d66a8ef 100644
--- a/Source/WebCore/dom/Node.h
+++ b/Source/WebCore/dom/Node.h
@@ -259,6 +259,7 @@
     WEBCORE_EXPORT Node* deprecatedShadowAncestorNode() const;
     ShadowRoot* containingShadowRoot() const;
     ShadowRoot* shadowRoot() const;
+    bool isUnclosedNode(const Node&) const;
 
 #if ENABLE(SHADOW_DOM)
     HTMLSlotElement* assignedSlot() const;