Title: [208641] trunk
Revision
208641
Author
[email protected]
Date
2016-11-11 21:03:27 -0800 (Fri, 11 Nov 2016)

Log Message

event.composedPath() does not include window
https://bugs.webkit.org/show_bug.cgi?id=164609
<rdar://problem/29210383>

Reviewed by Antti Koivisto.

Source/WebCore:

Fixed the bug by including WindowContext be a part of the regular EventPath. This also simplifies
dispatchEventInDOM which used to had a special logic for dispatching an event on the window.

Also fixed a bug in EventDispatcher::dispatchEvent that event.target would be nullptr when an event was
dispatched inside a disconnected shadow tree or prevented from propagating to the document tree.
Preserve the final target by simply saving event.target() prior to invoking the default event handler instead.

Test: fast/shadow-dom/event-path-with-window.html

* dom/EventDispatcher.cpp:
(WebCore::WindowEventContext): Deleted. Moved to EventPath.cpp.
(WebCore::dispatchEventInDOM): Removed the code for WindowContext. The generic event dispatching logic
will do the same work now.
(WebCore::EventDispatcher::dispatchEvent): Restore the original target instead of using that of WindowContext.
* dom/EventPath.cpp:
(WebCore::WindowEventContext): Moved from EventDispatcher.cpp. Also made it a subclass of EventContext.
(WebCore::WindowEventContext::handleLocalEvents): Added.
(WebCore::EventPath::EventPath): When the parent's nullptr, check if the current node is Document. If it is,
follow https://dom.spec.whatwg.org/#interface-document where it says:
"A document’s get the parent algorithm, given an event, returns null if event’s type attribute value is 'load'
 or document does not have a browsing context, and the document’s associated Window object otherwise."
(WebCore::EventPath::setRelatedTarget): Skip over WindowContext.
(WebCore::EventPath::retargetTouch): Ditto.
(WebCore::EventPath::computePathUnclosedToTarget): When the target is DOMWindow, use its document as the target.
Also, include any event target that is not a node in the event path.

LayoutTests:

Added a W3C style testharness.js test for dispatching an inside a shadow tree connected to a document.

* fast/shadow-dom/event-path-with-window-expected.txt: Added.
* fast/shadow-dom/event-path-with-window.html: Added.
* fast/shadow-dom/resources/event-path-test-helpers.js:
(dispatchEventWithLog): Traverse from document to window. Also include the event object in the log.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (208640 => 208641)


--- trunk/LayoutTests/ChangeLog	2016-11-12 04:00:55 UTC (rev 208640)
+++ trunk/LayoutTests/ChangeLog	2016-11-12 05:03:27 UTC (rev 208641)
@@ -1,3 +1,18 @@
+2016-11-11  Ryosuke Niwa  <[email protected]>
+
+        event.composedPath() does not include window
+        https://bugs.webkit.org/show_bug.cgi?id=164609
+        <rdar://problem/29210383>
+
+        Reviewed by Antti Koivisto.
+
+        Added a W3C style testharness.js test for dispatching an inside a shadow tree connected to a document.
+
+        * fast/shadow-dom/event-path-with-window-expected.txt: Added.
+        * fast/shadow-dom/event-path-with-window.html: Added.
+        * fast/shadow-dom/resources/event-path-test-helpers.js:
+        (dispatchEventWithLog): Traverse from document to window. Also include the event object in the log.
+
 2016-11-11  Joseph Pecoraro  <[email protected]>
 
         test262: DataView get methods should allow for missing offset, set methods should allow for missing value

Added: trunk/LayoutTests/fast/shadow-dom/event-path-with-window-expected.txt (0 => 208641)


--- trunk/LayoutTests/fast/shadow-dom/event-path-with-window-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/fast/shadow-dom/event-path-with-window-expected.txt	2016-11-12 05:03:27 UTC (rev 208641)
@@ -0,0 +1,8 @@
+
+PASS The event must propagate out of open mode shadow boundaries when the composed flag is set 
+PASS The event must propagate out of closed mode shadow boundaries when the composed flag is set 
+PASS The event must propagate out of open mode shadow boundaries when the composed flag is set 
+PASS The event must propagate out of closed mode shadow boundaries when the composed flag is set 
+PASS The event must not propagate out of open mode shadow tree in which the relative target and the relative related target are the same 
+PASS The event must not propagate out of closed mode shadow tree in which the relative target and the relative related target are the same 
+

Added: trunk/LayoutTests/fast/shadow-dom/event-path-with-window.html (0 => 208641)


--- trunk/LayoutTests/fast/shadow-dom/event-path-with-window.html	                        (rev 0)
+++ trunk/LayoutTests/fast/shadow-dom/event-path-with-window.html	2016-11-12 05:03:27 UTC (rev 208641)
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Shadow DOM: Extensions to Event Interface</title>
+<meta name="author" title="Ryosuke Niwa" href=""
+<meta name="assert" content="Event interface must have composedPath() as a method">
+<link rel="help" href=""
+<script src=""
+<script src=""
+<script src=""
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+window.label = 'window';
+document.label = 'document';
+document.documentElement.label = 'html';
+document.body.label = 'body';
+
+// FIXME: Add a test for trimming event path.
+
+/*
+-SR: ShadowRoot  -S: Slot  target: (~)  *: indicates start  digit: event path order
+A ------------------------------- A-SR
++ B ------------ B-SR             + A1 ------ A1-SR (1)
+  + C            + B1 --- B1-SR   + A2-S      + A1a (*; 0)
+  + D --- D-SR     + B1a    + B1b --- B1b-SR
+          + D1              + B1c-S   + B1b1
+                                      + B1b2
+*/
+
+function testEventTargetAfterDispatching(mode) {
+    test(() => {
+        const nodes = createTestTree(mode);
+        const log = dispatchEventWithLog(nodes, nodes.A1a, new Event('my-event', {composed: false, bubbles: true}));
+
+        const expectedPath = ['A1a', 'A1-SR'];
+        assert_array_equals(log.eventPath, expectedPath);
+        assert_array_equals(log.eventPath.length, log.pathAtTargets.length);
+        assert_array_equals(log.pathAtTargets[0], expectedPath);
+        assert_array_equals(log.pathAtTargets[1], expectedPath);
+
+        assert_equals(log.event.target, nodes.A1a);
+        assert_equals(log.event.currentTarget, null);
+    }, 'The event must propagate out of ' + mode + ' mode shadow boundaries when the composed flag is set');
+}
+
+testEventTargetAfterDispatching('open');
+testEventTargetAfterDispatching('closed');
+
+/*
+-SR: ShadowRoot  -S: Slot  target: (~)  *: indicates start  digit: event path order
+window (7)
+ + document (6)
+   + body (5)
+     + A (4) --------------------------- A-SR (3)
+       + B ------------ B-SR             + A1 (2) --- A1-SR (1)
+         + C            + B1 --- B1-SR   + A2-S       + A1a (*; 0)
+         + D --- D-SR     + B1a    + B1b --- B1b-SR
+                 + D1              + B1c-S   + B1b1
+                                             + B1b2
+*/
+
+function testComposedEventInDocument(mode) {
+    test(() => {
+        const nodes = createTestTree(mode);
+        document.body.appendChild(nodes.A);
+
+        const log = dispatchEventWithLog(nodes, nodes.A1a, new Event('my-event', {composed: true, bubbles: true}));
+        let expectedPath = ['A1a', 'A1-SR', 'A1', 'A-SR', 'A', 'body', 'html', 'document', 'window'];
+
+        assert_array_equals(log.eventPath, expectedPath);
+        assert_array_equals(log.eventPath.length, log.pathAtTargets.length);
+        assert_array_equals(log.pathAtTargets[0], expectedPath);
+        assert_array_equals(log.pathAtTargets[1], expectedPath);
+        if (mode != 'open')
+            expectedPath = expectedPath.slice(expectedPath.indexOf('A1'));
+        assert_array_equals(log.pathAtTargets[2], expectedPath);
+        assert_array_equals(log.pathAtTargets[3], expectedPath);
+        if (mode != 'open')
+            expectedPath = expectedPath.slice(expectedPath.indexOf('A'));
+        assert_array_equals(log.pathAtTargets[4], expectedPath);
+        assert_array_equals(log.pathAtTargets[5], expectedPath);
+        assert_array_equals(log.pathAtTargets[6], expectedPath);
+        assert_array_equals(log.pathAtTargets[7], expectedPath);
+
+        assert_equals(log.event.target, nodes.A);
+        assert_equals(log.event.currentTarget, null);
+    }, 'The event must propagate out of ' + mode + ' mode shadow boundaries when the composed flag is set');
+}
+
+testComposedEventInDocument('open');
+testComposedEventInDocument('closed');
+
+/*
+-SR: ShadowRoot  -S: Slot  target: (~)  relatedTarget: [~]  *: indicates start  digit: event path order
+window
+ + document
+   + body
+     + A ------------------------------- A-SR (3)
+       + B ------------ B-SR             + A1 (2) -------- A1-SR (1)
+         + C            + B1 --- B1-SR   + A2-S [*; 0-3]   + A1a (*; 0)
+         + D --- D-SR     + B1a    + B1b --- B1b-SR
+                 + D1              + B1c-S   + B1b1
+                                             + B1b2
+*/
+
+function testComposedEventWithRelatedTargetInDocument(mode) {
+    test(() => {
+        const nodes = createTestTree(mode);
+        document.body.appendChild(nodes.A);
+
+        const log = dispatchEventWithLog(nodes, nodes.A1a, new MouseEvent('foo', {composed: true, bubbles: true, relatedTarget: nodes['A2-S']}));
+        let expectedPath = ['A1a', 'A1-SR', 'A1', 'A-SR'];
+
+        assert_array_equals(log.eventPath, expectedPath);
+        assert_array_equals(log.eventPath.length, log.pathAtTargets.length);
+        assert_array_equals(log.pathAtTargets[0], expectedPath);
+        assert_array_equals(log.pathAtTargets[1], expectedPath);
+        if (mode != 'open')
+            expectedPath = expectedPath.slice(expectedPath.indexOf('A1'));
+        assert_array_equals(log.pathAtTargets[2], expectedPath);
+        assert_array_equals(log.pathAtTargets[3], expectedPath);
+        assert_array_equals(log.relatedTargets, ['A2-S', 'A2-S', 'A2-S', 'A2-S']);
+    }, 'The event must not propagate out of ' + mode + ' mode shadow tree in which the relative target and the relative related target are the same');
+}
+
+testComposedEventWithRelatedTargetInDocument('open');
+testComposedEventWithRelatedTargetInDocument('closed');
+
+</script>
+</body>
+</html>

Modified: trunk/LayoutTests/fast/shadow-dom/resources/event-path-test-helpers.js (208640 => 208641)


--- trunk/LayoutTests/fast/shadow-dom/resources/event-path-test-helpers.js	2016-11-12 04:00:55 UTC (rev 208640)
+++ trunk/LayoutTests/fast/shadow-dom/resources/event-path-test-helpers.js	2016-11-12 05:03:27 UTC (rev 208641)
@@ -7,7 +7,7 @@
     var attachedNodes = [];
     for (var nodeKey in shadow) {
         var startingNode = shadow[nodeKey];
-        for (var node = startingNode; node; node = node.parentNode) {
+        for (var node = startingNode; node; node = node == document ? window : node.parentNode) {
             if (attachedNodes.indexOf(node) >= 0)
                 continue;
             attachedNodes.push(node);
@@ -25,7 +25,7 @@
 
     target.dispatchEvent(event);
 
-    return {eventPath: eventPath, relatedTargets: relatedTargets, pathAtTargets: pathAtTargets};
+    return {event: event, eventPath: eventPath, relatedTargets: relatedTargets, pathAtTargets: pathAtTargets};
 }
 
 /*

Modified: trunk/Source/WebCore/ChangeLog (208640 => 208641)


--- trunk/Source/WebCore/ChangeLog	2016-11-12 04:00:55 UTC (rev 208640)
+++ trunk/Source/WebCore/ChangeLog	2016-11-12 05:03:27 UTC (rev 208641)
@@ -1,3 +1,37 @@
+2016-11-11  Ryosuke Niwa  <[email protected]>
+
+        event.composedPath() does not include window
+        https://bugs.webkit.org/show_bug.cgi?id=164609
+        <rdar://problem/29210383>
+
+        Reviewed by Antti Koivisto.
+
+        Fixed the bug by including WindowContext be a part of the regular EventPath. This also simplifies
+        dispatchEventInDOM which used to had a special logic for dispatching an event on the window.
+
+        Also fixed a bug in EventDispatcher::dispatchEvent that event.target would be nullptr when an event was
+        dispatched inside a disconnected shadow tree or prevented from propagating to the document tree.
+        Preserve the final target by simply saving event.target() prior to invoking the default event handler instead.
+
+        Test: fast/shadow-dom/event-path-with-window.html
+
+        * dom/EventDispatcher.cpp:
+        (WebCore::WindowEventContext): Deleted. Moved to EventPath.cpp.
+        (WebCore::dispatchEventInDOM): Removed the code for WindowContext. The generic event dispatching logic
+        will do the same work now.
+        (WebCore::EventDispatcher::dispatchEvent): Restore the original target instead of using that of WindowContext.
+        * dom/EventPath.cpp:
+        (WebCore::WindowEventContext): Moved from EventDispatcher.cpp. Also made it a subclass of EventContext.
+        (WebCore::WindowEventContext::handleLocalEvents): Added.
+        (WebCore::EventPath::EventPath): When the parent's nullptr, check if the current node is Document. If it is,
+        follow https://dom.spec.whatwg.org/#interface-document where it says:
+        "A document’s get the parent algorithm, given an event, returns null if event’s type attribute value is 'load'
+         or document does not have a browsing context, and the document’s associated Window object otherwise."
+        (WebCore::EventPath::setRelatedTarget): Skip over WindowContext.
+        (WebCore::EventPath::retargetTouch): Ditto.
+        (WebCore::EventPath::computePathUnclosedToTarget): When the target is DOMWindow, use its document as the target.
+        Also, include any event target that is not a node in the event path.
+
 2016-11-11  Dave Hyatt  <[email protected]>
 
         [CSS Parser] Support all the correct blend modes

Modified: trunk/Source/WebCore/dom/EventDispatcher.cpp (208640 => 208641)


--- trunk/Source/WebCore/dom/EventDispatcher.cpp	2016-11-12 04:00:55 UTC (rev 208640)
+++ trunk/Source/WebCore/dom/EventDispatcher.cpp	2016-11-12 05:03:27 UTC (rev 208641)
@@ -39,40 +39,6 @@
 
 namespace WebCore {
 
-class WindowEventContext {
-public:
-    WindowEventContext(Node*, const EventContext*);
-
-    DOMWindow* window() const { return m_window.get(); }
-    EventTarget* target() const { return m_target.get(); }
-    bool handleLocalEvents(Event&);
-
-private:
-    RefPtr<DOMWindow> m_window;
-    RefPtr<EventTarget> m_target;
-};
-
-WindowEventContext::WindowEventContext(Node* node, const EventContext* topEventContext)
-{
-    Node* topLevelContainer = topEventContext ? topEventContext->node() : node;
-    if (!is<Document>(*topLevelContainer))
-        return;
-
-    m_window = downcast<Document>(*topLevelContainer).domWindow();
-    m_target = topEventContext ? topEventContext->target() : node;
-}
-
-bool WindowEventContext::handleLocalEvents(Event& event)
-{
-    if (!m_window)
-        return false;
-
-    event.setTarget(m_target.copyRef());
-    event.setCurrentTarget(m_window.get());
-    m_window->fireEventListeners(event);
-    return true;
-}
-
 void EventDispatcher::dispatchScopedEvent(Node& node, Event& event)
 {
     // We need to set the target here because it can go away by the time we actually fire the event.
@@ -101,17 +67,11 @@
     }
 }
 
-static void dispatchEventInDOM(Event& event, const EventPath& path, WindowEventContext& windowEventContext)
+static void dispatchEventInDOM(Event& event, const EventPath& path)
 {
     // Trigger capturing event handlers, starting at the top and working our way down.
     event.setEventPhase(Event::CAPTURING_PHASE);
 
-    // We don't dispatch load events to the window. This quirk was originally
-    // added because Mozilla doesn't propagate load events to the window object.
-    bool shouldFireEventAtWindow = event.type() != eventNames().loadEvent;
-    if (shouldFireEventAtWindow && windowEventContext.handleLocalEvents(event) && event.propagationStopped())
-        return;
-
     for (size_t i = path.size() - 1; i > 0; --i) {
         const EventContext& eventContext = path.contextAt(i);
         if (eventContext.currentTargetSameAsTarget())
@@ -140,11 +100,6 @@
         if (event.propagationStopped())
             return;
     }
-    if (event.bubbles() && !event.cancelBubble()) {
-        event.setEventPhase(Event::BUBBLING_PHASE);
-        if (shouldFireEventAtWindow)
-            windowEventContext.handleLocalEvents(event);
-    }
 }
 
 bool EventDispatcher::dispatchEvent(Node* origin, Event& event)
@@ -171,8 +126,6 @@
 
     ASSERT_WITH_SECURITY_IMPLICATION(!NoEventDispatchAssertion::isEventDispatchForbidden());
 
-    WindowEventContext windowEventContext(node.get(), eventPath.lastContextIfExists());
-
     InputElementClickState clickHandlingState;
     if (is<HTMLInputElement>(*node))
         downcast<HTMLInputElement>(*node).willDispatchEvent(event, clickHandlingState);
@@ -179,10 +132,11 @@
 
     if (!event.propagationStopped() && !eventPath.isEmpty()) {
         event.setEventPath(eventPath);
-        dispatchEventInDOM(event, eventPath, windowEventContext);
+        dispatchEventInDOM(event, eventPath);
         event.clearEventPath();
     }
 
+    auto* finalTarget = event.target();
     event.setTarget(EventPath::eventTargetRespectingTargetRules(*node));
     event.setCurrentTarget(nullptr);
     event.resetPropagationFlags();
@@ -197,9 +151,7 @@
     if (!event.defaultPrevented() && !event.defaultHandled())
         callDefaultEventHandlersInTheBubblingOrder(event, eventPath);
 
-    // Ensure that after event dispatch, the event's target object is the
-    // outermost shadow DOM boundary.
-    event.setTarget(windowEventContext.target());
+    event.setTarget(finalTarget);
     event.setCurrentTarget(nullptr);
 
     return !event.defaultPrevented();

Modified: trunk/Source/WebCore/dom/EventPath.cpp (208640 => 208641)


--- trunk/Source/WebCore/dom/EventPath.cpp	2016-11-12 04:00:55 UTC (rev 208640)
+++ trunk/Source/WebCore/dom/EventPath.cpp	2016-11-12 05:03:27 UTC (rev 208641)
@@ -21,6 +21,7 @@
 #include "config.h"
 #include "EventPath.h"
 
+#include "DOMWindow.h"
 #include "Event.h"
 #include "EventContext.h"
 #include "EventNames.h"
@@ -32,6 +33,23 @@
 
 namespace WebCore {
 
+class WindowEventContext final : public EventContext {
+public:
+    WindowEventContext(Node&, DOMWindow&, EventTarget*);
+    void handleLocalEvents(Event&) const final;
+};
+
+WindowEventContext::WindowEventContext(Node& node, DOMWindow& currentTarget, EventTarget* target)
+    : EventContext(&node, &currentTarget, target)
+{ }
+
+void WindowEventContext::handleLocalEvents(Event& event) const
+{
+    event.setTarget(m_target.get());
+    event.setCurrentTarget(m_currentTarget.get());
+    m_currentTarget->fireEventListeners(event);
+}
+
 static inline bool shouldEventCrossShadowBoundary(Event& event, ShadowRoot& shadowRoot, EventTarget& target)
 {
     Node* targetNode = target.toNode();
@@ -108,10 +126,18 @@
                 break;
 
             ContainerNode* parent = node->parentNode();
-            if (!parent)
+            if (UNLIKELY(!parent)) {
+                // https://dom.spec.whatwg.org/#interface-document
+                if (is<Document>(*node) && event.type() != eventNames().loadEvent) {
+                    ASSERT(target);
+                    if (auto* window = downcast<Document>(*node).domWindow())
+                        m_path.append(std::make_unique<WindowEventContext>(*node, *window, target));
+                }
                 return;
+            }
 
-            if (ShadowRoot* shadowRootOfParent = parent->shadowRoot()) {
+            auto* shadowRootOfParent = parent->shadowRoot();
+            if (UNLIKELY(shadowRootOfParent)) {
                 if (auto* assignedSlot = shadowRootOfParent->findAssignedSlot(*node)) {
                     // node is assigned to a slot. Continue dispatching the event at this slot.
                     parent = assignedSlot;
@@ -144,7 +170,10 @@
     TreeScope* previousTreeScope = nullptr;
     size_t originalEventPathSize = m_path.size();
     for (unsigned contextIndex = 0; contextIndex < originalEventPathSize; contextIndex++) {
-        auto& context = downcast<MouseOrFocusEventContext>(*m_path[contextIndex]);
+        auto& ambgiousContext = *m_path[contextIndex];
+        if (!is<MouseOrFocusEventContext>(ambgiousContext))
+            continue;
+        auto& context = downcast<MouseOrFocusEventContext>(ambgiousContext);
 
         Node& currentTarget = *context.node();
         TreeScope& currentTreeScope = currentTarget.treeScope();
@@ -187,8 +216,10 @@
         if (UNLIKELY(previousTreeScope && &currentTreeScope != previousTreeScope))
             retargeter.moveToNewTreeScope(previousTreeScope, currentTreeScope);
 
-        Node* currentRelatedNode = retargeter.currentNode(currentTarget);
-        downcast<TouchEventContext>(*context).touchList(touchListType)->append(touch.cloneWithNewTarget(currentRelatedNode));
+        if (is<TouchEventContext>(*context)) {
+            Node* currentRelatedNode = retargeter.currentNode(currentTarget);
+            downcast<TouchEventContext>(*context).touchList(touchListType)->append(touch.cloneWithNewTarget(currentRelatedNode));
+        }
 
         previousTreeScope = &currentTreeScope;
     }
@@ -223,18 +254,25 @@
     return false;
 }
 
+// https://dom.spec.whatwg.org/#dom-event-composedpath
 Vector<EventTarget*> EventPath::computePathUnclosedToTarget(const EventTarget& target) const
 {
     Vector<EventTarget*> path;
     const Node* targetNode = const_cast<EventTarget&>(target).toNode();
-    if (!targetNode)
-        return path;
+    if (!targetNode) {
+        const DOMWindow* domWindow = const_cast<EventTarget&>(target).toDOMWindow();
+        if (!domWindow)
+            return path;
+        targetNode = domWindow->document();
+        ASSERT(targetNode);
+    }
 
     for (auto& context : m_path) {
         if (Node* nodeInPath = context->currentTarget()->toNode()) {
             if (targetNode->isUnclosedNode(*nodeInPath))
                 path.append(context->currentTarget());
-        }
+        } else
+            path.append(context->currentTarget());
     }
 
     return path;
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to