Skip to content

Commit

Permalink
Make PostEventHandler::CheckPointerCaptureState synthesize `ePointe…
Browse files Browse the repository at this point in the history
…rMove` and `eMouseMove` if nobody captures the pointer anymore

When an element starts capturing a pointer, pointer/mouse boundary events are
dispatched by `EventStateManager::PreHandleEvent` [1].  However, when the
capturing element loses the capture, they are not dispatched.

When the pointer capture is implicitly released, the pointer may be over another
document.  Therefore, this patch synthesizes an internal `ePointerMove` and
`eMouseMove` on the widget to make `PresShell::HandleEvent` redirects the event
to proper document under the pointer.

Unfortunately, I add 2 manual tests into WPT.  The reason is, a drag operation
across document boundary with test driver does not work even if I specify the
pointer position within the parent document coordinates.  This is same both on
Firefox and Chrome.  Additionally, writing the new tests as a mochitest won't
work too.  If I use synthesized mouse events, I see similar failure.
Additionally, when I use native events, it works, but unstable to run on CI.

1. https://searchfox.org/mozilla-central/rev/669fac9888b173c02baa4c036e980c0c204dfe02/dom/events/EventStateManager.cpp#1139-1140

Differential Revision: https://phabricator.services.mozilla.com/D218896

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1793267
gecko-commit: f3d7c6dd48d53773bf29d8fe448cda233b955b68
gecko-reviewers: smaug
  • Loading branch information
masayuki-nakano authored and moz-wptsync-bot committed Aug 20, 2024
1 parent 2eb5c22 commit f9d5548
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Pointer Events when mouse button up on the parent document while an element in a child document captures the pointer</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";

setup({explicit_timeout: true});

addEventListener("load", () => {
promise_test(async t => {
const iframe = document.querySelector("iframe");
const button = iframe.contentDocument.querySelector("button");
const div = iframe.contentDocument.querySelector("div");
const divInParent = document.querySelector("iframe + div");

let pointerEvents = [];
let mouseEvents = [];
function recordPointerEvent(event) {
pointerEvents.push(event);
}
function recordMouseEvent(event) {
mouseEvents.push(event);
}

function stringifyEvent(event) {
function stringifyTarget(target) {
switch (target) {
case button:
return "<button> in child";
case div:
return "<div> in child which captured the pointer";
case divInParent:
return "<div> in parent";
default:
return target?.nodeName;
}
}
return `"${event.type}" on ${stringifyTarget(event.target)}`;
}

const pointerEventTypes = ["pointerup", "lostpointercapture", "pointerover", "pointerout", "pointerenter", "pointerleave", "pointermove"];
const mouseEventTypes = ["mouseup", "mouseover", "mouseout", "mouseenter", "mouseleave", "mousemove"];
const promisePointerUp = new Promise(resolve => {
button.addEventListener("pointerdown", event => {
div.setPointerCapture(event.pointerId);
iframe.contentWindow.addEventListener("pointerup", event => {
recordPointerEvent(event);
[button, div, divInParent].forEach(target => {
pointerEventTypes.forEach(type => {
target.addEventListener(type, recordPointerEvent);
});
mouseEventTypes.forEach(type => {
target.addEventListener(type, recordMouseEvent);
});
});
resolve();
}, {once: true});
}, {once: true});
});

await promisePointerUp;
await new Promise(
resolve => requestAnimationFrame(
() => requestAnimationFrame(resolve)
)
);

const stringifiedPointerEvents = [];
const stringifiedMouseEvents = [];
for (const event of pointerEvents) {
stringifiedPointerEvents.push(stringifyEvent(event));
}
for (const event of mouseEvents) {
stringifiedMouseEvents.push(stringifyEvent(event));
}
const stringifiedExpectedPointerEvents = [
stringifyEvent({ type: "pointerup", target: div }),
stringifyEvent({ type: "lostpointercapture", target: div }),
stringifyEvent({ type: "pointerout", target: div }),
stringifyEvent({ type: "pointerleave", target: div }),
stringifyEvent({ type: "pointerover", target: divInParent }),
stringifyEvent({ type: "pointerenter", target: divInParent }),
];
if (pointerEvents[pointerEvents.length - 1]?.type == "pointermove") {
stringifiedExpectedPointerEvents.push(
stringifyEvent({ type: "pointermove", target: divInParent })
);
}
const stringifiedExpectedMouseEvents = [
stringifyEvent({ type: "mouseup", target: div }),
stringifyEvent({ type: "mouseout", target: div }),
stringifyEvent({ type: "mouseleave", target: div }),
stringifyEvent({ type: "mouseover", target: divInParent }),
stringifyEvent({ type: "mouseenter", target: divInParent }),
];
if (mouseEvents[mouseEvents.length - 1]?.type == "mousemove") {
stringifiedExpectedMouseEvents.push(
stringifyEvent({ type: "mousemove", target: divInParent })
);
}

t.step(() => {
assert_array_equals(stringifiedPointerEvents, stringifiedExpectedPointerEvents)
assert_array_equals(stringifiedMouseEvents, stringifiedExpectedMouseEvents)
});
t.done();
}, "boundary events should be fired for notifying web apps of over the element in parent document");
}, {once: true});
</script>
</head>
<body>
<div>
<p>Test steps:</p>
<ol>
<li>Press the button with primary button of your mouse and start dragging</li>
<li>Move the mouse cursor over the red border box and release the mouse button</li>
</ol>
</div>
<iframe srcdoc="<button>Start dragging from this button</button><div><br></div>"></iframe>
<div style='border: 1px solid red'>And release mouse button over this box</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>Pointer Events when mouse button up on a sub-document while an element in parent document captures the pointer</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
"use strict";

setup({explicit_timeout: true});

addEventListener("load", () => {
promise_test(async t => {
const button = document.querySelector("button");
const div = document.querySelector("button + div");
const iframe = document.querySelector("iframe");
const divInChild = iframe.contentDocument.querySelector("div");

let pointerEvents = [];
let mouseEvents = [];
function recordPointerEvent(event) {
pointerEvents.push(event);
}
function recordMouseEvent(event) {
mouseEvents.push(event);
}

function stringifyEvent(event) {
function stringifyTarget(target) {
switch (target) {
case button:
return "<button>";
case div:
return "<div> in parent which captured the pointer";
case divInChild:
return "<div> in child";
default:
return target?.nodeName;
}
}
return `"${event.type}" on ${stringifyTarget(event.target)}`;
}

const pointerEventTypes = ["pointerup", "lostpointercapture", "pointerover", "pointerout", "pointerenter", "pointerleave", "pointermove"];
const mouseEventTypes = ["mouseup", "mouseover", "mouseout", "mouseenter", "mouseleave", "mousemove"];
const promisePointerUp = new Promise(resolve => {
button.addEventListener("pointerdown", event => {
div.setPointerCapture(event.pointerId);
addEventListener("pointerup", event => {
recordPointerEvent(event);
[button, div, divInChild].forEach(target => {
pointerEventTypes.forEach(type => {
target.addEventListener(type, recordPointerEvent);
});
mouseEventTypes.forEach(type => {
target.addEventListener(type, recordMouseEvent);
});
});
resolve();
}, {once: true});
}, {once: true});
});

await promisePointerUp;
await new Promise(
resolve => requestAnimationFrame(
() => requestAnimationFrame(resolve)
)
);

const stringifiedPointerEvents = [];
const stringifiedMouseEvents = [];
for (const event of pointerEvents) {
stringifiedPointerEvents.push(stringifyEvent(event));
}
for (const event of mouseEvents) {
stringifiedMouseEvents.push(stringifyEvent(event));
}
const stringifiedExpectedPointerEvents = [
stringifyEvent({ type: "pointerup", target: div }),
stringifyEvent({ type: "lostpointercapture", target: div }),
stringifyEvent({ type: "pointerout", target: div }),
stringifyEvent({ type: "pointerleave", target: div }),
stringifyEvent({ type: "pointerover", target: divInChild }),
stringifyEvent({ type: "pointerenter", target: divInChild }),
];
if (pointerEvents[pointerEvents.length - 1]?.type == "pointermove") {
stringifiedExpectedPointerEvents.push(
stringifyEvent({ type: "pointermove", target: divInChild })
);
}
const stringifiedExpectedMouseEvents = [
stringifyEvent({ type: "mouseup", target: div }),
stringifyEvent({ type: "mouseout", target: div }),
stringifyEvent({ type: "mouseleave", target: div }),
stringifyEvent({ type: "mouseover", target: divInChild }),
stringifyEvent({ type: "mouseenter", target: divInChild }),
];
if (mouseEvents[mouseEvents.length - 1]?.type == "mousemove") {
stringifiedExpectedMouseEvents.push(
stringifyEvent({ type: "mousemove", target: divInChild })
);
}

t.step(() => {
assert_array_equals(stringifiedPointerEvents, stringifiedExpectedPointerEvents)
assert_array_equals(stringifiedMouseEvents, stringifiedExpectedMouseEvents)
});
t.done();
}, "boundary events should be fired for notifying web apps of over the element in child document");
}, {once: true});
</script>
</head>
<body>
<div>
<p>Test steps:</p>
<ol>
<li>Press the button with primary button of your mouse and start dragging</li>
<li>Move the mouse cursor over the red border box and release the mouse button</li>
</ol>
</div>
<button>Start dragging from this button</button>
<div><br></div>
<iframe srcdoc="<div style='border: 1px solid red'>And release mouse button over this box</div>"></iframe>
</body>
</html>

0 comments on commit f9d5548

Please sign in to comment.