forked from mirrors/gecko-dev
Bug 1888018 - part 3: Make OverOutElementsWrapper restore original mouseover target if it's removed temporarily r=smaug
Chromium stores the last "over" event target before dispatching the boundary events [1][2]. However, Chrome and Safari do not fire mouse boundary events when the last over target is removed temporarily. Therefore, I think that they consider whether the target is removed not immediately after removing the node. Currently, we consider whether we should dispatch mouse boundary events for the layout/DOM changes with next mouse event or next synthesized `eMouseMove` which is enqueued when the last `mouseover` target is removed [3] (i.e., it's considered at latest next animation frame after removing the target). Therefore, we can restore the original `mouseover` target if it's reconnected to the DOM again before that. Although the timing of considering that could be incompatible with the other browsers in strictly speaking, but I think that this is enough because the new tests pass on Firefox and Chrome and I don't have any ideas about more time-sensitive test cases. About `pointerover`, we don't need to change the behavior because it's well defined by the Pointer Events spec and the behavior is compatible between browsers. Note that the new test, `mouse_boundary_events_after_reappending_last_over_target.tentative.html` is not compatible with the expectation of mouse boundary events in `pointerevent_after_target_appended.html` which is in the scope of Interop-2024. Therefore, there are some new failures of its result (note that the tests were not passed before D202907 (D202376 is adding the new behavior and it's enabled by D202907). The new failure results are, we stop dispatching `mouseover` and `mouseenter` after `mousedown` or `mouseup` whose event listener moves its event target because it's reconnected to different position in same parent immediately and element under the cursor is not changed by the temporary removal. Safari behaves same as our new behavior [4][5] and this is consistent with the result of new tests. Therefore, I think that Chrome should fix it, so I'll file a new spec issue about this to discuss what's the best. (Chrome passes most tests in `mouse_boundary_events_after_reappending_last_over_target.tentative.html`. They fail the cases moving the last `mouseover` event target outside the deepest connected `mouseenter` event target but it stays under the cursor because they don't dispatch mouse boundary events and fails. This means that they won't dispatch `mouseleave` event on the ex-parent of the target. And also they fail the cases moving the `mouseover` event target outside the deepest connected `mouseenter` event target and changing the element under the cursor because they dispatch `mouseout` and `mouseleave` on the moved target. Finally, they fail the case re-appending the target in a next animation after removal because they dispatch redundant `mouseenter` events on all ancestors.) 1. https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/input/mouse_event_manager.cc;l=415;drc=5742c2e870cdd5e453212d92f13a854993cd60bf 2. https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/input/mouse_event_manager.cc;l=451;drc=5742c2e870cdd5e453212d92f13a854993cd60bf 3. https://searchfox.org/mozilla-central/rev/294e1fbdcc9ca0c328c372392e03bb49df4ee77e/dom/events/EventStateManager.cpp#6273-6276 4. https://wpt.fyi/results/pointerevents/pointerevent_after_target_appended.html%3Fmouse?run_id=6221043298992128&run_id=5090975864586240&run_id=5106342989135872&run_id=5101843876675584 5. https://wpt.fyi/results/pointerevents/pointerevent_after_target_appended.html%3Ftouch?run_id=6221043298992128&run_id=5090975864586240&run_id=5106342989135872&run_id=5101843876675584 Differential Revision: https://phabricator.services.mozilla.com/D205536
This commit is contained in:
parent
e4408d8bd5
commit
f3b05ead87
7 changed files with 906 additions and 14 deletions
|
|
@ -364,6 +364,77 @@ void OverOutElementsWrapper::ContentRemoved(nsIContent& aContent) {
|
|||
UpdateDeepestEnterEventTarget(aContent.GetFlattenedTreeParent());
|
||||
}
|
||||
|
||||
void OverOutElementsWrapper::TryToRestorePendingRemovedOverTarget(
|
||||
const WidgetEvent* aEvent) {
|
||||
if (!MaybeHasPendingRemovingOverEventTarget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LogModule* const logModule = mType == BoundaryEventType::Mouse
|
||||
? sMouseBoundaryLog
|
||||
: sPointerBoundaryLog;
|
||||
|
||||
// If we receive a mouse event immediately, let's try to restore the last
|
||||
// "over" event target as the following "out" event target. We assume that a
|
||||
// synthesized mousemove or another mouse event is being dispatched at latest
|
||||
// the next animation frame from the removal. However, synthesized mouse move
|
||||
// which is enqueued by ContentRemoved() may not sent to this instance because
|
||||
// the target is considered with the latest layout, so the document of this
|
||||
// instance may be moved somewhere before the next animation frame.
|
||||
// Therefore, we should not restore the last "over" target if we receive an
|
||||
// unexpected event like a keyboard event, a wheel event, etc.
|
||||
if (aEvent->AsMouseEvent()) {
|
||||
// Restore the original "over" event target should be allowed only when it's
|
||||
// reconnected under the last deepest "enter" event target because we need
|
||||
// to dispatch "leave" events later at least on the ancestors which have
|
||||
// never been removed from the tree.
|
||||
// XXX If new ancestor is inserted between mDeepestEnterEventTarget and
|
||||
// mPendingToRemoveLastOverEventTarget, we will dispatch "leave" event even
|
||||
// though we have not dispatched "enter" event on the element. For fixing
|
||||
// this, we need to store the full path of the last "out" event target when
|
||||
// it's removed from the tree. I guess we can be relax for this issue
|
||||
// because this hack is required for web apps which reconnect the target
|
||||
// to the same position immediately.
|
||||
// XXX Should be IsInclusiveFlatTreeDescendantOf()? However, it may
|
||||
// be reconnected into a subtree which is different from where the
|
||||
// last over element was.
|
||||
nsCOMPtr<nsIContent> pendingRemovingOverEventTarget =
|
||||
GetPendingRemovingOverEventTarget();
|
||||
if (pendingRemovingOverEventTarget &&
|
||||
pendingRemovingOverEventTarget->IsInclusiveDescendantOf(
|
||||
mDeepestEnterEventTarget)) {
|
||||
// StoreOverEventTargetAndDeepestEnterEventTarget() always resets
|
||||
// mLastOverWidget. When we restore the pending removing "over" event
|
||||
// target, we need to keep storing the original "over" widget too.
|
||||
nsCOMPtr<nsIWeakReference> widget = std::move(mLastOverWidget);
|
||||
StoreOverEventTargetAndDeepestEnterEventTarget(
|
||||
pendingRemovingOverEventTarget);
|
||||
mLastOverWidget = std::move(widget);
|
||||
MOZ_LOG(logModule, LogLevel::Info,
|
||||
("The \"over\" event target (%p) is restored",
|
||||
mDeepestEnterEventTarget.get()));
|
||||
return;
|
||||
}
|
||||
MOZ_LOG(logModule, LogLevel::Debug,
|
||||
("Forgetting the last \"over\" event target (%p) because it is not "
|
||||
"reconnected under the deepest enter event target (%p)",
|
||||
mPendingRemovingOverEventTarget.get(),
|
||||
mDeepestEnterEventTarget.get()));
|
||||
} else {
|
||||
MOZ_LOG(logModule, LogLevel::Debug,
|
||||
("Forgetting the last \"over\" event target (%p) because an "
|
||||
"unexpected event (%s) is being dispatched, that means that "
|
||||
"EventStateManager didn't receive a synthesized mousemove which "
|
||||
"should be dispatched at next animation frame from the removal",
|
||||
mPendingRemovingOverEventTarget.get(), ToChar(aEvent->mMessage)));
|
||||
}
|
||||
|
||||
// Now, we should not restore mPendingRemovingOverEventTarget to
|
||||
// mDeepestEnterEventTarget anymore since mPendingRemovingOverEventTarget was
|
||||
// moved outside the subtree of mDeepestEnterEventTarget.
|
||||
mPendingRemovingOverEventTarget = nullptr;
|
||||
}
|
||||
|
||||
void OverOutElementsWrapper::WillDispatchOverAndEnterEvent(
|
||||
nsIContent* aOverEventTarget) {
|
||||
StoreOverEventTargetAndDeepestEnterEventTarget(aOverEventTarget);
|
||||
|
|
@ -424,23 +495,58 @@ void OverOutElementsWrapper::DidDispatchOverAndEnterEvent(
|
|||
void OverOutElementsWrapper::StoreOverEventTargetAndDeepestEnterEventTarget(
|
||||
nsIContent* aOverEventTargetAndDeepestEnterEventTarget) {
|
||||
mDeepestEnterEventTarget = aOverEventTargetAndDeepestEnterEventTarget;
|
||||
mDeepestEnterEventTargetIsOverEventTarget = true;
|
||||
mPendingRemovingOverEventTarget = nullptr;
|
||||
mDeepestEnterEventTargetIsOverEventTarget = !!mDeepestEnterEventTarget;
|
||||
mLastOverWidget = nullptr; // Set it after dispatching the "over" event.
|
||||
}
|
||||
|
||||
void OverOutElementsWrapper::UpdateDeepestEnterEventTarget(
|
||||
nsIContent* aDeepestEnterEventTarget) {
|
||||
if (MOZ_UNLIKELY(mDeepestEnterEventTarget == aDeepestEnterEventTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!aDeepestEnterEventTarget) {
|
||||
// If the root element is removed, we don't need to dispatch "leave"
|
||||
// events on any elements. Therefore, we can forget everything.
|
||||
StoreOverEventTargetAndDeepestEnterEventTarget(nullptr);
|
||||
} else {
|
||||
mDeepestEnterEventTarget = aDeepestEnterEventTarget;
|
||||
mDeepestEnterEventTargetIsOverEventTarget = false;
|
||||
// Do not update mLastOverWidget here because it's required to ignore some
|
||||
// following pointer events which are fired on widget under different top
|
||||
// level widget.
|
||||
return;
|
||||
}
|
||||
|
||||
if (LastOverEventTargetIsOutEventTarget()) {
|
||||
MOZ_ASSERT(mDeepestEnterEventTarget);
|
||||
if (mType == BoundaryEventType::Pointer) {
|
||||
// The spec of Pointer Events defines that once the `pointerover` event
|
||||
// target is removed from the tree, `pointerout` should not be fired on
|
||||
// that and the closest connected ancestor at the target removal should be
|
||||
// kept as the deepest `pointerleave` target. All browsers considers the
|
||||
// last `pointerover` event target is removed immediately when it occurs.
|
||||
// Therefore, we don't need the special handling which we do for the
|
||||
// `mouseout` event target below for considering whether we'll dispatch
|
||||
// `pointerout` on the last `pointerover` target.
|
||||
mPendingRemovingOverEventTarget = nullptr;
|
||||
} else {
|
||||
// Now, the `mouseout` event target is removed from the DOM at least
|
||||
// temporarily. Let's keep storing it for restoring it if it's
|
||||
// reconnected into mDeepestEnterEventTarget in a tick because the other
|
||||
// browsers do not treat temporary removal of the last `mouseover` target
|
||||
// keeps storing it as the next `mouseout` event target.
|
||||
MOZ_ASSERT(!mPendingRemovingOverEventTarget);
|
||||
MOZ_ASSERT(mDeepestEnterEventTarget);
|
||||
mPendingRemovingOverEventTarget =
|
||||
do_GetWeakReference(mDeepestEnterEventTarget);
|
||||
}
|
||||
} else {
|
||||
MOZ_ASSERT(!mDeepestEnterEventTargetIsOverEventTarget);
|
||||
// If mDeepestEnterEventTarget is not the last "over" event target, we've
|
||||
// already done the complicated state managing above. Therefore, we only
|
||||
// need to update mDeepestEnterEventTarget in this case.
|
||||
}
|
||||
mDeepestEnterEventTarget = aDeepestEnterEventTarget;
|
||||
mDeepestEnterEventTargetIsOverEventTarget = false;
|
||||
// Do not update mLastOverWidget here because it's required to ignore some
|
||||
// following pointer events which are fired on widget under different top
|
||||
// level widget.
|
||||
}
|
||||
|
||||
/******************************************************************/
|
||||
|
|
@ -858,6 +964,23 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
|
|||
}
|
||||
}
|
||||
|
||||
if (mMouseEnterLeaveHelper && aEvent->IsTrusted()) {
|
||||
// When the last `mouseover` event target is removed from the document,
|
||||
// we makes mMouseEnterLeaveHelper update the last deepest `mouseenter`
|
||||
// event target to the removed node parent and mark it as not the following
|
||||
// `mouseout` event target. However, the other browsers may dispatch
|
||||
// `mouseout` on it if it's restored "immediately". Therefore, we use
|
||||
// the next animation frame as the deadline. ContentRemoved() enqueues a
|
||||
// synthesized `mousemove` to dispatch mouse boundary events under the
|
||||
// mouse cursor soon and the synthesized event (or eMouseExitFromWidget if
|
||||
// our window is moved) will reach here at latest the next animation frame.
|
||||
// Therefore, we can use the event as the deadline. If the removed last
|
||||
// `mouseover` target is reconnected before a synthesized mouse event or
|
||||
// a real mouse event, let's restore it as the following `mouseout` event
|
||||
// target. Otherwise, e.g., a keyboard event, let's forget it.
|
||||
mMouseEnterLeaveHelper->TryToRestorePendingRemovedOverTarget(aEvent);
|
||||
}
|
||||
|
||||
switch (aEvent->mMessage) {
|
||||
case eContextMenu:
|
||||
if (PointerLockManager::IsLocked()) {
|
||||
|
|
@ -4313,6 +4436,7 @@ void EventStateManager::NotifyDestroyPresContext(nsPresContext* aPresContext) {
|
|||
// as if the new presentation is resized, a new element may be hovered.
|
||||
ResetHoverState();
|
||||
|
||||
mMouseEnterLeaveHelper = nullptr;
|
||||
mPointersEnterLeaveHelper.Clear();
|
||||
PointerEventHandler::NotifyDestroyPresContext(presContext);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,12 +118,42 @@ class OverOutElementsWrapper final : public nsISupports {
|
|||
: nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when EventStateManager::PreHandleEvent() receives an event which
|
||||
* should be treated as the deadline to restore the last "over" event target
|
||||
* as the next "out" event target and for avoiding to dispatch redundant
|
||||
* "over" event on the same target again when it was removed but reconnected.
|
||||
* If the last "over" event target was reconnected under the last deepest
|
||||
* "enter" event target, this restores the last "over" event target.
|
||||
* Otherwise, makes the instance forget the last "over" target because the
|
||||
* user maybe has seen that the last "over" target is completely removed from
|
||||
* the tree.
|
||||
*
|
||||
* @param aEvent The event which the caller received. If this is set to
|
||||
* nullptr or not a mouse event, this forgets the pending
|
||||
* last "over" event target.
|
||||
*/
|
||||
void TryToRestorePendingRemovedOverTarget(const WidgetEvent* aEvent);
|
||||
|
||||
/**
|
||||
* Return true if we have a pending removing last "over" event target at least
|
||||
* for the weak reference to it. In other words, when this returns true, we
|
||||
* need to handle the pending removing "over" event target.
|
||||
*/
|
||||
[[nodiscard]] bool MaybeHasPendingRemovingOverEventTarget() const {
|
||||
return mPendingRemovingOverEventTarget;
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* Whether the last "over" event target is the target of "out" event if you
|
||||
* dispatch "out" event.
|
||||
*/
|
||||
[[nodiscard]] bool LastOverEventTargetIsOutEventTarget() const {
|
||||
MOZ_ASSERT_IF(mDeepestEnterEventTargetIsOverEventTarget,
|
||||
mDeepestEnterEventTarget);
|
||||
MOZ_ASSERT_IF(mDeepestEnterEventTargetIsOverEventTarget,
|
||||
!MaybeHasPendingRemovingOverEventTarget());
|
||||
return mDeepestEnterEventTargetIsOverEventTarget;
|
||||
}
|
||||
|
||||
|
|
@ -131,12 +161,29 @@ class OverOutElementsWrapper final : public nsISupports {
|
|||
nsIContent* aOverEventTargetAndDeepestEnterEventTarget);
|
||||
void UpdateDeepestEnterEventTarget(nsIContent* aDeepestEnterEventTarget);
|
||||
|
||||
nsCOMPtr<nsIContent> GetPendingRemovingOverEventTarget() const {
|
||||
nsCOMPtr<nsIContent> pendingRemovingOverEventTarget =
|
||||
do_QueryReferent(mPendingRemovingOverEventTarget);
|
||||
return pendingRemovingOverEventTarget.forget();
|
||||
}
|
||||
|
||||
// The deepest event target of the last "enter" event. If
|
||||
// mDeepestEnterEventTargetIsOverEventTarget is true, this is the last "over"
|
||||
// event target too. If it's set to false, this is an ancestor of the last
|
||||
// "over" event target which has not been removed from the DOM tree.
|
||||
// "over" event target which is not removed from the DOM tree.
|
||||
nsCOMPtr<nsIContent> mDeepestEnterEventTarget;
|
||||
|
||||
// The last "over" event target which will be considered as disconnected or
|
||||
// connected later because web apps may remove the "over" event target
|
||||
// temporarily and reconnect it to the deepest "enter" target immediately.
|
||||
// In such case, we should keep treating it as the last "over" event target
|
||||
// as the next "out" event target.
|
||||
// FYI: This needs to be a weak pointer. Otherwise, the leak window checker
|
||||
// of mochitests will detect windows in the closed tabs which ran tests
|
||||
// synthesizing mouse moves because while a <browser> is stored with a strong
|
||||
// pointer, the child window is also grabbed by the element.
|
||||
nsWeakPtr mPendingRemovingOverEventTarget;
|
||||
|
||||
// While we're dispatching "over" and "enter" events, this is set to the
|
||||
// "over" event target. If it's removed from the DOM tree, this is set to
|
||||
// nullptr.
|
||||
|
|
@ -158,7 +205,7 @@ class OverOutElementsWrapper final : public nsISupports {
|
|||
// to false. Then, mDeepestEnterEventTarget may be an ancestor of the
|
||||
// "over" element which should be the deepest target of next "leave"
|
||||
// element but shouldn't be target of "out" event.
|
||||
bool mDeepestEnterEventTargetIsOverEventTarget = true;
|
||||
bool mDeepestEnterEventTargetIsOverEventTarget = false;
|
||||
};
|
||||
|
||||
class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@
|
|||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[mouse events from mouse received before/after child moved at mousedown]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
expected: FAIL
|
||||
|
||||
[mouse events from mouse received before/after child moved at mouseup]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
expected: FAIL
|
||||
|
||||
[pointer events from mouse received before/after child attached at pointerdown]
|
||||
expected: FAIL
|
||||
|
|
@ -71,8 +69,8 @@
|
|||
|
||||
[mouse events from touch received before/after child moved at mouseup]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
if os == "mac": NOTRUN
|
||||
FAIL
|
||||
|
||||
[mouse events from touch received before/after child attached at mouseup]
|
||||
expected:
|
||||
|
|
@ -82,3 +80,4 @@
|
|||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
if os == "mac": NOTRUN
|
||||
FAIL
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
[pointerevent_pointer_boundary_events_after_reappending_last_over_target.html]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
||||
[After re-appending the last over element at pointerover, pointer boundary events should be fired on the original target again]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element at pointerenter, pointer boundary events should be fired on the original target again]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element after pointerover, pointer boundary events should be fired on the original target again]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
|
@ -1,2 +1,33 @@
|
|||
[mouse_boundary_events_after_reappending_last_over_target.tentative.html]
|
||||
prefs: [layout.reflow.synthMouseMove:true]
|
||||
[After re-appending the last over element after mouseover, mouse boundary events should not be fired]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element to outside the deepest mouseenter target (but keeps it as under the cursor) at mouseover, mouse boundary events should be fired on it again to correct the following mouseleave targets]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element to outside the deepest mouseenter target at mouseover, mouse boundary events should be fired only on the element under the cursor]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element to outside the deepest mouseenter target (but keeps under the cursor) after mouseover, mouse boundary events should be fired on it again to correct the following mouseleave event targets]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element to outside the deepest mouseenter target after mouseover, mouse boundary events should be fired only on the element under the cursor]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After re-appending the last over element to the deepest mouseenter target without the original parent after mouseover, mouse boundary events should not be fired]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After removing and re-appending the last over element with flushing layout after mouseover, mouse boundary events should not be fired]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
||||
[After removing after mouseover and re-appending the last over element at next animation frame, mouse boundary events should be fired]
|
||||
expected:
|
||||
if not early_beta_or_earlier: FAIL
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Even temporary removal of "pointerover" target should be considered as removed</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";
|
||||
|
||||
/**
|
||||
* https://w3c.github.io/pointerevents/#dfn-fire-a-pointer-event
|
||||
* > If the previous target at any point will no longer be connected, update the
|
||||
* > previous target to the nearest still connected parent following the event
|
||||
* > path corresponding to dispatching events to the previous target
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
addEventListener("load", () => {
|
||||
function promiseTick() {
|
||||
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
||||
}
|
||||
|
||||
function append3NestedDivElementsToBody() {
|
||||
const div1 = document.createElement("div");
|
||||
div1.setAttribute("id", "grandparent");
|
||||
div1.setAttribute("style", "width: 32px; height: 32px; margin: 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);
|
||||
return { div1, div2, div3 };
|
||||
}
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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("pointerover", event => {
|
||||
div2.appendChild(div3);
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.pointerMove(div3Rect.x + 11, div3Rect.y + 11, {})
|
||||
.send();
|
||||
const expectedEvents = [
|
||||
{ type: "pointerover", target: div3 },
|
||||
{ type: "pointerenter", target: document.body },
|
||||
{ type: "pointerenter", target: div1 },
|
||||
{ type: "pointerenter", target: div2 },
|
||||
{ type: "pointerenter", target: div3 },
|
||||
{ type: "pointermove", target: div3 },
|
||||
// Now, the first pointer move input is handled, then, the next pointer
|
||||
// move should cause "over" and "enter" on the child again because the
|
||||
// target is changed to its parent at the node removal.
|
||||
{ type: "pointerover", target: div3 },
|
||||
{ type: "pointerenter", target: div3 },
|
||||
{ type: "pointermove", target: div3 },
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element at pointerover, " +
|
||||
"pointer boundary events should be fired on the original target again");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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("pointerover", event => {
|
||||
div3.addEventListener("pointerenter", () => {
|
||||
div2.appendChild(div3);
|
||||
}, {once: true});
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.pointerMove(div3Rect.x + 11, div3Rect.y + 11, {})
|
||||
.send();
|
||||
const expectedEvents = [
|
||||
{ type: "pointerover", target: div3 },
|
||||
{ type: "pointerenter", target: document.body },
|
||||
{ type: "pointerenter", target: div1 },
|
||||
{ type: "pointerenter", target: div2 },
|
||||
{ type: "pointerenter", target: div3 },
|
||||
{ type: "pointermove", target: div3 },
|
||||
// Now, the first pointer move input is handled, then, the next pointer
|
||||
// move should cause "over" and "enter" on the child again because the
|
||||
// target is changed to its parent at the node removal.
|
||||
{ type: "pointerover", target: div3 },
|
||||
{ type: "pointerenter", target: div3 },
|
||||
{ type: "pointermove", target: div3 },
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element at pointerenter, " +
|
||||
"pointer boundary events should be fired on the original target again");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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("pointerover", event => {
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div2.appendChild(div3);
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(div3Rect.x + 11, div3Rect.y + 11, {})
|
||||
.send();
|
||||
const expectedEvents = [
|
||||
{ type: "pointerover", target: div3 },
|
||||
{ type: "pointerenter", target: document.body },
|
||||
{ type: "pointerenter", target: div1 },
|
||||
{ type: "pointerenter", target: div2 },
|
||||
{ type: "pointerenter", target: div3 },
|
||||
{ type: "pointermove", target: div3 },
|
||||
// Now, the first pointer move input is handled, then, the next pointer
|
||||
// move should cause "over" and "enter" on the child again because the
|
||||
// target is changed to its parent at the node removal.
|
||||
{ type: "pointerover", target: div3 },
|
||||
{ type: "pointerenter", target: div3 },
|
||||
{ type: "pointermove", target: div3 },
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element after pointerover, " +
|
||||
"pointer boundary events should be fired on the original target again");
|
||||
}, {once: true});
|
||||
</script>
|
||||
</head>
|
||||
<body style="padding-top: 32px"></body>
|
||||
</html>
|
||||
|
|
@ -145,6 +145,463 @@ addEventListener("load", () => {
|
|||
},
|
||||
"After re-appending the last over element at mouseenter, " +
|
||||
"mouse boundary events should not be fired");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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("mouseover", event => {
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div2.appendChild(div3);
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Removing the node temporarily should not cause mouse boundary events.
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element after mouseover, " +
|
||||
"mouse boundary events should not be fired");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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});
|
||||
}
|
||||
}
|
||||
let firstMouseOver = true;
|
||||
div3.addEventListener("mouseover", event => {
|
||||
div1.insertBefore(div3, div2);
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// The "mouseover" target was moved to outside the deepest "mouseenter"
|
||||
// target after the node is removed. Therefore, mouse boundary events
|
||||
// should be fired on the original "mouseover" target again.
|
||||
{ type: "mouseleave", target: div2 },
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element to outside the deepest mouseenter target (but keeps it as under the cursor) at mouseover, " +
|
||||
"mouse boundary events should be fired on it again to correct the following mouseleave targets");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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});
|
||||
}
|
||||
}
|
||||
let firstMouseOver = true;
|
||||
div3.addEventListener("mouseover", event => {
|
||||
div1.append(div3);
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Now, div2 (id=parent) should be under the mouse cursor. However,
|
||||
// the "mouseover" target was once removed and not reconnected under the
|
||||
// deepest "mouseenter" target. Therefore, mouse boundary events should
|
||||
// not be fired on the div3, but should be fired on the div2.
|
||||
{ type: "mouseover", target: div2},
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element to outside the deepest mouseenter target at mouseover, " +
|
||||
"mouse boundary events should be fired only on the element under the cursor");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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});
|
||||
}
|
||||
}
|
||||
let firstMouseOver = true;
|
||||
div3.addEventListener("mouseover", event => {
|
||||
div2.remove();
|
||||
div1.appendChild(div3);
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Reconnecting the last "mouseover" target to a grandparent, div3, (i.e.,
|
||||
// without parent, div2) immediately should be treated as a temporary
|
||||
// removal because browsers can store only the deepest last "mouseenter"
|
||||
// target instead of the full path of the event targets. Therefore,
|
||||
// "mouseover" nor "mouseenter" should not be fired again on div3.
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element to the deepest mouseenter target without the original parent at mouseover, " +
|
||||
"mouse boundary events should not be fired");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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});
|
||||
}
|
||||
}
|
||||
let firstMouseOver = true;
|
||||
div3.addEventListener("mouseover", event => {
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div1.insertBefore(div3, div2);
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// The "mouseover" target was moved to outside the deepest "mouseenter"
|
||||
// target after the node is removed. Therefore, mouse boundary events
|
||||
// should be fired on the original "mouseover" target again.
|
||||
{ type: "mouseleave", target: div2 },
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element to outside the deepest mouseenter target (but keeps under the cursor) after mouseover, " +
|
||||
"mouse boundary events should be fired on it again to correct the following mouseleave event targets");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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});
|
||||
}
|
||||
}
|
||||
let firstMouseOver = true;
|
||||
div3.addEventListener("mouseover", event => {
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div1.append(div3);
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Now, div2 (id=parent) should be under the mouse cursor. However,
|
||||
// the "mouseover" target was once removed and not reconnected under the
|
||||
// deepest "mouseenter" target. Therefore, mouse boundary events should
|
||||
// not be fired on the div3, but should be fired on the div2.
|
||||
{ type: "mouseover", target: div2},
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element to outside the deepest mouseenter target after mouseover, " +
|
||||
"mouse boundary events should be fired only on the element under the cursor");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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});
|
||||
}
|
||||
}
|
||||
let firstMouseOver = true;
|
||||
div3.addEventListener("mouseover", event => {
|
||||
div2.remove();
|
||||
div1.appendChild(div3);
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div2.remove();
|
||||
div1.appendChild(div3);
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Reconnecting the last "mouseover" target to a grandparent, div3, (i.e.,
|
||||
// without parent, div2) immediately should be treated as a temporary
|
||||
// removal because browsers can store only the deepest last "mouseenter"
|
||||
// target instead of the full path of the event targets. Therefore,
|
||||
// "mouseover" nor "mouseenter" should not be fired again on div3.
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After re-appending the last over element to the deepest mouseenter target without the original parent after mouseover, " +
|
||||
"mouse boundary events should not be fired");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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("mouseover", event => {
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div3.remove();
|
||||
div2.getBoundingClientRect(); // maybe refresh the layout
|
||||
div2.appendChild(div3);
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Removing the node temporarily should not cause mouse boundary events.
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After removing and re-appending the last over element with flushing layout after mouseover, " +
|
||||
"mouse boundary events should not be fired");
|
||||
|
||||
promise_test(async () => {
|
||||
const {div1, div2, div3} = append3NestedDivElementsToBody();
|
||||
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("mouseover", event => {
|
||||
events = [];
|
||||
events.push({type: event.type, target: event.target});
|
||||
}, {once: true});
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.pointerMove(div3Rect.x + 10, div3Rect.y + 10, {})
|
||||
.send();
|
||||
await promiseTick();
|
||||
div3.remove();
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
div2.appendChild(div3);
|
||||
await promiseTick();
|
||||
const expectedEvents = [
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: document.body },
|
||||
{ type: "mouseenter", target: div1 },
|
||||
{ type: "mouseenter", target: div2 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
{ type: "mousemove", target: div3 },
|
||||
// Removing the node and appending it occurred in different animation
|
||||
// frames. Therefore, it shouldn't be treated as a temporary removal
|
||||
// since user may have seen the layout change.
|
||||
{ type: "mouseover", target: div2 }, // no mouseout on div3 because it's not connected at this moment
|
||||
{ type: "mouseout", target: div2 },
|
||||
{ type: "mouseover", target: div3 },
|
||||
{ type: "mouseenter", target: div3 },
|
||||
];
|
||||
assert_equals(
|
||||
stringifyEvents(events),
|
||||
stringifyEvents(expectedEvents),
|
||||
);
|
||||
div1.remove();
|
||||
await new test_driver.Actions()
|
||||
.pointerMove(1, 1, {})
|
||||
.send();
|
||||
},
|
||||
"After removing after mouseover and re-appending the last over element at next animation frame, " +
|
||||
"mouse boundary events should be fired");
|
||||
}, {once: true});
|
||||
</script>
|
||||
</head>
|
||||
|
|
|
|||
Loading…
Reference in a new issue