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