mirror of
https://github.com/servo/servo.git
synced 2025-07-16 03:43:38 +01:00
453 lines
16 KiB
HTML
453 lines
16 KiB
HTML
<!DOCTYPE HTML>
|
|
<meta charset="UTF-8">
|
|
<title>CSS Toggles: ARIA roles and inferred keyboard handling</title>
|
|
<link rel="author" title="L. David Baron" href="https://dbaron.org/">
|
|
<link rel="author" title="Google" href="http://www.google.com/">
|
|
<link rel="help" href="https://tabatkins.github.io/css-toggle/">
|
|
<link rel="help" href="https://github.com/tabatkins/css-toggle/issues/41">
|
|
<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="support/toggle-helpers.js"></script>
|
|
<style>
|
|
|
|
/* for send_keys */
|
|
div { min-height: 10px }
|
|
|
|
</style>
|
|
|
|
<body>
|
|
|
|
<div id="container"></div>
|
|
<script>
|
|
|
|
let aria_role_tests = [
|
|
// Markup to create the test assertions:
|
|
// data-expected-role: The expected aria role for this element.
|
|
// data-expected-trigger-keys: When present, indicates that keyboard events
|
|
// should be tested on this element, and indicates the keys
|
|
// (space-separated) that are expected to toggle the toggle.
|
|
// data-expected-arrows-between-children: When present, indicates
|
|
// that arrow keys should navigate between the children of this
|
|
// element that have the given role.
|
|
//
|
|
// Helper markup to create more markup:
|
|
// class=group: group the group with the toggle-group property
|
|
// class=group-self: same, but with the self keyword (narrow scope)
|
|
// class=root: create a test-role toggle with the toggle-root property
|
|
// class=root-group: same, but with the 'group' keyword
|
|
// class=root-self: same, but with the 'self' keyword
|
|
// class=trigger: toggle-trigger to activate test-role toggle
|
|
// class=visibility: toggle-visibility connected to test-role toggle
|
|
`
|
|
<div></div>
|
|
`,
|
|
`
|
|
<div class="root">
|
|
<div></div>
|
|
</div>
|
|
`,
|
|
`
|
|
<div class="root trigger" data-expected-role="checkbox"></div>
|
|
`,
|
|
// Test that ARIA attributes override the toggle inference:
|
|
`
|
|
<div class="root trigger" role="link" data-expected-role="link"></div>
|
|
`,
|
|
`
|
|
<div class="root">
|
|
<div class="trigger" data-expected-role="button"></div>
|
|
</div>
|
|
`,
|
|
|
|
// Radios and radio groups:
|
|
`
|
|
<div class="group" data-expected-role="radiogroup">
|
|
<div class="root-group trigger" data-expected-role="radio" data-expected-trigger-keys="Space"></div>
|
|
</div>
|
|
`,
|
|
`
|
|
<div class="group" data-expected-role="radiogroup" data-expected-arrows-between-children="radio">
|
|
<div class="root-group trigger" data-expected-role="radio" data-expected-trigger-keys="Space"></div>
|
|
<div class="root-group trigger" data-expected-role="radio" data-expected-trigger-keys="Space"></div>
|
|
</div>
|
|
`,
|
|
`
|
|
<div>
|
|
<div class="root-group trigger" data-expected-role="radio"></div>
|
|
</div>
|
|
`,
|
|
`
|
|
<div style="toggle-group: another-group">
|
|
<div class="root-group trigger" data-expected-role="radio"></div>
|
|
</div>
|
|
`,
|
|
`
|
|
<div style="toggle-group: another-group, test-role, third-group" data-expected-role="radiogroup">
|
|
<div class="root-group trigger" data-expected-role="radio"></div>
|
|
</div>
|
|
`,
|
|
|
|
|
|
// Checkboxes and checkbox groups:
|
|
`
|
|
<div>
|
|
<div class="root trigger" data-expected-role="checkbox" data-expected-trigger-keys="Space"></div>
|
|
</div>
|
|
`,
|
|
// TODO(https://crbug.com/1250716): This is a checkbox group... but we
|
|
// can't distinguish that with current ARIA roles.
|
|
`
|
|
<div data-expected-arrows-between-children="checkbox">
|
|
<div class="root trigger" data-expected-role="checkbox" data-expected-trigger-keys="Space"></div>
|
|
<div class="root trigger" data-expected-role="checkbox" data-expected-trigger-keys="Space"></div>
|
|
</div>
|
|
`,
|
|
|
|
// Disclosure:
|
|
// TODO(https://crbug.com/1250716): This is a disclosure... but how is
|
|
// it possible to distinguish with ARIA roles (compare to next test!)?
|
|
`
|
|
<div class="root">
|
|
<div class="trigger" data-expected-role="button" data-expected-trigger-keys="Space Enter"></div>
|
|
<div class="visibility"></div>
|
|
</div>
|
|
`,
|
|
// This is not a disclosure because it has a toggle-group.
|
|
`
|
|
<div class="root-group">
|
|
<div class="trigger" data-expected-role="button" data-expected-trigger-keys="Space Enter"></div>
|
|
<div class="visibility"></div>
|
|
</div>
|
|
`,
|
|
// This is button with popup (absolute positioning)
|
|
// TODO(https://crbug.com/1250716): This test doesn't actually
|
|
// distinguish this from disclosure because the internal kPopUpButton
|
|
// role maps to "button" in kReverseRoles in ax_object.cc.
|
|
`
|
|
<div class="root">
|
|
<div class="trigger" data-expected-role="button"></div>
|
|
<div class="visibility" style="position: absolute"></div>
|
|
</div>
|
|
`,
|
|
// This is button with popup (fixed positioning)
|
|
// TODO(https://crbug.com/1250716): This test doesn't actually
|
|
// distinguish this from disclosure because the internal kPopUpButton
|
|
// role maps to "button" in kReverseRoles in ax_object.cc.
|
|
`
|
|
<div class="root">
|
|
<div class="trigger" data-expected-role="button" data-expected-trigger-keys="Space Enter"></div>
|
|
<div class="visibility" style="position: fixed"></div>
|
|
</div>
|
|
`,
|
|
// This is button with popup (popover)
|
|
// TODO(https://crbug.com/1250716): This test doesn't actually
|
|
// distinguish this from disclosure because the internal kPopUpButton
|
|
// role maps to "button" in kReverseRoles in ax_object.cc.
|
|
`
|
|
<div class="root">
|
|
<div class="trigger" data-expected-role="button" data-expected-trigger-keys="Space Enter"></div>
|
|
<div class="visibility" popover="auto"></div>
|
|
</div>
|
|
`,
|
|
// This is disclosure (NOT button with popup) (sticky positioning)
|
|
`
|
|
<div class="root">
|
|
<div class="trigger" data-expected-role="button" data-expected-trigger-keys="Space Enter"></div>
|
|
<div class="visibility" style="position: sticky"></div>
|
|
</div>
|
|
`,
|
|
|
|
// Accordion:
|
|
`
|
|
<div class="group">
|
|
<div class="root-group" data-expected-role="region">
|
|
<div class="trigger" data-expected-role="button"></div>
|
|
<div class="visibility"></div>
|
|
</div>
|
|
<div class="root-group" data-expected-role="region">
|
|
<div class="trigger" data-expected-role="button"></div>
|
|
<div class="visibility"></div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
// Not accordion because of other siblings:
|
|
`
|
|
<div class="group">
|
|
<div class="root-group">
|
|
<div class="trigger" data-expected-role="button"></div>
|
|
<div class="visibility"></div>
|
|
</div>
|
|
<div class="root-group">
|
|
<div class="trigger" data-expected-role="button"></div>
|
|
<div class="visibility"></div>
|
|
</div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
</div>
|
|
`,
|
|
|
|
// Tree:
|
|
// TODO(https://crbug.com/1250716): This should probably also work
|
|
// with the toggles on the <button>!
|
|
// TODO(https://crbug.com/1250716): This should probably mark the
|
|
// non-interactive items as treeitem as well!
|
|
// TODO(https://crbug.com/1250716): Do the elements getting the roles
|
|
// here make sense?
|
|
// TODO(https://crbug.com/1250716): The requirement for having
|
|
// multiple disclosure-ish children to qualify as accordion-ish
|
|
// probably doesn't make sense here. The test below is basically the
|
|
// minimal example that gets detected as a tree, but simpler things
|
|
// definitely should be!
|
|
// TODO(https://crbug.com/1250716): The inner parts of the tree should
|
|
// also be getting tree roles!
|
|
`
|
|
<ul data-expected-role="tree">
|
|
<li class="root-self" data-expected-role="group">
|
|
<button class="trigger" data-expected-role="treeitem"></button>
|
|
<ul class="visibility" data-expected-role="list">
|
|
<li>item</li>
|
|
<li class="root-self">
|
|
<button class="trigger" data-expected-role="button"></button>
|
|
<ul class="visibility" data-expected-role="list">
|
|
<li>item</li>
|
|
<li>item</li>
|
|
</ul>
|
|
</li>
|
|
<li class="root-self">
|
|
<button class="trigger" data-expected-role="button"></button>
|
|
<ul class="visibility" data-expected-role="list">
|
|
<li>item</li>
|
|
<li>item</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li class="root-self" data-expected-role="group">
|
|
<button class="trigger" data-expected-role="treeitem"></button>
|
|
<ul class="visibility" data-expected-role="list">
|
|
<li class="root-self">
|
|
<button class="trigger" data-expected-role="button"></button>
|
|
<ul class="visibility" data-expected-role="list">
|
|
<li>item</li>
|
|
<li>item</li>
|
|
</ul>
|
|
</li>
|
|
<li class="root-self">
|
|
<button class="trigger" data-expected-role="button"></button>
|
|
<ul class="visibility" data-expected-role="list">
|
|
<li>item</li>
|
|
<li>item</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
`,
|
|
|
|
// Tabs:
|
|
`
|
|
<section class="group" data-expected-role="tablist" data-expected-arrows-between-children="tab">
|
|
<h1 class="root-group trigger" data-expected-role="tab" data-expected-trigger-keys="Space Enter"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 class="root-group trigger" data-expected-role="tab" data-expected-trigger-keys="Space Enter"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 class="root-group trigger" data-expected-role="tab" data-expected-trigger-keys="Space Enter"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
</section>
|
|
`,
|
|
`
|
|
<section class="group" data-expected-role="tablist">
|
|
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<div></div>
|
|
</section>
|
|
`,
|
|
`
|
|
<section class="group" data-expected-role="tablist">
|
|
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 style="toggle-root: other-toggle; toggle-trigger: other-toggle" data-expected-role="checkbox"></h1>
|
|
</section>
|
|
`,
|
|
`
|
|
<section class="group" data-expected-role="tablist">
|
|
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 class="root-group trigger" data-expected-role="tab"></h1>
|
|
<div class="visibility" data-expected-role="tabpanel"></div>
|
|
<h1 style="toggle-root: other-toggle; toggle-trigger: other-toggle" data-expected-role="button"></h1>
|
|
<div style="toggle-visibility: toggle other-toggle"></div>
|
|
</section>
|
|
`,
|
|
// TODO(https://crbug.com/758089): The expected role for the <section>
|
|
// should be generic rather than null!
|
|
`
|
|
<section class="group" data-expected-role="null">
|
|
<h1 class="root-group trigger" data-expected-role="button"></h1>
|
|
<div class="visibility"></div>
|
|
<h1 class="root-group trigger" data-expected-role="button"></h1>
|
|
<div class="visibility"></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
</section>
|
|
`,
|
|
`
|
|
<section class="group" data-expected-role="radiogroup">
|
|
<h1 class="root-group trigger" data-expected-role="radio"></h1>
|
|
<h1 class="root-group trigger" data-expected-role="radio"></h1>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
<div></div>
|
|
</section>
|
|
`,
|
|
];
|
|
|
|
function find_toggle_in_scope(e) {
|
|
let allow_self = true;
|
|
while (e) {
|
|
let toggle = e.toggles.get("test-role");
|
|
if (toggle && (allow_self || toggle.scope == "wide"))
|
|
return toggle;
|
|
let sibling = e.previousElementSibling;
|
|
if (sibling) {
|
|
e = sibling;
|
|
allow_self = false;
|
|
}
|
|
e = e.parentNode;
|
|
allow_self = true;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
for (let t of aria_role_tests) {
|
|
promise_test(async function() {
|
|
container.innerHTML = t;
|
|
|
|
for (let e of container.querySelectorAll('.group')) {
|
|
e.style.toggleGroup = "test-role";
|
|
}
|
|
for (let e of container.querySelectorAll('.group-self')) {
|
|
e.style.toggleGroup = "test-role self";
|
|
}
|
|
for (let e of container.querySelectorAll('.root')) {
|
|
e.style.toggleRoot = "test-role";
|
|
}
|
|
for (let e of container.querySelectorAll('.root-group')) {
|
|
e.style.toggleRoot = "test-role group";
|
|
}
|
|
for (let e of container.querySelectorAll('.root-self')) {
|
|
e.style.toggleRoot = "test-role self";
|
|
}
|
|
for (let e of container.querySelectorAll('.trigger')) {
|
|
e.style.toggleTrigger = "test-role";
|
|
}
|
|
for (let e of container.querySelectorAll('.visibility')) {
|
|
e.style.toggleVisibility = "toggle test-role";
|
|
}
|
|
|
|
for (let e of container.querySelectorAll('.root, .root-nogroup')) {
|
|
await wait_for_toggle_creation(e);
|
|
}
|
|
|
|
let count = 0;
|
|
for (let e of container.querySelectorAll("*")) {
|
|
if (e == container)
|
|
continue;
|
|
|
|
let expected_role = "generic";
|
|
if (e.hasAttribute("data-expected-role")) {
|
|
expected_role = e.getAttribute("data-expected-role");
|
|
// TODO(https://crbug.com/758089): See above regarding <section>;
|
|
// this null handling should eventually be removed.
|
|
if (expected_role === "null") {
|
|
expected_role = null;
|
|
}
|
|
}
|
|
++count;
|
|
// NOTE: This relies on Element.computedRole, which is an
|
|
// experimental feature behind the ComputedAccessibilityInfo flag
|
|
// in blink.
|
|
assert_equals(e.computedRole, expected_role, `role on ${e.tagName} element (#${count})`);
|
|
}
|
|
|
|
if (container.querySelector("[data-expected-arrows-between-children], [data-expected-trigger-keys]")) {
|
|
// We should do keyboard tests for this test.
|
|
for (let e of container.querySelectorAll("*")) {
|
|
if (e == container)
|
|
continue;
|
|
|
|
let arrows = e.parentNode.hasAttribute("data-expected-arrows-between-children") && e.getAttribute("data-expected-role") == e.parentNode.getAttribute("data-expected-arrows-between-children");
|
|
let trigger_keys = [];
|
|
if (e.hasAttribute("data-expected-trigger-keys")) {
|
|
trigger_keys = e.getAttribute("data-expected-trigger-keys").split(" ");
|
|
}
|
|
|
|
if (!e.classList.contains("trigger")) {
|
|
// It is a bug in the test to have expected key handling for elements
|
|
// that are not toggle triggers.
|
|
assert_false(arrows);
|
|
assert_equals(trigger_keys.length, 0);
|
|
continue;
|
|
}
|
|
|
|
// Test handling of space, enter, and all arrows, and check that
|
|
// it matches expectations.
|
|
let keys_to_test = {
|
|
"Enter": "\uE007",
|
|
// better known as " ", but "Space" in this test.
|
|
// https://w3c.github.io/webdriver/#keyboard-actions says
|
|
// "\uE00D" but that doesn't work.
|
|
"Space": " ",
|
|
"ArrowLeft": "\uE012",
|
|
"ArrowUp": "\uE013",
|
|
"ArrowRight": "\uE014",
|
|
"ArrowDown": "\uE015",
|
|
};
|
|
for (let key in keys_to_test) {
|
|
let toggle = find_toggle_in_scope(e);
|
|
let expected_state = toggle.valueAsNumber;
|
|
e.focus();
|
|
let expected_focus = e;
|
|
assert_equals(document.activeElement, expected_focus, `focus before ${key} key`);
|
|
await test_driver.send_keys(document.body, keys_to_test[key]);
|
|
if (trigger_keys.includes(key)) {
|
|
expected_state = expected_state ? 0 : 1;
|
|
}
|
|
if (key.startsWith("Arrow")) {
|
|
if (arrows) {
|
|
let role = e.parentNode.getAttribute("data-expected-arrows-between-children");
|
|
let direction;
|
|
if (key == "ArrowLeft" || key == "ArrowUp") {
|
|
direction = "previousElementSibling";
|
|
} else {
|
|
direction = "nextElementSibling";
|
|
}
|
|
let new_element = e;
|
|
while ((new_element = new_element[direction])) {
|
|
if (new_element.getAttribute("data-expected-role") == role)
|
|
break;
|
|
}
|
|
if (new_element)
|
|
expected_focus = new_element;
|
|
}
|
|
}
|
|
assert_equals(toggle.valueAsNumber, expected_state, `state after ${key} key`);
|
|
assert_equals(document.activeElement, expected_focus, `focus after ${key} key`);
|
|
}
|
|
}
|
|
}
|
|
|
|
}, `aria role and key handling test: ${t}`);
|
|
}
|
|
|
|
</script>
|