Commit 11da9e36 authored by Mark Striemer's avatar Mark Striemer
Browse files

Bug 1567600 - Part 3: Better accessibility for named-deck and about:addons...

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

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

--HG--
extra : rebase_source : 275cafcc49c9c314a5f5c170afda295c0e9c24f7
extra : source : 4bc220f957a7db41e3e0ee2b5a48fbe7f424c911
extra : histedit_source : 1a8bc2d7bba9cf83792ac77ff6b8f689f1ee30c2
parent 739d322b
Loading
Loading
Loading
Loading
+0 −4
Original line number Diff line number Diff line
@@ -420,11 +420,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;
}
+6 −4
Original line number Diff line number Diff line
@@ -106,10 +106,12 @@

    <template name="addon-details">
      <div class="deck-tab-group">
        <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">
+27 −11
Original line number Diff line number Diff line
@@ -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",
@@ -1414,7 +1415,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;
    });

@@ -1724,7 +1726,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":
@@ -1751,7 +1753,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.
@@ -1780,7 +1782,7 @@ class AddonCard extends HTMLElement {
        default:
          // Handle a click on the card itself.
          if (!this.expanded) {
            loadViewFn("detail", this.addon.id);
            loadViewFn(`detail/${this.addon.id}`, e);
          } else if (
            e.target.localName == "a" &&
            e.target.getAttribute("data-telemetry-name")
@@ -2036,11 +2038,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");
@@ -2077,6 +2083,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;
  }
@@ -2251,7 +2261,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]")) {
@@ -3018,11 +3028,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() {
@@ -3039,10 +3050,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" &&
@@ -3148,13 +3160,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();
+11 −15
Original line number Diff line number Diff line
@@ -922,7 +922,7 @@ var gViewController = {
    );
  },

  loadView(aViewId) {
  loadView(aViewId, sourceEvent) {
    var isRefresh = false;
    if (aViewId == this.currentViewId) {
      if (this.isLoading) {
@@ -937,9 +937,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);
@@ -992,7 +996,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)) {
@@ -4391,17 +4395,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);
@@ -4444,11 +4440,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();
    },
+79 −4
Original line number Diff line number Diff line
@@ -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 = "";
Loading