Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!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"
/>
<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>