forked from WebKit/WebKit-http
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Capturing event listeners are called during bubbling phase for shadow…
… hosts https://bugs.webkit.org/show_bug.cgi?id=174288 LayoutTests/imported/w3c: Reviewed by Darin Adler. * web-platform-tests/dom/events/Event-dispatch-handlers-changed-expected.txt: Rebaselined. This test's expectation is now wrong because event listner 3 is added after the event listener list is cloned for capturing event listeners but before cloned for bubbling event listeners. As a result, event listener 3 is now invoked. It used to be not called because both bubbling and capturing event listeners are called after cloning the event listner list once, which didn't have event listener 3. * web-platform-tests/dom/events/EventTarget-dispatchEvent-expected.txt: Rebaselined. This test expects event listener 2, which is bubbling, to be called between two capturing event listeners 1 and 3, which is no longer true after this patch. Source/WebCore: <rdar://problem/33530455> Reviewed by Darin Adler. Implemented the new behavior proposed in whatwg/dom#686 [1] to fix the problem that capturing event listeners on a shadow host is invoked during bubbling phase when an event is dispatched within its shadow tree. To see why this is a problem, suppose we fire a composed event at span#target in the following DOM tree: section#hostParent + div#host -- ShadowRoot - p#parent - span#target Then capturing and bubbling event listeners on #target, #parent, #host, and #hostParent are invoked in the following order in WebKit & Chrome right now: 1. #hostParent, capturing, eventPhase: CAPTURING_PHASE 2. #parent, capturing, eventPhase: CAPTURING_PHASE 3. #target, capturing, eventPhase: AT_TARGET 4. #target, non-capturing, eventPhase: AT_TARGET 5. #parent, non-capturing, eventPhase: BUBBLING_PHASE 6. #host, capturing, eventPhase: AT_TARGET 7. #host, non-capturing, eventPhase: AT_TARGET 8. #hostParent, non-capturing, eventPhase: BUBBLING_PHASE This is counter-intuitive because capturing event listeners on #host isn't invoked until bubblign phase started. A more natural ordering would be: 1. #hostParent, capturing, eventPhase: CAPTURING_PHASE 2. #host, capturing, eventPhase: AT_TARGET 3. #parent, capturing, eventPhase: CAPTURING_PHASE 4. #target, capturing, eventPhase: AT_TARGET 5. #target, non-capturing, eventPhase: AT_TARGET 6. #parent, non-capturing, eventPhase: BUBBLING_PHASE 7. #host, non-capturing, eventPhase: AT_TARGET 8. #hostParent, non-capturing, eventPhase: BUBBLING_PHASE This also happens to be the order by which Gecko's current shadow DOM implementation invoke event listners. This patch implements this new behavior using the spec-change proposed in [1]. Note that this patch also impacts the invocation order of event listeners when there is no shadow tree. Namely, before this patch, event listeners on the event's target is invoked in the registration order. After this patch, all capturing event listeners are invoked before bubbling event listeners are invoked. To implement this behavior, this patch introduces EventTarget::EventInvokePhase indicating whether we're in the capturing phase or bubbling phase to EventTarget::fireEventListeners. We can't use Event's eventPhase enum because that's set to Event::Phase::AT_TARGET when we're at a shadow host. Test: fast/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees.html * Modules/modern-media-controls/media/media-controller-support.js: (MediaControllerSupport.prototype.enable): Use capturing event listeners so that we can update the states of media controls before author scripts recieve the event. (MediaControllerSupport.prototype.disable): Ditto. * dom/EventContext.cpp: (WebCore::EventContext::handleLocalEvents const): (WebCore::MouseOrFocusEventContext::handleLocalEvents const): (WebCore::TouchEventContext::handleLocalEvents const): * dom/EventContext.h: * dom/EventDispatcher.cpp: (WebCore::dispatchEventInDOM): Invoke capturing event listners even when target and current target are same. This happens when the current target is a shadow host and event's target is in its shadow tree. Also merged the special code path for the event's target with the code in the bubbling phase. * dom/EventPath.cpp: (WebCore::WindowEventContext::handleLocalEvents const): * dom/EventTarget.cpp: (WebCore::EventTarget::dispatchEvent): Invoke capturing and bubbling event listeners in the order. (WebCore::EventTarget::fireEventListeners): (WebCore::EventTarget::innerInvokeEventListeners): Renamed from fireEventListeners to match the spec. Use EventInvokePhase to filter out event listeners so that we can invoke capturing event listners before bubbling event listeners even when eventPhase is Event::Phase::AT_TARGET. * dom/EventTarget.h: * dom/Node.cpp: (WebCore::Node::handleLocalEvents): * dom/Node.h: * html/HTMLFormElement.cpp: (WebCore::HTMLFormElement::handleLocalEvents): * html/HTMLFormElement.h: * page/DOMWindow.cpp: (WebCore::DOMWindow::dispatchEvent): LayoutTests: Reviewed by Darin Adler. Added a W3C style testharness.js test and rebaselined two tests. See below for rationals of rebaselines. * fast/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees-expected.txt: Added. * fast/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees.html: Added. * media/media-load-event-expected.txt: Rebaselined. The logging of oncanplaythrough event is now happening before canplaythrough() is called because the logging is done by waitForEvent which uses a capturing event listener whereas canplaythrough is called by a event handler, which is non-capturing. * platform/ios-11/imported/w3c/web-platform-tests/dom/events/EventTarget-dispatchEvent-expected.txt: * platform/ios/imported/w3c/web-platform-tests/dom/events/EventTarget-dispatchEvent-expected.txt: git-svn-id: http://svn.webkit.org/repository/webkit/trunk@236002 268f45cc-cd09-0410-ab3c-d52691b4dbfc
- Loading branch information
rniwa@webkit.org
committed
Sep 14, 2018
1 parent
d3a818d
commit 83a6595
Showing
22 changed files
with
373 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
...s/fast/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
|
||
PASS Capturing event listeners should be invoked before bubbling event listeners on the target without shadow trees | ||
PASS Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree | ||
PASS Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a doubly nested shadow tree | ||
PASS Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched via a slot | ||
PASS Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree which passes through another shadow tree | ||
|
191 changes: 191 additions & 0 deletions
191
LayoutTests/fast/shadow-dom/capturing-and-bubbling-event-listeners-across-shadow-trees.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>Shadow DOM: Capturing event listeners should be invoked before bubbling event listeners</title> | ||
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> | ||
<script src="../../resources/testharness.js"></script> | ||
<script src="../../resources/testharnessreport.js"></script> | ||
<script src="../../imported/w3c/web-platform-tests/shadow-dom/resources/shadow-dom.js"></script> | ||
<script> | ||
|
||
function attachEventListeners(eventType, tree) { | ||
const eventLogs = []; | ||
const makeComposedPathResult = (event) => event.composedPath().map((node) => node.id) | ||
for (const id in tree) { | ||
const node = tree[id]; | ||
node.addEventListener(eventType, event => eventLogs.push( | ||
['bubbling', event.eventPhase, event.target.id, event.currentTarget.id, makeComposedPathResult(event)]), {capture: false}); | ||
node.addEventListener(eventType, event => eventLogs.push( | ||
['capturing', event.eventPhase, event.target.id, event.currentTarget.id, makeComposedPathResult(event)]), {capture: true}); | ||
} | ||
return eventLogs; | ||
} | ||
|
||
</script> | ||
</head> | ||
<body> | ||
|
||
<div id="test1"> | ||
<div id="parent"> | ||
<div id="target"></div> | ||
</div> | ||
</div> | ||
<script> | ||
test(() => { | ||
const tree = createTestTree(document.getElementById('test1')); | ||
const logs = attachEventListeners('my-event', tree); | ||
tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); | ||
|
||
const composedPath = ['target', 'parent', 'test1']; | ||
assert_object_equals(logs, [ | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'test1', composedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'parent', composedPath], | ||
['capturing', Event.AT_TARGET, 'target', 'target', composedPath], | ||
['bubbling', Event.AT_TARGET, 'target', 'target', composedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'parent', composedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'test1', composedPath], | ||
]); | ||
}, 'Capturing event listeners should be invoked before bubbling event listeners on the target without shadow trees'); | ||
</script> | ||
|
||
<div id="test2"> | ||
<div id="host"> | ||
<template id="shadowRoot" data-mode="closed"> | ||
<div id="target"></div> | ||
</template> | ||
</div> | ||
</div> | ||
<script> | ||
test(() => { | ||
const tree = createTestTree(document.getElementById('test2')); | ||
const logs = attachEventListeners('my-event', tree); | ||
tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); | ||
|
||
const innerComposedPath = ['target', 'shadowRoot', 'host', 'test2']; | ||
const outerComposedPath = ['host', 'test2']; | ||
assert_object_equals(logs, [ | ||
['capturing', Event.CAPTURING_PHASE, 'host', 'test2', outerComposedPath], | ||
['capturing', Event.AT_TARGET, 'host', 'host', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'shadowRoot', innerComposedPath], | ||
['capturing', Event.AT_TARGET, 'target', 'target', innerComposedPath], | ||
['bubbling', Event.AT_TARGET, 'target', 'target', innerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'shadowRoot', innerComposedPath], | ||
['bubbling', Event.AT_TARGET, 'host', 'host', outerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'host', 'test2', outerComposedPath], | ||
]); | ||
}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree'); | ||
</script> | ||
|
||
<div id="test3"> | ||
<div id="outerHost"> | ||
<template id="outerShadowRoot" data-mode="closed"> | ||
<div id="innerHost"> | ||
<template id="innerShadowRoot" data-mode="closed"> | ||
<div id="target"></div> | ||
</template> | ||
</div> | ||
</template> | ||
</div> | ||
</div> | ||
<script> | ||
test(() => { | ||
const tree = createTestTree(document.getElementById('test3')); | ||
const logs = attachEventListeners('my-event', tree); | ||
tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); | ||
|
||
const innerShadowComposedPath = ['target', 'innerShadowRoot', 'innerHost', 'outerShadowRoot', 'outerHost', 'test3']; | ||
const outerShadowComposedPath = ['innerHost', 'outerShadowRoot', 'outerHost', 'test3']; | ||
const outerComposedPath = ['outerHost', 'test3']; | ||
assert_object_equals(logs, [ | ||
['capturing', Event.CAPTURING_PHASE, 'outerHost', 'test3', outerComposedPath], | ||
['capturing', Event.AT_TARGET, 'outerHost', 'outerHost', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'innerHost', 'outerShadowRoot', outerShadowComposedPath], | ||
['capturing', Event.AT_TARGET, 'innerHost', 'innerHost', outerShadowComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'innerShadowRoot', innerShadowComposedPath], | ||
['capturing', Event.AT_TARGET, 'target', 'target', innerShadowComposedPath], | ||
|
||
['bubbling', Event.AT_TARGET, 'target', 'target', innerShadowComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'innerShadowRoot', innerShadowComposedPath], | ||
['bubbling', Event.AT_TARGET, 'innerHost', 'innerHost', outerShadowComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'innerHost', 'outerShadowRoot', outerShadowComposedPath], | ||
['bubbling', Event.AT_TARGET, 'outerHost', 'outerHost', outerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'outerHost', 'test3', outerComposedPath], | ||
]); | ||
}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a doubly nested shadow tree'); | ||
</script> | ||
|
||
<div id="test4"> | ||
<div id="host"> | ||
<template id="shadowRoot" data-mode="closed"> | ||
<slot id="slot"></slot> | ||
</template> | ||
<div id="target"></div> | ||
</div> | ||
</div> | ||
<script> | ||
test(() => { | ||
const tree = createTestTree(document.getElementById('test4')); | ||
const logs = attachEventListeners('my-event', tree); | ||
tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); | ||
|
||
const innerComposedPath = ['target', 'slot', 'shadowRoot', 'host', 'test4']; | ||
const outerComposedPath = ['target', 'host', 'test4']; | ||
assert_object_equals(logs, [ | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'test4', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'host', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'shadowRoot', innerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'slot', innerComposedPath], | ||
['capturing', Event.AT_TARGET, 'target', 'target', outerComposedPath], | ||
|
||
['bubbling', Event.AT_TARGET, 'target', 'target', outerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'slot', innerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'shadowRoot', innerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'host', outerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'test4', outerComposedPath], | ||
]); | ||
}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched via a slot'); | ||
</script> | ||
|
||
<div id="test5"> | ||
<div id="upperHost"> | ||
<template id="upperShadowRoot" data-mode="closed"> | ||
<slot id="upperSlot"></slot> | ||
</template> | ||
<div id="lowerHost"> | ||
<template id="lowerShadowRoot" data-mode="closed"> | ||
<div id="target"></div> | ||
</template> | ||
</div> | ||
</div> | ||
</div> | ||
<script> | ||
test(() => { | ||
const tree = createTestTree(document.getElementById('test5')); | ||
const logs = attachEventListeners('my-event', tree); | ||
tree.target.dispatchEvent(new Event('my-event', { bubbles: true, composed: true })); | ||
|
||
const lowerComposedPath = ['target', 'lowerShadowRoot', 'lowerHost', 'upperHost', 'test5']; | ||
const upperComposedPath = ['lowerHost', 'upperSlot', 'upperShadowRoot', 'upperHost', 'test5']; | ||
const outerComposedPath = ['lowerHost', 'upperHost', 'test5']; | ||
assert_object_equals(logs, [ | ||
['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'test5', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'upperHost', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'upperShadowRoot', upperComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'lowerHost', 'upperSlot', upperComposedPath], | ||
['capturing', Event.AT_TARGET, 'lowerHost', 'lowerHost', outerComposedPath], | ||
['capturing', Event.CAPTURING_PHASE, 'target', 'lowerShadowRoot', lowerComposedPath], | ||
['capturing', Event.AT_TARGET, 'target', 'target', lowerComposedPath], | ||
|
||
['bubbling', Event.AT_TARGET, 'target', 'target', lowerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'target', 'lowerShadowRoot', lowerComposedPath], | ||
['bubbling', Event.AT_TARGET, 'lowerHost', 'lowerHost', outerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'upperSlot', upperComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'upperShadowRoot', upperComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'upperHost', outerComposedPath], | ||
['bubbling', Event.BUBBLING_PHASE, 'lowerHost', 'test5', outerComposedPath], | ||
]); | ||
}, 'Capturing event listeners should be invoked before bubbling event listeners when an event is dispatched inside a shadow tree which passes through another shadow tree'); | ||
</script> | ||
|
||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...s/imported/w3c/web-platform-tests/dom/events/Event-dispatch-handlers-changed-expected.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
|
||
PASS Dispatch additional events inside an event listener | ||
FAIL Dispatch additional events inside an event listener assert_array_equals: actual_targets lengths differ, expected 16 got 17 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.