327 lines
9.0 KiB
HTML
327 lines
9.0 KiB
HTML
<!doctype html>
|
|
<meta charset="utf-8" />
|
|
<title>
|
|
Selectors: :active is observable from within pointerdown/mousedown listeners
|
|
</title>
|
|
<link rel="author" href="mailto:wpt@keithcirkel.co.uk" />
|
|
<link rel="help" href="https://drafts.csswg.org/selectors/#the-active-pseudo" />
|
|
<link
|
|
rel="help"
|
|
href="https://html.spec.whatwg.org/multipage/semantics-other.html#selector-active"
|
|
/>
|
|
<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>
|
|
|
|
<style>
|
|
.target,
|
|
.parent {
|
|
min-width: 80px;
|
|
min-height: 80px;
|
|
}
|
|
</style>
|
|
|
|
<div id="scratch"></div>
|
|
|
|
<script>
|
|
function waitForRender() {
|
|
return new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
);
|
|
}
|
|
|
|
// Use pointer events to perform a click, so each pointerevent
|
|
// can be monitored.
|
|
function click(element, button = "LEFT", { moveWithin = false } = {}) {
|
|
const actions = new test_driver.Actions();
|
|
let chain = actions
|
|
.pointerMove(0, 0, {})
|
|
.pointerMove(0, 0, { origin: element })
|
|
.pointerDown({ button: actions.ButtonType[button] });
|
|
if (moveWithin) {
|
|
chain = chain.pointerMove(5, 5, { origin: element });
|
|
}
|
|
return chain.pointerUp({ button: actions.ButtonType[button] }).send();
|
|
}
|
|
|
|
async function resetPointer() {
|
|
await new test_driver.Actions().pointerMove(0, 0).send();
|
|
await waitForRender();
|
|
}
|
|
|
|
const scratch = document.getElementById("scratch");
|
|
|
|
function makeTarget(t, childTag = "button") {
|
|
const parent = document.createElement("div");
|
|
parent.className = "parent";
|
|
const child = document.createElement(childTag);
|
|
child.className = "target";
|
|
child.append(childTag === "span" ? "some text" : "target");
|
|
parent.append(child);
|
|
scratch.append(parent);
|
|
t.add_cleanup(() => parent.remove());
|
|
return { parent, child };
|
|
}
|
|
|
|
const downEvents = ["pointerdown", "mousedown"];
|
|
|
|
for (const downEvent of downEvents) {
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matched = null;
|
|
target.addEventListener(
|
|
downEvent,
|
|
() => {
|
|
matched = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
assert_true(
|
|
matched,
|
|
`target matches :active synchronously inside its ${downEvent} listener`,
|
|
);
|
|
}, `Clicked element matches :active within its ${downEvent} listener`);
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { parent, child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matched = null;
|
|
target.addEventListener(
|
|
downEvent,
|
|
() => {
|
|
matched = parent.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
assert_true(
|
|
matched,
|
|
`ancestor matches :active within the ${downEvent} listener of a descendant`,
|
|
);
|
|
}, `Ancestor of clicked element matches :active within ${downEvent} listener`);
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { parent, child: textTarget } = makeTarget(t, "span");
|
|
await waitForRender();
|
|
|
|
let matched = null;
|
|
parent.addEventListener(
|
|
downEvent,
|
|
() => {
|
|
matched = parent.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(textTarget);
|
|
assert_true(
|
|
matched,
|
|
`parent element matches :active when a text node is the press target (${downEvent})`,
|
|
);
|
|
}, `Text node press target activates its parent element (${downEvent})`);
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matchedDuringPress = null;
|
|
target.addEventListener(
|
|
downEvent,
|
|
(e) => {
|
|
e.preventDefault();
|
|
matchedDuringPress = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
await waitForRender();
|
|
|
|
assert_true(
|
|
matchedDuringPress,
|
|
`target matches :active even when the ${downEvent} listener calls preventDefault()`,
|
|
);
|
|
assert_false(
|
|
target.matches(":active"),
|
|
`target is no longer :active after release even when ${downEvent} calls preventDefault()`,
|
|
);
|
|
}, `:active applies on press and clears on release with preventDefault() on ${downEvent}`);
|
|
}
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matchedWhileHeld = null;
|
|
target.addEventListener(
|
|
"pointerdown",
|
|
() => {
|
|
matchedWhileHeld = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
await waitForRender();
|
|
|
|
assert_true(
|
|
matchedWhileHeld,
|
|
"target matches :active while the pointer is held down",
|
|
);
|
|
assert_false(
|
|
target.matches(":active"),
|
|
"target no longer matches :active after the pointer is released",
|
|
);
|
|
}, ":active is set while held and cleared after the pointer is released");
|
|
|
|
const upEvents = ["pointerup", "mouseup"];
|
|
|
|
for (const upEvent of upEvents) {
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matchedDuringUp = null;
|
|
target.addEventListener(
|
|
upEvent,
|
|
() => {
|
|
matchedDuringUp = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
await waitForRender();
|
|
|
|
assert_false(
|
|
matchedDuringUp,
|
|
`target no longer matches :active synchronously inside its ${upEvent} listener`,
|
|
);
|
|
assert_false(
|
|
target.matches(":active"),
|
|
`target no longer matches :active after ${upEvent} handling completes`,
|
|
);
|
|
}, `Clicked element no longer matches :active within its ${upEvent} listener`);
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
target.addEventListener(upEvent, (e) => e.preventDefault(), {
|
|
signal: t.signal,
|
|
});
|
|
|
|
let matchedWhileHeld = null;
|
|
target.addEventListener(
|
|
"pointerdown",
|
|
() => {
|
|
matchedWhileHeld = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
await waitForRender();
|
|
|
|
assert_true(
|
|
matchedWhileHeld,
|
|
"target is :active while the pointer is held down",
|
|
);
|
|
assert_false(
|
|
target.matches(":active"),
|
|
`target is no longer :active after release even when ${upEvent} calls preventDefault()`,
|
|
);
|
|
}, `:active is cleared on release even when ${upEvent} listener calls preventDefault()`);
|
|
}
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matchedDuringMove = null;
|
|
target.addEventListener(
|
|
"pointermove",
|
|
() => {
|
|
matchedDuringMove = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target, "LEFT", { moveWithin: true });
|
|
await waitForRender();
|
|
|
|
assert_true(
|
|
matchedDuringMove,
|
|
"target still matches :active while the held pointer moves within it",
|
|
);
|
|
}, ":active persists while the held pointer moves within the target");
|
|
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matchedDuringClick = null;
|
|
target.addEventListener(
|
|
"click",
|
|
() => {
|
|
matchedDuringClick = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target);
|
|
await waitForRender();
|
|
|
|
assert_false(
|
|
matchedDuringClick,
|
|
"target no longer matches :active inside its click listener",
|
|
);
|
|
}, "Clicked element no longer matches :active within its click listener");
|
|
|
|
for (const button of ["RIGHT", "MIDDLE"]) {
|
|
promise_test(async (t) => {
|
|
t.add_cleanup(resetPointer);
|
|
const { child: target } = makeTarget(t);
|
|
await waitForRender();
|
|
|
|
let matchedWhileHeld = null;
|
|
target.addEventListener(
|
|
"pointerdown",
|
|
() => {
|
|
matchedWhileHeld = target.matches(":active");
|
|
},
|
|
{ signal: t.signal },
|
|
);
|
|
|
|
await click(target, button);
|
|
await waitForRender();
|
|
|
|
assert_true(
|
|
matchedWhileHeld,
|
|
`target matches :active for a ${button.toLowerCase()}-button press`,
|
|
);
|
|
assert_false(
|
|
target.matches(":active"),
|
|
`target no longer matches :active after the ${button.toLowerCase()}-button release`,
|
|
);
|
|
}, `Non-primary (${button.toLowerCase()}) button press activates :active`);
|
|
}
|
|
</script>
|