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