mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-03-01 05:48:26 +00:00
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:
parent
c19ef4049e
commit
056f2d5ae8
@ -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;
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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 = "";
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user