Commit 739d322b authored by Mark Striemer's avatar Mark Striemer
Browse files

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

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

--HG--
extra : rebase_source : cdaf8ac3f9b05a40562df2aa8dcc12deb4aac5dd
extra : source : d9333a0ddf9395ddb8ea66d48e04155aca90eb69
extra : histedit_source : c851be8e1ff22f09e55bfdd9b5aff2de9c5d7c82
parent 89341e1b
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -208,11 +208,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">
+96 −5
Original line number Diff line number Diff line
@@ -532,6 +532,7 @@ class PanelList extends HTMLElement {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot.appendChild(importTemplate("panel-list"));
    this.setAttribute("role", "menu");
  }

  attributeChangedCallback(name, oldVal, newVal) {
@@ -627,14 +628,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.
@@ -649,6 +648,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);
@@ -677,6 +677,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
@@ -701,9 +748,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() {
@@ -723,6 +805,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() {
@@ -740,6 +823,10 @@ class PanelItem extends HTMLElement {
  set checked(val) {
    this.toggleAttribute("checked", val);
  }

  focus() {
    this.button.focus();
  }
}
customElements.define("panel-item", PanelItem);

@@ -1764,12 +1851,16 @@ class AddonCard extends HTMLElement {
    this.addEventListener("change", this);
    this.addEventListener("click", this);
    this.addEventListener("mousedown", this);
    this.panel.addEventListener("shown", this);
    this.panel.addEventListener("hidden", this);
  }

  removeListeners() {
    this.removeEventListener("change", this);
    this.removeEventListener("click", this);
    this.removeEventListener("mousedown", this);
    this.panel.removeEventListener("shown", this);
    this.panel.removeEventListener("hidden", this);
  }

  onNewInstall(install) {
+16 −20
Original line number Diff line number Diff line
@@ -388,34 +388,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");
  let shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
  space();
  await shown;
  is(moreOptionsMenu.open, true, "The menu is open");
  space();
  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");

  // Test tabbing out of the menu.
  space();
  shown = BrowserTestUtils.waitForEvent(moreOptionsMenu, "shown");
  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({ shiftKey: true });
  is(moreOptionsMenu.open, true, "The menu stays open");
  isFocused(expandButton, "The focus has looped to the bottom");
  tab();
  is(moreOptionsMenu.open, false, "Tabbing out of the menu closes it");
  is(moreOptionsMenu.open, true, "The menu stays open");
  isFocused(toggleDisableButton, "The focus has looped to the top");

  // Focus the button again, focus may have moved out of the browser.
  moreOptionsButton.focus();
  isFocused(moreOptionsButton, "The button is focused again");
  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");
@@ -425,8 +424,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");
@@ -447,7 +444,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");