Implement proper handling of events with a related target in regard to shadow DOM...
authorhayato@chromium.org <hayato@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 12 Aug 2011 05:18:28 +0000 (05:18 +0000)
committerhayato@chromium.org <hayato@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 12 Aug 2011 05:18:28 +0000 (05:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=65899

Reviewed by Dimitri Glazkov.

Fixes issues in the following corner cases:
1. When both a target node and a relatedTarget node are immediate children of
the same shadow root, an event is not dispatched.
2. If a target node is an ancestor of a relatedTarget node, crossing
shadow boundaries, or vice verse, an event is not dispatched or wrongly
dispatched.

Source/WebCore:

Test: fast/dom/shadow/shadow-boundary-events.html

* dom/EventDispatcher.cpp:
(WebCore::EventDispatcher::adjustToShadowBoundaries):

LayoutTests:

* fast/dom/shadow/shadow-boundary-crossing-expected.txt: Renamed from LayoutTests/fast/events/shadow-boundary-crossing-expected.txt.
* fast/dom/shadow/shadow-boundary-crossing.html: Renamed from LayoutTests/fast/events/shadow-boundary-crossing.html.
* fast/dom/shadow/shadow-boundary-events-expected.txt: Added.
* fast/dom/shadow/shadow-boundary-events.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@92922 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/fast/dom/shadow/shadow-boundary-crossing-expected.txt [moved from LayoutTests/fast/events/shadow-boundary-crossing-expected.txt with 100% similarity]
LayoutTests/fast/dom/shadow/shadow-boundary-crossing.html [moved from LayoutTests/fast/events/shadow-boundary-crossing.html with 100% similarity]
LayoutTests/fast/dom/shadow/shadow-boundary-events-expected.txt [new file with mode: 0644]
LayoutTests/fast/dom/shadow/shadow-boundary-events.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/EventDispatcher.cpp

index 47b7cdd134fddeb35ce2fe0ec095967ad7c602d1..247aa1c21dbd786113e27eab86da20cf1f05f2b2 100644 (file)
@@ -1,3 +1,22 @@
+2011-08-11  Hayato Ito  <hayato@chromium.org>
+
+        Implement proper handling of events with a related target in regard to shadow DOM boundaries.
+        https://bugs.webkit.org/show_bug.cgi?id=65899
+
+        Reviewed by Dimitri Glazkov.
+
+        Fixes issues in the following corner cases:
+        1. When both a target node and a relatedTarget node are immediate children of
+        the same shadow root, an event is not dispatched.
+        2. If a target node is an ancestor of a relatedTarget node, crossing
+        shadow boundaries, or vice verse, an event is not dispatched or wrongly
+        dispatched.
+
+        * fast/dom/shadow/shadow-boundary-crossing-expected.txt: Renamed from LayoutTests/fast/events/shadow-boundary-crossing-expected.txt.
+        * fast/dom/shadow/shadow-boundary-crossing.html: Renamed from LayoutTests/fast/events/shadow-boundary-crossing.html.
+        * fast/dom/shadow/shadow-boundary-events-expected.txt: Added.
+        * fast/dom/shadow/shadow-boundary-events.html: Added.
+
 2011-08-11  Ryosuke Niwa  <rniwa@webkit.org>
 
         Share code between isStyleSpanOrSpanWithOnlyStyleAttribute, isUnstyledStyleSpan,
diff --git a/LayoutTests/fast/dom/shadow/shadow-boundary-events-expected.txt b/LayoutTests/fast/dom/shadow/shadow-boundary-events-expected.txt
new file mode 100644 (file)
index 0000000..0de3dd2
--- /dev/null
@@ -0,0 +1,64 @@
+Tests to ensure that shadow DOM boundary is not crossed during event propagation. Can only run within DRT.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+
+Move mouse from a node to its sibling node. All nodes are outside of shadow boundary.
+Moving mouse from divB to divC
+PASS dispatchedEvent("mouseover") is ["divC(<-divB)(@divC)", "divC(<-divB)(@divA)"]
+PASS dispatchedEvent("mouseout") is ["divB(<-divC)(@divB)", "divB(<-divC)(@divA)"]
+
+Target is an ancestor of relatedTarget. All nodes are outside of shadow boundary.
+Moving mouse from divB to divA
+PASS dispatchedEvent("mouseover") is ["divA(<-divB)(@divA)"]
+PASS dispatchedEvent("mouseout") is ["divB(<-divA)(@divB)", "divB(<-divA)(@divA)"]
+
+RelatedTarget is an ancestor of target. All nodes are outside of shadow boundary.
+Moving mouse from divA to divB
+PASS dispatchedEvent("mouseover") is ["divB(<-divA)(@divB)", "divB(<-divA)(@divA)"]
+PASS dispatchedEvent("mouseout") is ["divA(<-divB)(@divA)"]
+
+Both target and relatedTarget are immediate children of the same shadow root.
+Moving mouse from shadowD/shadowF/shadowG/divH to shadowD/shadowF/shadowG/divI
+PASS dispatchedEvent("mouseover") is ["divI(<-divH)(@divI)"]
+PASS dispatchedEvent("mouseout") is ["divH(<-divI)(@divH)"]
+
+Target is an ancestor of relatedTarget.
+Moving mouse from shadowD/shadowF/shadowG/divI to shadowD/divE
+PASS dispatchedEvent("mouseover") is ["divE(<-shadowF)(@divE)"]
+PASS dispatchedEvent("mouseout") is ["divI(<-divE)(@divI)", "shadowG(<-divE)(@shadowG)", "shadowF(<-divE)(@shadowF)", "shadowF(<-divE)(@divE)"]
+
+Target (shadow host) is an ancestor of relatedTarget.
+Moving mouse from shadowD/shadowF/shadowG/divI to shadowD/shadowF
+PASS dispatchedEvent("mouseover") is []
+PASS dispatchedEvent("mouseout") is ["divI(<-shadowF)(@divI)", "shadowG(<-shadowF)(@shadowG)"]
+
+Target (shadow host) is an ancestor of relatedTarget (shadow host).
+Moving mouse from shadowD/shadowF/shadowG to shadowD
+PASS dispatchedEvent("mouseover") is []
+PASS dispatchedEvent("mouseout") is ["shadowG(<-shadowD)(@shadowG)", "shadowF(<-shadowD)(@shadowF)", "shadowF(<-shadowD)(@divE)"]
+
+RelatedTarget is ancestor of target.
+Moving mouse from shadowD/divE to shadowD/shadowF/shadowG/divI
+PASS dispatchedEvent("mouseover") is ["divI(<-divE)(@divI)", "shadowG(<-divE)(@shadowG)", "shadowF(<-divE)(@shadowF)", "shadowF(<-divE)(@divE)"]
+PASS dispatchedEvent("mouseout") is ["divE(<-shadowF)(@divE)"]
+
+RelatedTarget (shadow host) is ancestor of target.
+Moving mouse from shadowD/shadowF to shadowD/shadowF/shadowG/divI
+PASS dispatchedEvent("mouseover") is ["divI(<-shadowF)(@divI)", "shadowG(<-shadowF)(@shadowG)"]
+PASS dispatchedEvent("mouseout") is []
+
+RelatedTarget (shadow host) is an ancestor of target (shadow host).
+Moving mouse from shadowD to shadowD/shadowF/shadowG
+PASS dispatchedEvent("mouseover") is ["shadowG(<-shadowD)(@shadowG)", "shadowF(<-shadowD)(@shadowF)", "shadowF(<-shadowD)(@divE)"]
+PASS dispatchedEvent("mouseout") is []
+
+Target and relatedTarget exist in separated subtree, crossing shadow boundaries. Making sure that event is not dispatched beyond the lowest common boundary.
+Moving mouse from shadowD/shadowF/shadowG/divH to shadowD/shadowK/divL
+PASS dispatchedEvent("mouseover") is ["divL(<-shadowF)(@divL)", "shadowK(<-shadowF)(@shadowK)", "shadowK(<-shadowF)(@divJ)"]
+PASS dispatchedEvent("mouseout") is ["divH(<-shadowK)(@divH)", "shadowG(<-shadowK)(@shadowG)", "shadowF(<-shadowK)(@shadowF)", "shadowF(<-shadowK)(@divE)"]
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/dom/shadow/shadow-boundary-events.html b/LayoutTests/fast/dom/shadow/shadow-boundary-events.html
new file mode 100644 (file)
index 0000000..6088633
--- /dev/null
@@ -0,0 +1,177 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../js/resources/js-test-pre.js"></script>
+<script src="resources/create-dom.js"></script>
+</head>
+<body>
+<p id="description"></p>
+<div id="sandbox"></div>
+<pre id="console"></pre>
+<script>
+description("Tests to ensure that shadow DOM boundary is not crossed during event propagation. Can only run within DRT.");
+
+function moveMouseOver(element)
+{
+    if (!window.eventSender || !window.internals)
+        return;
+
+    var defaultPaddingSize = 20;
+    var x = element.offsetLeft + element.offsetWidth / 2;
+    var y;
+    if (element.hasChildNodes() || window.internals.shadowRoot(element))
+        y = element.offsetTop + defaultPaddingSize;
+    else
+        y = element.offsetTop + element.offsetHeight / 2;
+    eventSender.mouseMoveTo(x, y);
+}
+
+var eventRecords = {};
+
+function clearEventRecords()
+{
+    eventRecords = {};
+}
+
+function dispatchedEvent(eventType)
+{
+    var events = eventRecords[eventType];
+    if (!events)
+        return [];
+    return events;
+}
+
+function recordEvent(event)
+{
+    var eventType = event.type
+    if (!eventRecords[eventType]) {
+        eventRecords[eventType] = []
+    }
+    // Records each event in the following format per event type:
+    //   eventRecords[eventType] = ['target.id(<-relatedTarget.id)(@currentTarget.id)',,,]
+    //   * RelatedTarget and currentTarget may be omitted if they are not defined.
+    // A new event is pushed back to the array of its event type.
+    var eventString = '';
+    eventString += event.target.id;
+    if (event.relatedTarget)
+        eventString += '(<-' + event.relatedTarget.id + ')';
+    if (event.currentTarget)
+        eventString += '(@' + event.currentTarget.id + ')';
+    eventRecords[eventType].push(eventString);
+}
+
+function getElementInShadow(path)
+{
+    var ids = path.split('/');
+    var element = document.getElementById(ids[0]);
+    for (var i = 1; element != null && i < ids.length; ++i) {
+        var shadowRoot = internals.shadowRoot(element);
+        element = internals.getElementByIdInShadowRoot(shadowRoot, ids[i]);
+    }
+    return element;
+}
+
+function prepareDomTree(parent)
+{
+    parent.appendChild(
+        createDom('div', {'id': 'divA', 'style': 'padding-top: 40px'},
+                  createDom('div', {'id': 'divB', 'style': 'width: 40px; height: 40px'}),
+                  createDom('div', {'id': 'divC', 'style': 'width: 40px; height: 40px'}),
+                  createShadow('div', {'id': 'shadowD', 'style': 'padding-top: 40px'},
+                               createDom('div', {'id': 'divE', 'style': 'padding-top: 40px'},
+                                         createShadow('div', {'id': 'shadowF', 'style': 'padding-top: 40px'},
+                                                      createShadow('div', {'id': 'shadowG', 'style': 'padding-top: 40px'},
+                                                                   createDom('div', {'id': 'divH', 'style': 'width: 40px; height: 40px'}),
+                                                                   createDom('div', {'id': 'divI', 'style': 'width: 40px; height: 40px'})))),
+                               createDom('div', {'id': 'divJ', 'style': 'padding-top: 40px'},
+                                         createShadow('div', {'id': 'shadowK', 'style': 'padding-top: 40px'},
+                                                      createDom('div', {'id': 'divL', 'style': 'width: 40px; height: 40px'}))))));
+
+    var ids = ['divA', 'divB', 'divC',
+               'shadowD', 'shadowD/divE', 'shadowD/shadowF', 'shadowD/shadowF/shadowG',
+               'shadowD/shadowF/shadowG/divH', 'shadowD/shadowF/shadowG/divI',
+               'shadowD/divJ', 'shadowD/shadowK', 'shadowD/shadowK/divL'];
+    for (var i = 0; i < ids.length; ++i) {
+        var element = getElementInShadow(ids[i]);
+        element.addEventListener('mouseover', recordEvent, false);
+        element.addEventListener('mouseout', recordEvent, false);
+    }
+}
+
+function moveMouse(oldElementId, newElementId, message)
+{
+    debug('\n' + message + '\n' + 'Moving mouse from ' + oldElementId + ' to ' + newElementId);
+    moveMouseOver(getElementInShadow(oldElementId));
+    clearEventRecords();
+    moveMouseOver(getElementInShadow(newElementId));
+}
+
+function test()
+{
+    if (window.layoutTestController)
+        layoutTestController.dumpAsText();
+    prepareDomTree(document.getElementById('sandbox'));
+
+    moveMouse('divB', 'divC',
+              'Move mouse from a node to its sibling node. All nodes are outside of shadow boundary.');
+    shouldBe('dispatchedEvent("mouseover")', '["divC(<-divB)(@divC)", "divC(<-divB)(@divA)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divB(<-divC)(@divB)", "divB(<-divC)(@divA)"]');
+
+    moveMouse('divB', 'divA',
+              'Target is an ancestor of relatedTarget. All nodes are outside of shadow boundary.');
+    shouldBe('dispatchedEvent("mouseover")', '["divA(<-divB)(@divA)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divB(<-divA)(@divB)", "divB(<-divA)(@divA)"]');
+
+    moveMouse('divA', 'divB',
+              'RelatedTarget is an ancestor of target. All nodes are outside of shadow boundary.');
+    shouldBe('dispatchedEvent("mouseover")', '["divB(<-divA)(@divB)", "divB(<-divA)(@divA)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divA(<-divB)(@divA)"]');
+
+    moveMouse('shadowD/shadowF/shadowG/divH', 'shadowD/shadowF/shadowG/divI',
+              'Both target and relatedTarget are immediate children of the same shadow root.');
+    shouldBe('dispatchedEvent("mouseover")', '["divI(<-divH)(@divI)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divH(<-divI)(@divH)"]');
+
+    moveMouse('shadowD/shadowF/shadowG/divI', 'shadowD/divE',
+              'Target is an ancestor of relatedTarget.');
+    shouldBe('dispatchedEvent("mouseover")', '["divE(<-shadowF)(@divE)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divI(<-divE)(@divI)", "shadowG(<-divE)(@shadowG)", "shadowF(<-divE)(@shadowF)", "shadowF(<-divE)(@divE)"]');
+
+    moveMouse('shadowD/shadowF/shadowG/divI', 'shadowD/shadowF',
+              'Target (shadow host) is an ancestor of relatedTarget.');
+    shouldBe('dispatchedEvent("mouseover")', '[]');
+    shouldBe('dispatchedEvent("mouseout")', '["divI(<-shadowF)(@divI)", "shadowG(<-shadowF)(@shadowG)"]');
+
+    moveMouse('shadowD/shadowF/shadowG', 'shadowD',
+              'Target (shadow host) is an ancestor of relatedTarget (shadow host).');
+    shouldBe('dispatchedEvent("mouseover")', '[]');
+    shouldBe('dispatchedEvent("mouseout")', '["shadowG(<-shadowD)(@shadowG)", "shadowF(<-shadowD)(@shadowF)", "shadowF(<-shadowD)(@divE)"]');
+
+    moveMouse('shadowD/divE', 'shadowD/shadowF/shadowG/divI',
+              'RelatedTarget is ancestor of target.');
+    shouldBe('dispatchedEvent("mouseover")', '["divI(<-divE)(@divI)", "shadowG(<-divE)(@shadowG)", "shadowF(<-divE)(@shadowF)", "shadowF(<-divE)(@divE)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divE(<-shadowF)(@divE)"]');
+
+    moveMouse('shadowD/shadowF', 'shadowD/shadowF/shadowG/divI',
+              'RelatedTarget (shadow host) is ancestor of target.');
+    shouldBe('dispatchedEvent("mouseover")', '["divI(<-shadowF)(@divI)", "shadowG(<-shadowF)(@shadowG)"]');
+    shouldBe('dispatchedEvent("mouseout")', '[]');
+
+    moveMouse('shadowD', 'shadowD/shadowF/shadowG',
+              'RelatedTarget (shadow host) is an ancestor of target (shadow host).');
+    shouldBe('dispatchedEvent("mouseover")', '["shadowG(<-shadowD)(@shadowG)", "shadowF(<-shadowD)(@shadowF)", "shadowF(<-shadowD)(@divE)"]');
+    shouldBe('dispatchedEvent("mouseout")', '[]');
+
+    moveMouse('shadowD/shadowF/shadowG/divH', 'shadowD/shadowK/divL',
+              'Target and relatedTarget exist in separated subtree, crossing shadow boundaries. Making sure that event is not dispatched beyond the lowest common boundary.');
+    shouldBe('dispatchedEvent("mouseover")', '["divL(<-shadowF)(@divL)", "shadowK(<-shadowF)(@shadowK)", "shadowK(<-shadowF)(@divJ)"]');
+    shouldBe('dispatchedEvent("mouseout")', '["divH(<-shadowK)(@divH)", "shadowG(<-shadowK)(@shadowG)", "shadowF(<-shadowK)(@shadowF)", "shadowF(<-shadowK)(@divE)"]');
+}
+
+test();
+
+var successfullyParsed = true;
+</script>
+<script src="../../js/resources/js-test-post.js"></script>
+</body>
+</html>
index 7b75a46135fb27891252519991b6ef9e1cb3dabf..1b38611410afc23033872a382a5c9eba67efafba 100644 (file)
@@ -1,3 +1,22 @@
+2011-08-11  Hayato Ito  <hayato@chromium.org>
+
+        Implement proper handling of events with a related target in regard to shadow DOM boundaries.
+        https://bugs.webkit.org/show_bug.cgi?id=65899
+
+        Reviewed by Dimitri Glazkov.
+
+        Fixes issues in the following corner cases:
+        1. When both a target node and a relatedTarget node are immediate children of
+        the same shadow root, an event is not dispatched.
+        2. If a target node is an ancestor of a relatedTarget node, crossing
+        shadow boundaries, or vice verse, an event is not dispatched or wrongly
+        dispatched.
+
+        Test: fast/dom/shadow/shadow-boundary-events.html
+
+        * dom/EventDispatcher.cpp:
+        (WebCore::EventDispatcher::adjustToShadowBoundaries):
+
 2011-08-11  John Bauman  <jbauman@chromium.org>
 
         Readback composited webgl results for printing
index 8b01f34dc0c325ce92c71eeb2091c54aca1e3769..39921c0d61abb95410d002776cb53d92cbaf939a 100644 (file)
@@ -164,20 +164,22 @@ PassRefPtr<EventTarget> EventDispatcher::adjustToShadowBoundaries(PassRefPtr<Nod
     if (!diverged) {
         // The relatedTarget is an ancestor or shadowHost of the target.
         // FIXME: Remove the first check once conversion to new shadow DOM is complete <http://webkit.org/b/48698>
-        if (m_node->shadowHost() == relatedTarget.get() || isShadowHost(relatedTarget.get()))
-            lowestCommonBoundary = m_ancestors.begin();
+        if (m_node->shadowHost() == relatedTarget.get() || isShadowHost(relatedTarget.get())) {
+            ASSERT(targetAncestor - 1 >= m_ancestors.begin());
+            lowestCommonBoundary = targetAncestor - 1;
+        }
     } else if ((*firstDivergentBoundary) == m_node.get()) {
         // Since ancestors does not contain target itself, we must account
         // for the possibility that target is a shadowHost of relatedTarget
         // and thus serves as the lowestCommonBoundary.
         // Luckily, in this case the firstDivergentBoundary is target.
         lowestCommonBoundary = m_ancestors.begin();
+        m_shouldPreventDispatch = true;
     }
 
     if (lowestCommonBoundary != m_ancestors.end()) {
         // Trim ancestors to lowestCommonBoundary to keep events inside of the common shadow DOM subtree.
         m_ancestors.shrink(lowestCommonBoundary - m_ancestors.begin());
-        m_shouldPreventDispatch = !m_ancestors.size();
     }
     // Set event's related target to the first encountered shadow DOM boundary in the divergent subtree.
     return firstDivergentBoundary != relatedTargetAncestors.begin() ? *firstDivergentBoundary : relatedTarget;