Bug 1567600 - Part 3: Better accessibility for named-deck and about:addons detail view r=Gijs

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Striemer 2019-08-22 21:00:09 +00:00
parent c19ef4049e
commit 056f2d5ae8
7 changed files with 130 additions and 43 deletions

View File

@ -436,11 +436,7 @@ addon-permissions-list > .addon-detail-row:first-of-type {
}
.deck-tab-group {
border-bottom: 1px solid var(--in-content-box-border-color);
border-top: 1px solid var(--in-content-box-border-color);
margin-top: 8px;
/* Pull the buttons flush with the side of the card */
margin-inline: calc(var(--card-padding) * -1);
font-size: 0;
line-height: 0;
}

View File

@ -118,10 +118,12 @@
<template name="addon-details">
<div class="deck-tab-group">
<named-deck-button deck="details-deck" name="details" data-l10n-id="details-addon-button"></named-deck-button>
<named-deck-button deck="details-deck" name="preferences" data-l10n-id="preferences-addon-button"></named-deck-button>
<named-deck-button deck="details-deck" name="permissions" data-l10n-id="permissions-addon-button"></named-deck-button>
<named-deck-button deck="details-deck" name="release-notes" data-l10n-id="release-notes-addon-button"></named-deck-button>
<named-deck-button-group>
<named-deck-button deck="details-deck" name="details" data-l10n-id="details-addon-button"></named-deck-button>
<named-deck-button deck="details-deck" name="preferences" data-l10n-id="preferences-addon-button"></named-deck-button>
<named-deck-button deck="details-deck" name="permissions" data-l10n-id="permissions-addon-button"></named-deck-button>
<named-deck-button deck="details-deck" name="release-notes" data-l10n-id="release-notes-addon-button"></named-deck-button>
</named-deck-button-group>
</div>
<named-deck id="details-deck">
<section name="details">

View File

@ -16,6 +16,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
ClientID: "resource://gre/modules/ClientID.jsm",
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
E10SUtils: "resource://gre/modules/E10SUtils.jsm",
ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
@ -1459,7 +1460,8 @@ class AddonDetails extends HTMLElement {
}
// Hide the tab group if "details" is the only visible button.
this.tabGroup.hidden = Array.from(this.tabGroup.children).every(button => {
let tabGroupButtons = this.tabGroup.querySelectorAll("named-deck-button");
this.tabGroup.hidden = Array.from(tabGroupButtons).every(button => {
return button.name == "details" || button.hidden;
});
@ -1761,7 +1763,7 @@ class AddonCard extends HTMLElement {
openOptionsInTab(addon.optionsURL);
} else if (getOptionsType(addon) == "inline") {
this.recordActionEvent("preferences", "inline");
loadViewFn("detail", this.addon.id, "preferences");
loadViewFn(`detail/${this.addon.id}/preferences`, e);
}
break;
case "remove":
@ -1788,7 +1790,7 @@ class AddonCard extends HTMLElement {
}
break;
case "expand":
loadViewFn("detail", this.addon.id);
loadViewFn(`detail/${this.addon.id}`, e);
break;
case "more-options":
// Open panel on click from the keyboard.
@ -1814,7 +1816,7 @@ class AddonCard extends HTMLElement {
!this.expanded &&
(e.target === this.addonNameEl || !e.target.closest("a"))
) {
loadViewFn("detail", this.addon.id);
loadViewFn(`detail/${this.addon.id}`, e);
} else if (
e.target.localName == "a" &&
e.target.getAttribute("data-telemetry-name")
@ -2075,11 +2077,15 @@ class AddonCard extends HTMLElement {
throw new Error("addon-card must be initialized with setAddon()");
}
this.card = importTemplate("card").firstElementChild;
let headingId = ExtensionCommon.makeWidgetId(`${addon.name}-heading`);
this.setAttribute("aria-labelledby", headingId);
this.setAttribute("addon-id", addon.id);
this.card = importTemplate("card").firstElementChild;
let nameContainer = this.card.querySelector(".addon-name-container");
let nameHeading = document.createElement("h3");
let headingLevel = this.expanded ? "h1" : "h3";
let nameHeading = document.createElement(headingLevel);
nameHeading.classList.add("addon-name");
if (!this.expanded) {
let name = document.createElement("a");
@ -2116,6 +2122,10 @@ class AddonCard extends HTMLElement {
this.appendChild(this.card);
if (this.expanded && this.keyboardNavigation) {
requestAnimationFrame(() => this.optionsButton.focus());
}
// Return the promise of details rendering to wait on in DetailView.
return doneRenderPromise;
}
@ -2290,7 +2300,7 @@ class RecommendedAddonCard extends HTMLElement {
action: "manage",
addon: this.discoAddon,
});
loadViewFn("detail", this.addonId);
loadViewFn(`detail/${this.addonId}`, event);
break;
default:
if (event.target.matches(".disco-addon-author a[href]")) {
@ -3051,11 +3061,12 @@ class ListView {
}
class DetailView {
constructor({ param, root }) {
constructor({ isKeyboardNavigation, param, root }) {
let [id, selectedTab] = param.split("/");
this.id = id;
this.selectedTab = selectedTab;
this.root = root;
this.isKeyboardNavigation = isKeyboardNavigation;
}
async render() {
@ -3072,10 +3083,11 @@ class DetailView {
setCategoryFn(addon.type);
// Go back to the list view when the add-on is removed.
card.addEventListener("remove", () => loadViewFn("list", addon.type));
card.addEventListener("remove", () => loadViewFn(`list/${addon.type}`));
card.setAddon(addon);
card.expand();
card.keyboardNavigation = this.isKeyboardNavigation;
await card.render();
if (
this.selectedTab === "preferences" &&
@ -3181,13 +3193,17 @@ function initialize(opts) {
* resolve once the view has been updated to conform with other about:addons
* views.
*/
async function show(type, param) {
async function show(type, param, { isKeyboardNavigation }) {
let container = document.createElement("div");
container.setAttribute("current-view", type);
if (type == "list") {
await new ListView({ param, root: container }).render();
} else if (type == "detail") {
await new DetailView({ param, root: container }).render();
await new DetailView({
isKeyboardNavigation,
param,
root: container,
}).render();
} else if (type == "discover") {
let discoverView = new DiscoveryView();
let elem = discoverView.render();

View File

@ -746,7 +746,7 @@ var gViewController = {
);
},
loadView(aViewId) {
loadView(aViewId, sourceEvent) {
var isRefresh = false;
if (aViewId == this.currentViewId) {
if (this.isLoading) {
@ -761,9 +761,13 @@ var gViewController = {
isRefresh = true;
}
let isKeyboardNavigation =
sourceEvent &&
sourceEvent.mozInputSource === MouseEvent.MOZ_SOURCE_KEYBOARD;
var state = {
view: aViewId,
previousView: this.currentViewId,
isKeyboardNavigation,
};
if (!isRefresh) {
gHistory.pushState(state);
@ -815,7 +819,7 @@ var gViewController = {
}
},
loadViewInternal(aViewId, aPreviousView, aState) {
loadViewInternal(aViewId, aPreviousView, aState, aEvent) {
var view = this.parseViewId(aViewId);
if (!view.type || !(view.type in this.viewObjects)) {
@ -2186,17 +2190,9 @@ const addonTypes = new Set([
"locale",
]);
const htmlViewOpts = {
loadViewFn(type, ...params) {
let viewId = `addons://${type}`;
if (params.length > 0) {
for (let param of params) {
viewId += "/" + encodeURIComponent(param);
}
} else {
viewId += "/";
}
gViewController.loadView(viewId);
loadViewFn(view, sourceEvent) {
let viewId = `addons://${view}`;
gViewController.loadView(viewId, sourceEvent);
},
replaceWithDefaultViewFn() {
gViewController.replaceView(gViewDefault);
@ -2239,11 +2235,11 @@ function htmlView(type) {
this.node = this._browser.closest("#html-view");
},
async show(param, request, state, refresh) {
async show(param, request, state) {
await htmlBrowserLoaded;
this.node.setAttribute("type", type);
this.node.setAttribute("param", param);
await this._browser.contentWindow.show(type, param);
await this._browser.contentWindow.show(type, param, state);
gViewController.updateCommands();
gViewController.notifyViewChanged();
},

View File

@ -63,14 +63,16 @@ class NamedDeckButton extends HTMLElement {
`;
this.shadowRoot.appendChild(style);
let button = document.createElement("button");
button.appendChild(document.createElement("slot"));
this.shadowRoot.appendChild(button);
this.button = document.createElement("button");
this.button.setAttribute("role", "tab");
this.button.appendChild(document.createElement("slot"));
this.shadowRoot.appendChild(this.button);
this.addEventListener("click", this);
}
connectedCallback() {
this.id = `${this.deckId}-button-${this.name}`;
this.setSelectedFromDeck();
document.addEventListener("view-changed", this, { capture: true });
}
@ -79,6 +81,10 @@ class NamedDeckButton extends HTMLElement {
document.removeEventListener("view-changed", this, { capture: true });
}
focus() {
this.button.focus();
}
get deckId() {
return this.getAttribute("deck");
}
@ -112,6 +118,8 @@ class NamedDeckButton extends HTMLElement {
set selected(val) {
this.toggleAttribute("selected", !!val);
this.button.setAttribute("aria-selected", !!val);
this.button.setAttribute("tabindex", val ? "0" : "-1");
}
setSelectedFromDeck() {
@ -121,6 +129,69 @@ class NamedDeckButton extends HTMLElement {
}
customElements.define("named-deck-button", NamedDeckButton);
class NamedDeckButtonGroup extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
let style = document.createElement("style");
style.textContent = `
div {
border-bottom: 1px solid var(--in-content-box-border-color);
border-top: 1px solid var(--in-content-box-border-color);
font-size: 0;
line-height: 0;
}
`;
this.shadowRoot.appendChild(style);
let container = document.createElement("div");
container.setAttribute("role", "tablist");
container.appendChild(document.createElement("slot"));
this.shadowRoot.appendChild(container);
this.addEventListener("keydown", this);
}
handleEvent(e) {
if (
e.type === "keydown" &&
e.target.localName === "named-deck-button" &&
["ArrowLeft", "ArrowRight"].includes(e.key)
) {
let previousDirectionKey =
document.dir === "rtl" ? "ArrowRight" : "ArrowLeft";
this.walker.currentNode = e.target;
let nextItem =
e.key === previousDirectionKey
? this.walker.previousNode()
: this.walker.nextNode();
if (nextItem) {
nextItem.focus();
}
}
}
get walker() {
if (!this._walker) {
this._walker = document.createTreeWalker(this, NodeFilter.SHOW_ELEMENT, {
acceptNode: node => {
if (
node.hidden ||
node.disabled ||
node.localName !== "named-deck-button"
) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
}
return this._walker;
}
}
customElements.define("named-deck-button-group", NamedDeckButtonGroup);
/**
* A deck that is indexed by the "name" attribute of its children. The
* <named-deck-button> element is a companion element that can update its state
@ -210,7 +281,11 @@ class NamedDeck extends HTMLElement {
_setSelectedViewAttributes() {
let { selectedViewName } = this;
for (let view of this.children) {
if (view.getAttribute("name") == selectedViewName) {
let name = view.getAttribute("name");
view.setAttribute("aria-labelledby", `${this.id}-button-${name}`);
view.setAttribute("role", "tabpanel");
if (name === selectedViewName) {
view.slot = "selected";
} else {
view.slot = "";

View File

@ -87,18 +87,20 @@ function checkOptions(doc, options, expectedOptions) {
function assertDeckHeadingHidden(group) {
ok(group.hidden, "The tab group is hidden");
for (let button of group.children) {
let buttons = group.querySelectorAll("named-deck-button");
for (let button of buttons) {
ok(button.offsetHeight == 0, `The ${button.name} is hidden`);
}
}
function assertDeckHeadingButtons(group, visibleButtons) {
ok(!group.hidden, "The tab group is shown");
let buttons = group.querySelectorAll("named-deck-button");
ok(
group.children.length >= visibleButtons.length,
buttons.length >= visibleButtons.length,
`There should be at least ${visibleButtons.length} buttons`
);
for (let button of group.children) {
for (let button of buttons) {
if (visibleButtons.includes(button.name)) {
ok(!button.hidden, `The ${button.name} is shown`);
} else {

View File

@ -399,7 +399,7 @@ add_task(async function testReleaseNotesLoad() {
info("Switch away and back to release notes");
// Load details view.
let detailsBtn = tabGroup.firstElementChild;
let detailsBtn = tabGroup.querySelector('named-deck-button[name="details"]');
let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
detailsBtn.click();
await viewChanged;
@ -506,7 +506,7 @@ add_task(async function testReleaseNotesError() {
info("Switch away and back to release notes");
// Load details view.
let detailsBtn = tabGroup.firstElementChild;
let detailsBtn = tabGroup.querySelector('named-deck-button[name="details"]');
let viewChanged = BrowserTestUtils.waitForEvent(deck, "view-changed");
detailsBtn.click();
await viewChanged;