зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1875424 - Make `EventStateManager` dispatch `mouseleave` events on ancestors even after the last `mouseover` target is removed r=smaug
Currently, `EventStateManager` clears the last `mouseover` element when it's removed from the tree. Therefore, it does not fire `mouseleave` events when the cursor moves outside of ancestors which a `mouseenter` event is fired on. This patch makes `EventStateManager` keep storing the parent of removing content node which is an inclusive ancestor of the last `mouseover` target. The following patch will make the code easier to read. Differential Revision: https://phabricator.services.mozilla.com/D199198
This commit is contained in:
Родитель
6e0ae191ee
Коммит
d244813465
|
@ -4846,14 +4846,19 @@ void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent,
|
|||
aMovingInto, aMouseEvent,
|
||||
isPointer ? ePointerLeave : eMouseLeave);
|
||||
|
||||
// Fire mouseout
|
||||
nsCOMPtr<nsIContent> lastOverElement = wrapper->mLastOverElement;
|
||||
DispatchMouseOrPointerEvent(aMouseEvent, isPointer ? ePointerOut : eMouseOut,
|
||||
lastOverElement, aMovingInto);
|
||||
// Fire mouseout/pointerout only if the last mouseover/pointerover element has
|
||||
// not been removed yet.
|
||||
if (!wrapper->mLastOverElementRemoved) {
|
||||
nsCOMPtr<nsIContent> lastOverElement = wrapper->mLastOverElement;
|
||||
DispatchMouseOrPointerEvent(aMouseEvent,
|
||||
isPointer ? ePointerOut : eMouseOut,
|
||||
lastOverElement, aMovingInto);
|
||||
}
|
||||
leaveDispatcher.Dispatch();
|
||||
|
||||
wrapper->mLastOverFrame = nullptr;
|
||||
wrapper->mLastOverElement = nullptr;
|
||||
wrapper->mLastOverElementRemoved = false;
|
||||
|
||||
// Turn recursion protection back off
|
||||
wrapper->mFirstOutEventElement = nullptr;
|
||||
|
@ -4877,7 +4882,10 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent,
|
|||
|
||||
RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent);
|
||||
|
||||
if (!wrapper || wrapper->mLastOverElement == aContent) return;
|
||||
if (!wrapper || (!wrapper->mLastOverElementRemoved &&
|
||||
wrapper->mLastOverElement == aContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before firing mouseover, check for recursion
|
||||
if (aContent == wrapper->mFirstOverEventElement) return;
|
||||
|
@ -4897,7 +4905,10 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent,
|
|||
}
|
||||
// Firing the DOM event in the parent document could cause all kinds
|
||||
// of havoc. Reverify and take care.
|
||||
if (wrapper->mLastOverElement == aContent) return;
|
||||
if (!wrapper->mLastOverElementRemoved &&
|
||||
wrapper->mLastOverElement == aContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remember mLastOverElement as the related content for the
|
||||
// DispatchMouseOrPointerEvent() call below, since NotifyMouseOut() resets it,
|
||||
|
@ -4919,13 +4930,14 @@ void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent,
|
|||
// Store the first mouseOver event we fire and don't refire mouseOver
|
||||
// to that element while the first mouseOver is still ongoing.
|
||||
wrapper->mFirstOverEventElement = aContent;
|
||||
wrapper->mLastOverElement = aContent;
|
||||
wrapper->mLastOverElementRemoved = false;
|
||||
|
||||
// Fire mouseover
|
||||
wrapper->mLastOverFrame = DispatchMouseOrPointerEvent(
|
||||
aMouseEvent, isPointer ? ePointerOver : eMouseOver, aContent,
|
||||
lastOverElement);
|
||||
enterDispatcher.Dispatch();
|
||||
wrapper->mLastOverElement = aContent;
|
||||
|
||||
// Turn recursion protection back off
|
||||
wrapper->mFirstOverEventElement = nullptr;
|
||||
|
@ -5970,16 +5982,6 @@ bool EventStateManager::SetContentState(nsIContent* aContent,
|
|||
return true;
|
||||
}
|
||||
|
||||
void EventStateManager::ResetLastOverForContent(
|
||||
const uint32_t& aIdx, const RefPtr<OverOutElementsWrapper>& aElemWrapper,
|
||||
nsIContent* aContent) {
|
||||
if (aElemWrapper && aElemWrapper->mLastOverElement &&
|
||||
nsContentUtils::ContentIsFlattenedTreeDescendantOf(
|
||||
aElemWrapper->mLastOverElement, aContent)) {
|
||||
aElemWrapper->mLastOverElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void EventStateManager::RemoveNodeFromChainIfNeeded(ElementState aState,
|
||||
nsIContent* aContentRemoved,
|
||||
bool aNotify) {
|
||||
|
@ -6088,10 +6090,13 @@ void EventStateManager::ContentRemoved(Document* aDocument,
|
|||
|
||||
PointerEventHandler::ReleaseIfCaptureByDescendant(aContent);
|
||||
|
||||
// See bug 292146 for why we want to null this out
|
||||
ResetLastOverForContent(0, mMouseEnterLeaveHelper, aContent);
|
||||
if (mMouseEnterLeaveHelper) {
|
||||
mMouseEnterLeaveHelper->ContentRemoved(*aContent);
|
||||
}
|
||||
for (const auto& entry : mPointersEnterLeaveHelper) {
|
||||
ResetLastOverForContent(entry.GetKey(), entry.GetData(), aContent);
|
||||
if (entry.GetData()) {
|
||||
entry.GetData()->ContentRemoved(*aContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6875,4 +6880,33 @@ bool EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedY(
|
|||
MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL;
|
||||
}
|
||||
|
||||
void OverOutElementsWrapper::ContentRemoved(nsIContent& aContent) {
|
||||
if (!mLastOverElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nsContentUtils::ContentIsFlattenedTreeDescendantOf(mLastOverElement,
|
||||
&aContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFirstOverEventElement &&
|
||||
(mLastOverElement == mFirstOverEventElement ||
|
||||
nsContentUtils::ContentIsFlattenedTreeDescendantOf(
|
||||
mFirstOverEventElement, &aContent))) {
|
||||
if (mFirstOverEventElement == mFirstOutEventElement) {
|
||||
mFirstOutEventElement = nullptr;
|
||||
}
|
||||
mFirstOverEventElement = nullptr;
|
||||
}
|
||||
if (mFirstOutEventElement &&
|
||||
(mLastOverElement == mFirstOutEventElement ||
|
||||
nsContentUtils::ContentIsFlattenedTreeDescendantOf(mFirstOutEventElement,
|
||||
&aContent))) {
|
||||
mFirstOutEventElement = nullptr;
|
||||
}
|
||||
mLastOverElement = aContent.GetFlattenedTreeParent();
|
||||
mLastOverElementRemoved = true;
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -64,6 +64,8 @@ class OverOutElementsWrapper final : public nsISupports {
|
|||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS(OverOutElementsWrapper)
|
||||
|
||||
void ContentRemoved(nsIContent& aContent);
|
||||
|
||||
WeakFrame mLastOverFrame;
|
||||
|
||||
nsCOMPtr<nsIContent> mLastOverElement;
|
||||
|
@ -75,6 +77,11 @@ class OverOutElementsWrapper final : public nsISupports {
|
|||
// The last element on which we fired a out event, or null if
|
||||
// the last out event we fired has finished processing.
|
||||
nsCOMPtr<nsIContent> mFirstOutEventElement;
|
||||
|
||||
// Once the first mouseover element is removed from the tree, this is set
|
||||
// to true. Then, mLastOverElement may be an ancestor of the mouseover
|
||||
// element which should be the deepest target of next mouseleave element.
|
||||
bool mLastOverElementRemoved = false;
|
||||
};
|
||||
|
||||
class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
|
||||
|
@ -1127,9 +1134,6 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
|
|||
static void UpdateAncestorState(nsIContent* aStartNode,
|
||||
nsIContent* aStopBefore, ElementState aState,
|
||||
bool aAddState);
|
||||
static void ResetLastOverForContent(
|
||||
const uint32_t& aIdx, const RefPtr<OverOutElementsWrapper>& aChunk,
|
||||
nsIContent* aClosure);
|
||||
|
||||
/**
|
||||
* Update the attribute mLastRefPoint of the mouse event. It should be
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
[pointerevent_after_target_appended.html?mouse]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
||||
[pointer events from mouse received before/after child attached at pointerdown]
|
||||
expected: FAIL
|
||||
|
||||
[pointer events from mouse received before/after child moved at pointerdown]
|
||||
expected: FAIL
|
||||
|
||||
[pointer events from mouse received before/after child moved at pointerup]
|
||||
expected: FAIL
|
||||
|
||||
[mouse events from mouse received before/after child attached at mousedown]
|
||||
expected:
|
||||
if os == "android": FAIL
|
||||
|
||||
[mouse events from mouse received before/after child moved at mousedown]
|
||||
expected: FAIL
|
||||
|
||||
[mouse events from mouse received before/after child moved at mouseup]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[pointerevent_after_target_appended.html?pen]
|
||||
[pointer events from pen received before/after child attached at pointerdown]
|
||||
|
|
|
@ -27,18 +27,10 @@
|
|||
|
||||
|
||||
[pointerevent_after_target_removed.html?mouse]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
||||
[pointer events from mouse received before/after child removal at pointerdown]
|
||||
expected: FAIL
|
||||
|
||||
[pointer events from mouse received before/after child removal at pointerup]
|
||||
expected: FAIL
|
||||
|
||||
[mouse events from mouse received before/after child removal at mousedown]
|
||||
expected: FAIL
|
||||
|
||||
[mouse events from mouse received before/after child removal at mouseup]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[pointerevent_after_target_removed.html?touch]
|
||||
expected: TIMEOUT
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
[pointerevent_pointer_boundary_events_after_removing_last_over_element.html]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
|
@ -0,0 +1,2 @@
|
|||
[mouse_boundary_events_after_removing_last_over_element.html]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
|
@ -4,15 +4,9 @@
|
|||
[Removing an element at mousedown: mouseout and mouseleave should've been fired on the removed child]
|
||||
expected: FAIL
|
||||
|
||||
[Removing an element at mousedown: mouseenter should not have been fired on the parent]
|
||||
expected: FAIL
|
||||
|
||||
[Removing an element at mouseup: mouseout and mouseleave should've been fired on the removed child]
|
||||
expected: FAIL
|
||||
|
||||
[Removing an element at mouseup: mouseenter should not have been fired on the parent]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[synthetic-mouse-enter-leave-over-out-button-state-after-target-removed.tentative.html?buttonType=LEFT&button=0&buttons=1]
|
||||
expected:
|
||||
|
@ -21,11 +15,5 @@
|
|||
[Removing an element at mousedown: mouseout and mouseleave should've been fired on the removed child]
|
||||
expected: FAIL
|
||||
|
||||
[Removing an element at mousedown: mouseenter should not have been fired on the parent]
|
||||
expected: FAIL
|
||||
|
||||
[Removing an element at mouseup: mouseout and mouseleave should've been fired on the removed child]
|
||||
expected: FAIL
|
||||
|
||||
[Removing an element at mouseup: mouseenter should not have been fired on the parent]
|
||||
expected: FAIL
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redundant "pointerenter" shouldn't be fired without "pointerleave"s</title>
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
<script src=/resources/testdriver.js></script>
|
||||
<script src=/resources/testdriver-actions.js></script>
|
||||
<script src=/resources/testdriver-vendor.js></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
function stringifyEvents(eventArray) {
|
||||
if (!eventArray.length) {
|
||||
return "[]";
|
||||
}
|
||||
let result = "";
|
||||
eventArray.forEach(event => {
|
||||
if (result != "") {
|
||||
result += ", ";
|
||||
}
|
||||
result += `${event.type}@${
|
||||
event.target?.nodeType == Node.ELEMENT_NODE
|
||||
? `${event.target.localName}${
|
||||
event.target.id ? `#${event.target.id}` : ""
|
||||
}`
|
||||
: event.target?.localName
|
||||
}`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function eventsAfterClick(eventArray) {
|
||||
const indexAtClick = eventArray.findIndex(e => e.type == "click");
|
||||
if (indexAtClick >= 0) {
|
||||
return eventArray.slice(indexAtClick + 1);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
addEventListener("load", () => {
|
||||
promise_test(async () => {
|
||||
const div1 = document.createElement("div");
|
||||
div1.setAttribute("id", "grandparent");
|
||||
div1.setAttribute("style", "width: 32px; height: 32px");
|
||||
const div2 = document.createElement("div");
|
||||
div2.setAttribute("id", "parent");
|
||||
div2.setAttribute("style", "width: 32px; height: 32px");
|
||||
const div3 = document.createElement("div");
|
||||
div3.setAttribute("id", "child");
|
||||
div3.setAttribute("style", "width: 32px; height: 32px");
|
||||
div1.appendChild(div2);
|
||||
div2.appendChild(div3);
|
||||
document.body.appendChild(div1);
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const div3Rect = div3.getBoundingClientRect();
|
||||
let events = [];
|
||||
for (const type of ["pointerenter", "pointerleave", "pointerover", "pointerout", "pointermove"]) {
|
||||
for (const node of [document.body, div1, div2, div3]) {
|
||||
node.addEventListener(type, event => {
|
||||
if (event.target == node) {
|
||||
events.push({type: event.type, target: event.target});
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
}
|
||||
div3.addEventListener("click", event => {
|
||||
div3.remove();
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.pointerDown()
|
||||
.pointerUp() // The clicked in the child, then it's removed from the DOM tree
|
||||
.pointerMove(bodyRect.x + 10, bodyRect.y + 10, {}) // Then, move onto the <body>
|
||||
.send();
|
||||
// FYI: Comparing `pointerenter`s before `click` requires additional
|
||||
// initialization, but it's out of scope of this bug. Therefore, we
|
||||
// compare only events after `click`.
|
||||
const expectedEvents = [ // no events should be fired on the child due to disconnected
|
||||
{ type: "pointerleave", target: div2}, // no pointerout because of first pointer move after the mutation
|
||||
{ type: "pointerleave", target: div1},
|
||||
{ type: "pointerover", target: document.body},
|
||||
{ type: "pointermove", target: document.body},
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(eventsAfterClick(events)),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
}, "After removing the last over element, redundant pointerenter events should not be fired on the ancestors");
|
||||
|
||||
promise_test(async () => {
|
||||
const hostContainer = document.createElement("div");
|
||||
hostContainer.setAttribute("id", "containerOfShadowHost");
|
||||
hostContainer.setAttribute("style", "margin-top: 32px; height: 32px");
|
||||
const host = document.createElement("div");
|
||||
host.setAttribute("id", "shadowHost");
|
||||
host.setAttribute("style", "width: 32px; height: 32px");
|
||||
const root = host.attachShadow({mode: "open"});
|
||||
const rootElementInShadow = document.createElement("div");
|
||||
root.appendChild(rootElementInShadow);
|
||||
rootElementInShadow.setAttribute("id", "divInShadow");
|
||||
rootElementInShadow.setAttribute("style", "width: 32px; height: 32px");
|
||||
hostContainer.appendChild(host);
|
||||
document.body.appendChild(hostContainer);
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const rootElementInShadowRect = rootElementInShadow.getBoundingClientRect();
|
||||
let events = [];
|
||||
for (const type of ["pointerenter", "pointerleave", "pointerover", "pointerout", "pointermove"]) {
|
||||
for (const node of [document.body, hostContainer, host, root, rootElementInShadow]) {
|
||||
node.addEventListener(type, event => {
|
||||
if (event.target == node) {
|
||||
events.push({type: event.type, target: event.target});
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
}
|
||||
rootElementInShadow.addEventListener("click", event => {
|
||||
rootElementInShadow.remove();
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(rootElementInShadowRect.x + 10, rootElementInShadowRect.y + 10, {})
|
||||
.pointerDown()
|
||||
.pointerUp() // The clicked root element in the shadow is removed here.
|
||||
.pointerMove(bodyRect.x + 10, bodyRect.y + 10, {}) // Then, move onto the <body>
|
||||
.send();
|
||||
// FYI: Comparing `pointerenter`s before `click` requires additional
|
||||
// initialization, but it's out of scope of this bug. Therefore, we
|
||||
// compare only events after `click`.
|
||||
const expectedEvents = [ // no events should be fired on rootElementInShadow due to disconnected
|
||||
{ type: "pointerleave", target: host}, // no pointerout because of first pointer move after the mutation
|
||||
{ type: "pointerleave", target: hostContainer},
|
||||
{ type: "pointerover", target: document.body},
|
||||
{ type: "pointermove", target: document.body},
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(eventsAfterClick(events)),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
hostContainer.remove();
|
||||
}, "After removing the root element in the shadow under the cursor, pointerleave events should be targeted outside the shadow, but redundant pointerenter events should not be fired");
|
||||
}, {once: true});
|
||||
</script>
|
||||
</head>
|
||||
<body style="padding-top: 32px"></body>
|
||||
</html>
|
|
@ -0,0 +1,153 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Redundant "mouseenter" shouldn't be fired without "mouseleave"s</title>
|
||||
<script src=/resources/testharness.js></script>
|
||||
<script src=/resources/testharnessreport.js></script>
|
||||
<script src=/resources/testdriver.js></script>
|
||||
<script src=/resources/testdriver-actions.js></script>
|
||||
<script src=/resources/testdriver-vendor.js></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
function stringifyEvents(eventArray) {
|
||||
if (!eventArray.length) {
|
||||
return "[]";
|
||||
}
|
||||
let result = "";
|
||||
eventArray.forEach(event => {
|
||||
if (result != "") {
|
||||
result += ", ";
|
||||
}
|
||||
result += `${event.type}@${
|
||||
event.target?.nodeType == Node.ELEMENT_NODE
|
||||
? `${event.target.localName}${
|
||||
event.target.id ? `#${event.target.id}` : ""
|
||||
}`
|
||||
: event.target?.localName
|
||||
}`;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function eventsAfterClick(eventArray) {
|
||||
const indexAtClick = eventArray.findIndex(e => e.type == "click");
|
||||
if (indexAtClick >= 0) {
|
||||
return eventArray.slice(indexAtClick + 1);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
addEventListener("load", () => {
|
||||
promise_test(async () => {
|
||||
const div1 = document.createElement("div");
|
||||
div1.setAttribute("id", "grandparent");
|
||||
div1.setAttribute("style", "width: 32px; height: 32px");
|
||||
const div2 = document.createElement("div");
|
||||
div2.setAttribute("id", "parent");
|
||||
div2.setAttribute("style", "width: 32px; height: 32px");
|
||||
const div3 = document.createElement("div");
|
||||
div3.setAttribute("id", "child");
|
||||
div3.setAttribute("style", "width: 32px; height: 32px");
|
||||
div1.appendChild(div2);
|
||||
div2.appendChild(div3);
|
||||
document.body.appendChild(div1);
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const div3Rect = div3.getBoundingClientRect();
|
||||
let events = [];
|
||||
for (const type of ["mouseenter", "mouseleave", "mouseover", "mouseout", "mousemove"]) {
|
||||
for (const node of [document.body, div1, div2, div3]) {
|
||||
node.addEventListener(type, event => {
|
||||
if (event.target == node) {
|
||||
events.push({type: event.type, target: event.target});
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
}
|
||||
div3.addEventListener("click", event => {
|
||||
div3.remove();
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.pointerDown()
|
||||
.pointerUp() // The clicked in the child, then it's removed from the DOM tree
|
||||
.pointerMove(bodyRect.x + 10, bodyRect.y + 10, {}) // Then, move onto the <body>
|
||||
.send();
|
||||
// FYI: Comparing `mouseenter`s before `click` requires additional
|
||||
// initialization, but it's out of scope of this bug. Therefore, we
|
||||
// compare only events after `click`.
|
||||
const expectedEvents = [ // no events should be fired on the child due to disconnected
|
||||
{ type: "mouseover", target: div2 }, // mouseover should be fired because of the mutation
|
||||
{ type: "mouseout", target: div2}, // mouseout should be fired because of the mutation
|
||||
{ type: "mouseleave", target: div2},
|
||||
{ type: "mouseleave", target: div1},
|
||||
{ type: "mouseover", target: document.body},
|
||||
{ type: "mousemove", target: document.body},
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(eventsAfterClick(events)),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
}, "After removing the last over element, redundant mouseenter events should not be fired on the ancestors");
|
||||
|
||||
promise_test(async () => {
|
||||
const hostContainer = document.createElement("div");
|
||||
hostContainer.setAttribute("id", "containerOfShadowHost");
|
||||
hostContainer.setAttribute("style", "margin-top: 32px; height: 32px");
|
||||
const host = document.createElement("div");
|
||||
host.setAttribute("id", "shadowHost");
|
||||
host.setAttribute("style", "width: 32px; height: 32px");
|
||||
const root = host.attachShadow({mode: "open"});
|
||||
const rootElementInShadow = document.createElement("div");
|
||||
root.appendChild(rootElementInShadow);
|
||||
rootElementInShadow.setAttribute("id", "divInShadow");
|
||||
rootElementInShadow.setAttribute("style", "width: 32px; height: 32px");
|
||||
hostContainer.appendChild(host);
|
||||
document.body.appendChild(hostContainer);
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const rootElementInShadowRect = rootElementInShadow.getBoundingClientRect();
|
||||
let events = [];
|
||||
for (const type of ["mouseenter", "mouseleave", "mouseover", "mouseout", "mousemove"]) {
|
||||
for (const node of [document.body, hostContainer, host, root, rootElementInShadow]) {
|
||||
node.addEventListener(type, event => {
|
||||
if (event.target == node) {
|
||||
events.push({type: event.type, target: event.target});
|
||||
}
|
||||
}, {capture: true});
|
||||
}
|
||||
}
|
||||
rootElementInShadow.addEventListener("click", event => {
|
||||
rootElementInShadow.remove();
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(rootElementInShadowRect.x + 10, rootElementInShadowRect.y + 10, {})
|
||||
.pointerDown()
|
||||
.pointerUp() // The clicked root element in the shadow is removed here.
|
||||
.pointerMove(bodyRect.x + 10, bodyRect.y + 10, {}) // Then, move onto the <body>
|
||||
.send();
|
||||
// FYI: Comparing `mouseenter`s before `click` requires additional
|
||||
// initialization, but it's out of scope of this bug. Therefore, we
|
||||
// compare only events after `click`.
|
||||
const expectedEvents = [ // no events should be fired on rootElementInShadow due to disconnected
|
||||
{ type: "mouseover", target: host}, // mouseover should be fired because of the mutation
|
||||
{ type: "mouseout", target: host}, // mouseout should be fired because of the mutation
|
||||
{ type: "mouseleave", target: host},
|
||||
{ type: "mouseleave", target: hostContainer},
|
||||
{ type: "mouseover", target: document.body},
|
||||
{ type: "mousemove", target: document.body},
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(eventsAfterClick(events)),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
hostContainer.remove();
|
||||
}, "After removing the root element in the shadow under the cursor, mouseleave events should be targeted outside the shadow, but redundant mouseenter events should not be fired");
|
||||
}, {once: true});
|
||||
</script>
|
||||
</head>
|
||||
<body style="padding-top: 32px"></body>
|
||||
</html>
|
Загрузка…
Ссылка в новой задаче