Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- test takes ~12 seconds on Firefox macOS -->
<meta name="timeout" content="long">
<title>Tests that the sequential focus navigation starting point is tracked correctly</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
</head>
<body>
<button id="before-container">(before container)</button>
<div id="container"></div>
<button id="after-container">(after container)</button>
<template data-description="Focus navigation starting point should be maintained when focused element is removed.">
<button id="button1" data-prev>button1</button>
<button data-focus data-remove>focus</button>
<button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should be maintained when parent element is removed.">
<button id="button1" data-prev>button1</button>
<div data-remove>
<button data-focus>focus</button>
</div>
<button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should be maintained when shadow host is removed.">
<button id="button1" data-prev>button1</button>
<div data-remove data-shadow>
<template>
<button data-focus>focus</button>
</template>
</div>
<button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should be maintained when focused button is disabled.">
<button id="button1" data-prev>button1</button>
<button data-focus data-disable>focus</button>
<button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should not change when previous element is removed.">
<button id="button1" data-prev>button1</button><button data-focus data-remove-2>
will be removed
</button><button data-focus data-remove>
focus
</button><button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should not change when next element is removed.">
<button id="button1" data-prev>button1</button><button data-focus data-remove>
focus
</button><button data-focus data-remove-2>
will be removed
</button><button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should not change when next element is moved with moveBefore.">
<button id="button1" data-prev>button1</button><button data-focus data-remove>
focus
</button><button data-move id="moved">
will be moved
</button><button id="button2" data-next>button2</button>
<button id="button3">button3</button>
<div data-move-target></div>
<button id="button4">button4</button>
</template>
<template data-description="Focus navigation starting point should not change when previous element is moved with moveBefore.">
<button id="button1" data-prev>button1</button><button data-move id="moved">
will be moved
</button><button data-focus data-remove>
focus
</button><button id="button2" data-next>button2</button>
<button id="button3">button3</button>
<div data-move-target></div>
<button id="button4">button4</button>
</template>
<template data-description="Focus navigation starting point should not change when previous element gets new children.">
<button id="button1">button1</button><div data-add-child-with-data-prev>
parent
</div><button data-focus data-remove>
focus
</button><button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should not change when focused element at end of shadow tree is removed.">
<button id="button1" data-prev>button1</button>
<div data-remove data-shadow>
<template><button data-focus data-remove>focus</button></template>
</div>
<button id="button2" data-next>button2</button>
</template>
<template data-description="Focus navigation starting point should stay on element when it is disabled and re-enabled.">
<button data-prev>button1</button
><button data-focus data-disable data-reenable>
button2
</button><button data-next>button3</button>
</template>
<template data-description="Focus navigation starting point should stay on element when it is blurred.">
<button data-prev>button1</button
><button data-focus data-blur>
button2
</button><button data-next>button3</button>
</template>
<template data-description="Focus navigation starting point should be maintained when slotted focused element is removed.">
<div data-shadow>
<template><slot name="a"
></slot><slot name="b"
></slot><slot name="c"
></slot><slot name="d"
></slot><slot name="e"
></slot></template>
<button slot="a">button 1</button>
<button data-focus data-remove slot="c">focus</button>
<button slot="e">button 5</button>
<button data-prev slot="b">button 2</button>
<button data-next slot="d">button 4</button>
</div>
</template>
<template data-wait-for-event="hashchange"
data-description="Focus navigation starting point should be set when scrolling to fragment (and should not be overwritten when <a> is blurred).">
<a href="#fragment" data-click>click here</a>
<button id="button-1" data-prev>button 1</button>
<div id="fragment">Should set starting point to this div</div>
<button id="button-2" data-next>button 2</button>
</template>
<template data-description="Focus navigation starting point should be used when it's a descendant of the focused element.">
<div tabindex="0" data-focus>
<button data-prev id="button1">button 1</button>
<span data-click>in between</span>
<button data-next id="button2">button 2</button>
</div>
</template>
<template data-description="Backwards focus navigation should not be stuck when starting point is inside focused element but before any other focusable areas.">
<button data-prev id="button1">button 1</button>
<div tabindex="0" id="focusable" data-focus>
<span data-click>in between</span>
<button data-next id="button2">button 2</button>
</div>
</template>
<template data-description="Should start from focused element if it was focused after the selection was set.">
<button id="button1">button 1</button>
<span data-click>in between</span>
<button id="button2">button 2</button>
<button id="button3" data-prev>button 3</button>
<div tabindex="0" id="focusable" data-focus2>focusable</div>
<button id="button4" data-next>button 4</button>
</template>
<script>
"use strict";
const kShift = "\uE008";
const kTab = "\uE004";
// Runs callback on each element matching selector, descending into open shadow roots.
function forEachMatching(selector, callback, root) {
if (!root) { root = container; }
for (let element of root.querySelectorAll(selector)) {
callback(element);
}
for (let element of root.querySelectorAll("[data-shadow]")) {
if (element.shadowRoot) {
forEachMatching(selector, callback, element.shadowRoot);
}
}
}
async function forEachMatchingAsync(selector, callback) {
let elements = [];
forEachMatching(selector, e => elements.push(e));
for (let element of elements) {
await callback(element);
}
}
for (let template of document.querySelectorAll("template")) {
promise_test(async t => {
for (let backwards of [false, true]) {
location.hash = '';
container.replaceChildren();
let test = document.importNode(template.content, true);
container.append(test);
const waitForEvent = (() => {
const {promise, resolve} = Promise.withResolvers();
const eventName = template.dataset.waitForEvent;
if (eventName) {
addEventListener(eventName, resolve);
} else {
resolve();
}
return promise;
})();
// Ensure buttons are keyboard-focusable
// (They aren't by default on Safari)
for (let button of document.querySelectorAll("button")) {
button.tabIndex = 0;
}
forEachMatching("[data-shadow]", element => {
let shadow = element.attachShadow({mode: "open"});
shadow.append(element.querySelector("template").content);
});
forEachMatching("[data-focus]", element => {
element.focus();
});
await forEachMatchingAsync("[data-click]", test_driver.click);
forEachMatching("[data-blur]", element => {
element.blur();
});
forEachMatching("[data-disable]", element => {
element.disabled = true;
});
forEachMatching("[data-remove]", element => {
element.remove();
});
forEachMatching("[data-remove-2]", element => {
element.remove();
});
forEachMatching("[data-move]", element => {
forEachMatching("[data-move-target]", target => {
target.moveBefore(element, null);
});
});
forEachMatching("[data-add-child-with-data-prev]", element => {
let button = document.createElement("button");
button.id = "child";
button.append("child");
button.setAttribute("data-prev", "");
element.append(button);
});
forEachMatching("[data-reenable]", element => {
element.disabled = false;
});
await waitForEvent;
forEachMatching("[data-focus2]", element => {
element.focus();
});
let actions = new test_driver.Actions();
if (backwards) {
actions.keyDown(kShift);
}
actions.keyDown(kTab)
.keyUp(kTab);
if (backwards) {
actions.keyUp(kShift);
}
await actions.send();
let assertions = 0;
forEachMatching(backwards ? "[data-prev]" : "[data-next]", expected => {
let direction = backwards ? "Backwards" : "Forwards";
assertions++;
assert_equals(document.activeElement, expected,
`${direction} sequential focus navigation should go to ${expected.nodeName}#${expected.id}`);
});
assert_equals(assertions, 1, "Each test should have exactly one [data-prev] and one [data-next]");
}
}, template.dataset.description);
}
</script>
</body>
</html>