Bug 1567600 - Part 2: Better accessibility for about:addons panel-list r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D40506

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Striemer 2019-08-22 21:00:09 +00:00
Родитель 97c482692c
Коммит c19ef4049e
3 изменённых файлов: 116 добавлений и 33 удалений

Просмотреть файл

@ -220,11 +220,11 @@
<template name="panel-list">
<link rel="stylesheet" href="chrome://mozapps/content/extensions/panel-list.css">
<div class="arrow top"></div>
<div class="list">
<div class="arrow top" role="presentation"></div>
<div class="list" role="presentation">
<slot></slot>
</div>
<div class="arrow bottom"></div>
<div class="arrow bottom" role="presentation"></div>
</template>
<template name="panel-item">

Просмотреть файл

@ -551,6 +551,7 @@ class PanelList extends HTMLElement {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(importTemplate("panel-list"));
this.setAttribute("role", "menu");
}
attributeChangedCallback(name, oldVal, newVal) {
@ -646,14 +647,12 @@ class PanelList extends HTMLElement {
this.setAttribute("valign", valign);
this.parentNode.style.overflow = "";
this.removeAttribute("showing");
// Send the shown event after the next paint.
requestAnimationFrame(() => this.sendEvent("shown"));
}
addHideListeners() {
// Hide when a panel-item is clicked in the list.
this.addEventListener("click", this);
document.addEventListener("keydown", this);
// Hide when a click is initiated outside the panel.
document.addEventListener("mousedown", this);
// Hide if focus changes and the panel isn't in focus.
@ -668,6 +667,7 @@ class PanelList extends HTMLElement {
removeHideListeners() {
this.removeEventListener("click", this);
document.removeEventListener("keydown", this);
document.removeEventListener("mousedown", this);
document.removeEventListener("focusin", this);
window.removeEventListener("resize", this);
@ -696,6 +696,53 @@ class PanelList extends HTMLElement {
e.stopPropagation();
}
break;
case "keydown":
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Tab") {
// Ignore tabbing with a modifer other than shift.
if (e.key === "Tab" && (e.altKey || e.ctrlKey || e.metaKey)) {
return;
}
// Don't scroll the page or let the regular tab order take effect.
e.preventDefault();
// Keep moving to the next/previous element sibling until we find a
// panel-item that isn't hidden.
let moveForward =
e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey);
// If the menu is opened with the mouse, the active element might be
// somewhere else in the document. In that case we should ignore it
// to avoid walking unrelated DOM nodes.
this.walker.currentNode = this.contains(document.activeElement)
? document.activeElement
: this;
let nextItem = moveForward
? this.walker.nextNode()
: this.walker.previousNode();
// If the next item wasn't found, try looping to the top/bottom.
if (!nextItem) {
this.walker.currentNode = this;
if (moveForward) {
nextItem = this.walker.firstChild();
} else {
nextItem = this.walker.lastChild();
}
}
if (nextItem) {
nextItem.focus();
}
break;
} else if (e.key === "Escape") {
let { triggeringEvent } = this;
this.hide();
if (triggeringEvent && triggeringEvent.target) {
triggeringEvent.target.focus();
}
}
break;
case "mousedown":
case "focusin":
// There will be a focusin after the mousedown that opens the panel
@ -720,9 +767,44 @@ class PanelList extends HTMLElement {
}
}
onShow() {
this.setAlign();
get walker() {
if (!this._walker) {
this._walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => {
if (node.disabled || node.hidden || node.localName !== "panel-item") {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
}
return this._walker;
}
async onShow() {
let { triggeringEvent } = this;
this.addHideListeners();
await this.setAlign();
// Wait until the next paint for the alignment to be set and panel to be
// visible.
requestAnimationFrame(() => {
// Focus the first visible panel-item if we were opened with the keyboard.
if (
triggeringEvent &&
triggeringEvent.mozInputSource === MouseEvent.MOZ_SOURCE_KEYBOARD
) {
this.walker.currentNode = this;
let firstItem = this.walker.nextNode();
if (firstItem) {
firstItem.focus();
}
}
this.sendEvent("shown");
});
}
onHide() {
@ -742,6 +824,7 @@ class PanelItem extends HTMLElement {
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(importTemplate("panel-item"));
this.button = this.shadowRoot.querySelector("button");
this.button.setAttribute("role", "menuitem");
}
get disabled() {
@ -759,6 +842,10 @@ class PanelItem extends HTMLElement {
set checked(val) {
this.toggleAttribute("checked", val);
}
focus() {
this.button.focus();
}
}
customElements.define("panel-item", PanelItem);

Просмотреть файл

@ -385,34 +385,33 @@ add_task(async function testKeyboardSupport() {
// Test opening and closing the menu.
let moreOptionsMenu = card.querySelector("panel-list");
let expandButton = moreOptionsMenu.querySelector('[action="expand"]');
let toggleDisableButton = card.querySelector('[action="toggle-disabled"]');
is(moreOptionsMenu.open, false, "The menu is closed");
space();
is(moreOptionsMenu.open, true, "The menu is open");
space();
is(moreOptionsMenu.open, false, "The menu is closed");
// Test tabbing out of the menu.
space();
is(moreOptionsMenu.open, true, "The menu is open");
tab({ shiftKey: true });
is(moreOptionsMenu.open, false, "Tabbing away from the menu closes it");
tab();
isFocused(moreOptionsButton, "The button is focused again");
let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
space();
await shown;
is(moreOptionsMenu.open, true, "The menu is open");
for (let it of moreOptionsMenu.querySelectorAll("panel-item:not([hidden])")) {
tab();
isFocused(it, `After tab, focus item "${it.getAttribute("action")}"`);
}
isFocused(expandButton, "The last item is focused");
tab();
is(moreOptionsMenu.open, false, "Tabbing out of the menu closes it");
isFocused(toggleDisableButton, "The disable button is now focused");
EventUtils.synthesizeKey("Escape", {});
is(moreOptionsMenu.open, false, "The menu is closed");
isFocused(moreOptionsButton, "The more options button is focused");
// Focus the button again, focus may have moved out of the browser.
moreOptionsButton.focus();
isFocused(moreOptionsButton, "The button is focused again");
// Test tabbing out of the menu.
space();
shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
is(moreOptionsMenu.open, true, "The menu is open");
await shown;
tab({ shiftKey: true });
is(moreOptionsMenu.open, true, "The menu stays open");
isFocused(expandButton, "The focus has looped to the bottom");
tab();
is(moreOptionsMenu.open, true, "The menu stays open");
isFocused(toggleDisableButton, "The focus has looped to the top");
let hidden = BrowserTestUtils.waitForEvent(moreOptionsMenu, "hidden");
EventUtils.synthesizeKey("Escape", {});
await hidden;
isFocused(moreOptionsButton, "Escape closed the menu");
// Open the menu to test contents.
shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
@ -422,8 +421,6 @@ add_task(async function testKeyboardSupport() {
await shown;
// Disable the add-on.
let toggleDisableButton = card.querySelector('[action="toggle-disabled"]');
tab();
isFocused(toggleDisableButton, "The disable button is focused");
is(card.parentNode, enabledSection, "The card is in the enabled section");
let disabled = BrowserTestUtils.waitForEvent(list, "move");
@ -444,7 +441,6 @@ add_task(async function testKeyboardSupport() {
// Remove the add-on.
tab();
tab();
let removeButton = card.querySelector('[action="remove"]');
isFocused(removeButton, "The remove button is focused");
let removed = BrowserTestUtils.waitForEvent(list, "remove");