Bug 1543667 - adding a11y checks toolbar to a11y panel. r=nchevobbe
authorYura Zenevich <yura.zenevich@gmail.com>
Tue, 16 Apr 2019 14:53:32 +0000
changeset 469726 75957624201b
parent 469725 cfc3b3981766
child 469727 d0943c0f7615
push id35880
push usercbrindusan@mozilla.com
push dateWed, 17 Apr 2019 09:36:19 +0000
treeherdermozilla-central@79e6ed0b08d6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1543667
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1543667 - adding a11y checks toolbar to a11y panel. r=nchevobbe Differential Revision: https://phabricator.services.mozilla.com/D27228
devtools/client/accessibility/accessibility.css
devtools/client/accessibility/actions/audit.js
devtools/client/accessibility/components/AccessibilityTreeFilter.js
devtools/client/accessibility/components/Badge.js
devtools/client/accessibility/components/Button.js
devtools/client/accessibility/components/Toolbar.js
devtools/client/accessibility/components/moz.build
devtools/client/accessibility/constants.js
devtools/client/accessibility/reducers/audit.js
devtools/client/accessibility/test/browser/browser.ini
devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_toolbar.js
devtools/client/accessibility/test/browser/head.js
devtools/client/accessibility/test/jest/components/__snapshots__/accessibility-tree-filter.test.js.snap
devtools/client/accessibility/test/jest/components/accessibility-tree-filter.test.js
devtools/client/locales/en-US/accessibility.properties
--- a/devtools/client/accessibility/accessibility.css
+++ b/devtools/client/accessibility/accessibility.css
@@ -20,16 +20,18 @@
   --accessibility-link-color-active: var(--blue-70);
   --badge-active-background-color: var(--blue-50);
   --badge-active-border-color: #FFFFFFB3;
   --badge-interactive-background-color: var(--grey-20);
   --badge-interactive-color: var(--grey-90);
   --accessible-label-background-color: white;
   --accessible-label-border-color: #CACAD1;
   --accessible-label-color: var(--grey-60);
+  /* Similarly to webconsole, add more padding before the toolbar group. */
+  --separator-inline-margin: 5px;
 }
 
 :root.theme-dark {
   --accessibility-unfocused-tree-focused-node-background: var(--grey-70);
   --accessibility-unfocused-tree-focused-node-twisty-fill: var(--theme-selection-color);
   --accessibility-link-color: var(--theme-highlight-blue);
   --accessibility-link-color-active: var(--blue-40);
   --badge-active-background-color: var(--blue-60);
@@ -74,16 +76,29 @@ body {
   overflow: auto;
 }
 
 .mainFrame {
   height: 100%;
   color: var(--theme-toolbar-color);
 }
 
+.devtools-button,
+.toggle-button {
+  cursor: pointer;
+}
+
+.mainFrame .devtools-button.devtools-throbber::before,
+.mainFrame .toggle-button.devtools-throbber::before {
+  /* Default for .devtools-throbber is set to 1em which is too big for the
+     devtools toolbar. */
+  height: 9px;
+  width: 9px;
+}
+
 .split-box.horz {
   height: calc(100vh - var(--accessibility-toolbar-height));
 }
 
 .mainFrame .devtools-button,
 .description .devtools-button {
   padding: unset;
 }
@@ -109,16 +124,18 @@ body {
   box-shadow: 0 0 0 2px var(--accessibility-toolbar-focus-alpha30);
   border-radius: 2px;
   -moz-outline-radius: 2px;
 }
 
 .devtools-toolbar {
   display: flex;
   align-items: center;
+  font: message-box;
+  font-size: var(--accessibility-font-size);
 }
 
 .devtools-toolbar .help {
   cursor: pointer;
   width: 18px;
   margin-inline-start: auto;
   margin-inline-end: 3px;
   background: transparent;
@@ -304,38 +321,64 @@ body {
   border-bottom: 1px solid var(--theme-splitter-color);
   background: var(--theme-toolbar-background);
   font: message-box;
   font-size: var(--accessibility-font-size);
   height: var(--accessibility-toolbar-height);
   color: var(--theme-toolbar-color);
 }
 
+.badge.toggle-button,
 .mainFrame .treeTable .treeRow .badges .badge {
   background-color: var(--badge-interactive-background-color);
   color: var(--badge-interactive-color);
-  border: 1px solid var(--accessible-label-border-color);
   border-radius: 3px;
   padding: 0px 2px;
   margin-inline-start: 5px;
 }
 
+.badge.toggle-button {
+  border: 1px solid transparent;
+  color: var(--theme-body-color);
+}
+
+.devtools-toolbar .badge.toggle-button:focus {
+  outline: 2px solid var(--accessibility-toolbar-focus);
+  outline-offset: -2px;
+  box-shadow: 0 0 0 2px var(--accessibility-toolbar-focus-alpha30);
+  -moz-outline-radius: 2px;
+}
+
+.mainFrame .treeTable .treeRow .badges .badge {
+  border: 1px solid var(--accessible-label-border-color);
+}
+
 .mainFrame .treeTable:focus .treeRow.selected .badges .badge {
   background-color: var(--badge-interactive-background-color);
-  border-color: var(--accessible-label-border-color);
+  border: 1px solid var(--accessible-label-border-color);
   color: var(--badge-interactive-color);
 }
 
+.badge.toggle-button.checked,
 .mainFrame .treeTable:focus .treeRow.selected .badges .badge.checked,
 .mainFrame .treeTable .treeRow .badges .badge.checked {
   background-color: var(--badge-active-background-color);
-  border-color: var(--badge-active-border-color);
   color: var(--theme-selection-color);
 }
 
+.mainFrame .treeTable .treeRow .badges .badge.checked {
+  border: 1px solid var(--badge-active-border-color);
+}
+
+/* Avoid having a default dotted border on keyboard focus since we provide focus
+   styling*/
+.badge.toggle-button::-moz-focus-inner {
+  border: none;
+}
+
 /* Right Sidebar */
 .right-sidebar {
   display: flex;
   flex-direction: column;
   flex: 1;
   white-space: nowrap;
   font: message-box;
   font-size: var(--accessibility-font-size);
--- a/devtools/client/accessibility/actions/audit.js
+++ b/devtools/client/accessibility/actions/audit.js
@@ -1,15 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { AUDIT, FILTER_TOGGLE } = require("../constants");
+const { AUDIT, AUDITING, FILTER_TOGGLE } = require("../constants");
 
 exports.filterToggle = filter =>
   dispatch => dispatch({ filter, type: FILTER_TOGGLE });
 
-exports.audit = walker =>
+exports.auditing = filter =>
+  dispatch => dispatch({ auditing: filter, type: AUDITING });
+
+exports.audit = (walker, filter) =>
   dispatch => walker.audit()
     .then(response => dispatch({ type: AUDIT, response }))
     .catch(error => dispatch({ type: AUDIT, error }));
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/components/AccessibilityTreeFilter.js
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// React
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
+const { div } = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { L10N } = require("../utils/l10n");
+const ToggleButton = createFactory(require("./Button").ToggleButton);
+
+const actions = require("../actions/audit");
+
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { FILTERS } = require("../constants");
+
+const FILTER_LABELS = {
+  [FILTERS.CONTRAST]: "accessibility.badge.contrast",
+};
+
+class AccessibilityTreeFilter extends Component {
+  static get propTypes() {
+    return {
+      auditing: PropTypes.string.isRequired,
+      filters: PropTypes.object.isRequired,
+      dispatch: PropTypes.func.isRequired,
+      walker: PropTypes.object.isRequired,
+    };
+  }
+
+  async toggleFilter(filterKey) {
+    const { dispatch, filters, walker } = this.props;
+
+    if (!filters[filterKey]) {
+      dispatch(actions.auditing(filterKey));
+      await dispatch(actions.audit(walker, filterKey));
+    }
+
+    // We wait to dispatch filter toggle until the tree is ready to be filtered
+    // right after the audit. This is to make sure that we render an empty tree
+    // (filtered) while the audit is running.
+    dispatch(actions.filterToggle(filterKey));
+  }
+
+  onClick(filterKey, e) {
+    const { mozInputSource, MOZ_SOURCE_KEYBOARD } = e.nativeEvent;
+    if (e.isTrusted && mozInputSource === MOZ_SOURCE_KEYBOARD) {
+      // Already handled by key down handler on user input.
+      return;
+    }
+
+    this.toggleFilter(filterKey);
+  }
+
+  onKeyDown(filterKey, e) {
+    // We explicitely handle "click" and "keydown" events this way here because
+    // of the expectation of both Space and Enter triggering the click event
+    // even though Enter is the only one in the spec.
+    if (![" ", "Enter"].includes(e.key)) {
+      return;
+    }
+
+    this.toggleFilter(filterKey);
+  }
+
+  render() {
+    const { auditing, filters } = this.props;
+    const filterButtons = Object.entries(filters).map(([filterKey, active]) =>
+      ToggleButton({
+        className: "audit-badge badge",
+        key: filterKey,
+        active,
+        label: L10N.getStr(FILTER_LABELS[filterKey]),
+        onClick: this.onClick.bind(this, filterKey),
+        onKeyDown: this.onKeyDown.bind(this, filterKey),
+        busy: auditing === filterKey,
+      }));
+
+    return div({
+      role: "toolbar",
+    },
+      L10N.getStr("accessibility.tree.filters"),
+      ...filterButtons);
+  }
+}
+
+const mapStateToProps = ({ audit: { filters, auditing } }) => {
+  return { filters, auditing };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(AccessibilityTreeFilter);
--- a/devtools/client/accessibility/components/Badge.js
+++ b/devtools/client/accessibility/components/Badge.js
@@ -5,17 +5,17 @@
 
 // React
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const ToggleButton = createFactory(require("./Button").ToggleButton);
 
-const { audit, filterToggle } = require("../actions/audit");
+const { audit, auditing, filterToggle } = require("../actions/audit");
 const { preventDefaultAndStopPropagation } = require("devtools/client/shared/events");
 
 class Badge extends Component {
   static get propTypes() {
     return {
       active: PropTypes.bool.isRequired,
       filterKey: PropTypes.string.isRequired,
       dispatch: PropTypes.func.isRequired,
@@ -32,23 +32,27 @@ class Badge extends Component {
     this.onClick = this.onClick.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
   }
 
   shouldComponentUpdate(nextProps) {
     return nextProps.active !== this.props.active;
   }
 
-  toggleFilter() {
+  async toggleFilter() {
     const { dispatch, filterKey, walker, active } = this.props;
-    dispatch(filterToggle(filterKey));
+    if (!active) {
+      dispatch(auditing(filterKey));
+      await dispatch(audit(walker, filterKey));
+    }
 
-    if (!active) {
-      dispatch(audit(walker));
-    }
+    // We wait to dispatch filter toggle until the tree is ready to be filtered
+    // right after the audit. This is to make sure that we render an empty tree
+    // (filtered) while the audit is running.
+    dispatch(filterToggle(filterKey));
   }
 
   onClick(e) {
     preventDefaultAndStopPropagation(e);
     const { mozInputSource, MOZ_SOURCE_KEYBOARD } = e.nativeEvent;
     if (e.isTrusted && mozInputSource === MOZ_SOURCE_KEYBOARD) {
       // Already handled by key down handler on user input.
       return;
--- a/devtools/client/accessibility/components/Button.js
+++ b/devtools/client/accessibility/components/Button.js
@@ -56,29 +56,43 @@ class Button extends Component {
     return (button(props, span({
       className: classList.join(" "),
       tabIndex: -1,
     }, this.props.children)));
   }
 }
 
 function ToggleButton(props) {
-  const { active, disabled, label, className, onClick, onKeyDown, tooltip } = props;
+  const {
+    active,
+    busy,
+    disabled,
+    label,
+    className,
+    onClick,
+    onKeyDown,
+    tooltip,
+  } = props;
   const classList = [
     ...className.split(" "),
     "toggle-button",
   ];
 
   if (active) {
     classList.push("checked");
   }
 
+  if (busy) {
+    classList.push("devtools-throbber");
+  }
+
   return button({
     disabled,
     "aria-pressed": active === true,
+    "aria-busy": busy,
     className: classList.join(" "),
     onClick,
     onKeyDown,
     title: tooltip,
   }, label);
 }
 
 module.exports = {
--- a/devtools/client/accessibility/components/Toolbar.js
+++ b/devtools/client/accessibility/components/Toolbar.js
@@ -4,26 +4,28 @@
 "use strict";
 
 // React
 const { createFactory, Component } = require("devtools/client/shared/vendor/react");
 const { div } = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { L10N } = require("../utils/l10n");
 const Button = createFactory(require("./Button").Button);
+const AccessibilityTreeFilter = createFactory(require("./AccessibilityTreeFilter"));
 
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { disable, updateCanBeDisabled } = require("../actions/ui");
 
 const { A11Y_LEARN_MORE_LINK } = require("../constants");
 const { openDocLink } = require("devtools/client/shared/link");
 
 class Toolbar extends Component {
   static get propTypes() {
     return {
+      walker: PropTypes.object.isRequired,
       dispatch: PropTypes.func.isRequired,
       accessibility: PropTypes.object.isRequired,
       canBeDisabled: PropTypes.bool.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
@@ -60,17 +62,17 @@ class Toolbar extends Component {
   }
 
   onLearnMoreClick() {
     openDocLink(A11Y_LEARN_MORE_LINK +
       "?utm_source=devtools&utm_medium=a11y-panel-toolbar");
   }
 
   render() {
-    const { canBeDisabled } = this.props;
+    const { canBeDisabled, walker } = this.props;
     const { disabling } = this.state;
     const disableButtonStr = disabling ?
       "accessibility.disabling" : "accessibility.disable";
     let title;
     let isDisabled = false;
 
     if (canBeDisabled) {
       title = L10N.getStr("accessibility.disable.enabledTitle");
@@ -86,21 +88,26 @@ class Toolbar extends Component {
       }, Button({
         className: "disable",
         id: "accessibility-disable-button",
         onClick: this.onDisable,
         disabled: disabling || isDisabled,
         busy: disabling,
         title,
       }, L10N.getStr(disableButtonStr)),
-      Button({
-        className: "help",
-        title: L10N.getStr("accessibility.learnMore"),
-        onClick: this.onLearnMoreClick,
-      }))
+        div({
+          role: "separator",
+          className: "devtools-separator",
+        }),
+        AccessibilityTreeFilter({ walker }),
+        Button({
+          className: "help",
+          title: L10N.getStr("accessibility.learnMore"),
+          onClick: this.onLearnMoreClick,
+        }))
     );
   }
 }
 
 const mapStateToProps = ({ ui }) => ({
   canBeDisabled: ui.canBeDisabled,
 });
 
--- a/devtools/client/accessibility/components/moz.build
+++ b/devtools/client/accessibility/components/moz.build
@@ -1,16 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'AccessibilityRow.js',
     'AccessibilityRowValue.js',
     'AccessibilityTree.js',
+    'AccessibilityTreeFilter.js',
     'Accessible.js',
     'AuditController.js',
     'AuditFilter.js',
     'Badge.js',
     'Badges.js',
     'Button.js',
     'Checks.js',
     'ColorContrastAccessibility.js',
--- a/devtools/client/accessibility/constants.js
+++ b/devtools/client/accessibility/constants.js
@@ -28,16 +28,17 @@ exports.SELECT = "SELECT";
 exports.HIGHLIGHT = "HIGHLIGHT";
 exports.UNHIGHLIGHT = "UNHIGHLIGHT";
 exports.ENABLE = "ENABLE";
 exports.DISABLE = "DISABLE";
 exports.UPDATE_CAN_BE_DISABLED = "UPDATE_CAN_BE_DISABLED";
 exports.UPDATE_CAN_BE_ENABLED = "UPDATE_CAN_BE_ENABLED";
 exports.FILTER_TOGGLE = "FILTER_TOGGLE";
 exports.AUDIT = "AUDIT";
+exports.AUDITING = "AUDITING";
 
 // List of filters for accessibility checks.
 exports.FILTERS = {
   [AUDIT_TYPE.CONTRAST]: "CONTRAST",
 };
 
 // Ordered accessible properties to be displayed by the accessible component.
 exports.ORDERED_PROPS = [
--- a/devtools/client/accessibility/reducers/audit.js
+++ b/devtools/client/accessibility/reducers/audit.js
@@ -1,23 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-const { FILTER_TOGGLE, FILTERS, RESET } = require("../constants");
+const {
+  AUDIT,
+  AUDITING,
+  FILTER_TOGGLE,
+  FILTERS,
+  RESET,
+} = require("../constants");
 
 /**
  * Initial state definition
  */
 function getInitialState() {
   return {
     filters: {
       [FILTERS.CONTRAST]: false,
     },
+    auditing: null,
   };
 }
 
 function audit(state = getInitialState(), action) {
   switch (action.type) {
     case FILTER_TOGGLE:
       const { filter } = action;
       let { filters } = state;
@@ -26,16 +33,28 @@ function audit(state = getInitialState()
         ...filters,
         [filter]: active,
       };
 
       return {
         ...state,
         filters,
       };
+    case AUDITING:
+      const { auditing } = action;
+
+      return {
+        ...state,
+        auditing,
+      };
+    case AUDIT:
+      return {
+        ...state,
+        auditing: null,
+      };
     case RESET:
       return getInitialState();
     default:
       return state;
   }
 }
 
 exports.audit = audit;
--- a/devtools/client/accessibility/test/browser/browser.ini
+++ b/devtools/client/accessibility/test/browser/browser.ini
@@ -19,12 +19,13 @@ skip-if = (os == 'win' && processor == '
 skip-if = (os == 'win' && processor == 'aarch64') # bug 1533534
 [browser_accessibility_panel_highlighter.js]
 [browser_accessibility_panel_highlighter_multi_tab.js]
 skip-if = (os == 'linux' && debug && bits == 64) # Bug 1511247
 [browser_accessibility_relation_navigation.js]
 [browser_accessibility_reload.js]
 [browser_accessibility_sidebar_checks.js]
 [browser_accessibility_sidebar.js]
+[browser_accessibility_tree_audit_toolbar.js]
 [browser_accessibility_tree_audit.js]
 [browser_accessibility_tree_contrast.js]
 [browser_accessibility_tree_nagivation.js]
 [browser_accessibility_tree.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_toolbar.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global toggleRow, toggleBadge, toggleFilter */
+
+const TEST_URI = `<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Accessibility Panel Test</title>
+  </head>
+  <body>
+    <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);">
+      Top level header
+    </h1>
+    <h2 style="color:rgba(0,255,0,0.1); background-color:rgba(255,255,255,1);">
+      Second level header
+    </h2>
+  </body>
+</html>`;
+
+/**
+ * Test data has the format of:
+ * {
+ *   desc     {String}    description for better logging
+ *   setup    {Function}  An optional setup that needs to be performed before
+ *                        the state of the tree and the sidebar can be checked.
+ *   expected {JSON}      An expected states for the tree and the sidebar.
+ * }
+ */
+const tests = [{
+  desc: "Check initial state.",
+  expected: {
+    tree: [{
+      role: "document",
+      name: `"Accessibility Panel Test"`,
+    }],
+  },
+}, {
+  desc: "Run an audit from a11y panel toolbar by activating a filter.",
+  setup: async ({ doc }) => {
+    await toggleFilter(doc, 0);
+  },
+  expected: {
+    tree: [{
+      role: "text leaf",
+      name: `"Top level header "contrast`,
+      badges: [ "contrast" ],
+    }, {
+      role: "text leaf",
+      name: `"Second level header "contrast`,
+      badges: [ "contrast" ],
+    }],
+  },
+}, {
+  desc: "Click on the filter again.",
+  setup: async ({ doc }) => {
+    await toggleBadge(doc, 0, 0);
+  },
+  expected: {
+    tree: [{
+      role: "document",
+      name: `"Accessibility Panel Test"`,
+    }, {
+      role: "heading",
+      name: `"Top level header"`,
+    }, {
+      role: "text leaf",
+      name: `"Top level header "contrast`,
+      badges: [ "contrast" ],
+    }, {
+      role: "heading",
+      name: `"Second level header"`,
+    }, {
+      role: "text leaf",
+      name: `"Second level header "contrast`,
+      badges: [ "contrast" ],
+    }],
+  },
+}];
+
+/**
+ * Simple test that checks content of the Accessibility panel tree when the
+ * audit is activated via the panel's toolbar.
+ */
+addA11yPanelTestsTask(tests, TEST_URI,
+  "Test Accessibility panel tree with contrast filter audit activation.");
--- a/devtools/client/accessibility/test/browser/head.js
+++ b/devtools/client/accessibility/test/browser/head.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* import-globals-from ../../../shared/test/shared-head.js */
 /* import-globals-from ../../../inspector/test/shared-head.js */
 
 /* global waitUntilState, gBrowser */
 /* exported addTestTab, checkTreeState, checkSidebarState, checkAuditState, selectRow,
-            toggleRow, toggleBadge, addA11yPanelTestsTask, reload, navigate */
+            toggleRow, toggleBadge, toggleFilter, addA11yPanelTestsTask, reload,
+            navigate */
 
 "use strict";
 
 // Import framework's shared head.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
   this);
 
@@ -388,16 +389,32 @@ async function toggleBadge(doc, rowNumbe
   const expected = !badge.classList.contains("checked");
 
   EventUtils.synthesizeMouseAtCenter(badge, {}, win);
   await BrowserTestUtils.waitForCondition(() =>
     expected === badge.classList.contains("checked"), "Badge updated.");
 }
 
 /**
+ * Toggle an accessibility audit filter based on its index in the toolbar.
+ * @param  {document} doc         panel documnent.
+ * @param  {Number}   filterIndex index of the filter to be toggled.
+ */
+async function toggleFilter(doc, filterIndex) {
+  const win = doc.defaultView;
+  const filter = doc.querySelectorAll(
+    ".devtools-toolbar .badge.toggle-button")[filterIndex];
+  const expected = !filter.classList.contains("checked");
+
+  EventUtils.synthesizeMouseAtCenter(filter, {}, win);
+  await BrowserTestUtils.waitForCondition(() =>
+    expected === filter.classList.contains("checked"), "Filter updated.");
+}
+
+/**
  * Iterate over setups/tests structure and test the state of the
  * accessibility panel.
  * @param  {JSON}   tests test data that has the format of:
  *                    {
  *                      desc     {String}    description for better logging
  *                      setup    {Function}  An optional setup that needs to be
  *                                           performed before the state of the
  *                                           tree and the sidebar can be checked
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/accessibility-tree-filter.test.js.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AccessibilityTreeFilter component: audit filter filtered 1`] = `"<div role=\\"toolbar\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button checked\\">accessibility.badge.contrast</button></div>"`;
+
+exports[`AccessibilityTreeFilter component: audit filter filtered auditing 1`] = `"<div role=\\"toolbar\\">accessibility.tree.filters<button aria-pressed=\\"true\\" aria-busy=\\"true\\" class=\\"audit-badge badge toggle-button checked devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
+
+exports[`AccessibilityTreeFilter component: audit filter not filtered 1`] = `"<div role=\\"toolbar\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
+
+exports[`AccessibilityTreeFilter component: audit filter not filtered auditing 1`] = `"<div role=\\"toolbar\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"true\\" class=\\"audit-badge badge toggle-button devtools-throbber\\">accessibility.badge.contrast</button></div>"`;
+
+exports[`AccessibilityTreeFilter component: toggle filter 1`] = `"<div role=\\"toolbar\\">accessibility.tree.filters<button aria-pressed=\\"false\\" aria-busy=\\"false\\" class=\\"audit-badge badge toggle-button\\">accessibility.badge.contrast</button></div>"`;
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/jest/components/accessibility-tree-filter.test.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { mount } = require("enzyme");
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+
+const { ToggleButton } = require("devtools/client/accessibility/components/Button");
+const ConnectedAccessibilityTreeFilterClass =
+  require("devtools/client/accessibility/components/AccessibilityTreeFilter");
+const AccessibilityTreeFilterClass =
+  ConnectedAccessibilityTreeFilterClass.WrappedComponent;
+const AccessibilityTreeFilter = createFactory(ConnectedAccessibilityTreeFilterClass);
+const {
+  setupStore,
+} = require("devtools/client/accessibility/test/jest/helpers");
+const { FILTERS } = require("devtools/client/accessibility/constants");
+
+describe("AccessibilityTreeFilter component:", () => {
+  it("audit filter not filtered", () => {
+    const store = setupStore();
+
+    const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const filters = wrapper.find(AccessibilityTreeFilterClass);
+    expect(filters.children().length).toBe(1);
+
+    const toolbar = filters.childAt(0);
+    expect(toolbar.is("div")).toBe(true);
+    expect(toolbar.prop("role")).toBe("toolbar");
+
+    const filterButtons = filters.find(ToggleButton);
+    expect(filterButtons.length).toBe(1);
+
+    const button = filterButtons.at(0).childAt(0);
+    expect(button.is("button")).toBe(true);
+    expect(button.hasClass("audit-badge")).toBe(true);
+    expect(button.hasClass("badge")).toBe(true);
+    expect(button.hasClass("toggle-button")).toBe(true);
+    expect(button.prop("aria-pressed")).toBe(false);
+    expect(button.text()).toBe("accessibility.badge.contrast");
+  });
+
+  it("audit filter filtered", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
+    });
+
+    const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const button = wrapper.find("button");
+    expect(button.prop("aria-pressed")).toBe(true);
+    expect(button.hasClass("checked")).toBe(true);
+  });
+
+  it("audit filter not filtered auditing", () => {
+    const store = setupStore({
+      preloadedState: { audit: {
+        filters: {
+          [FILTERS.CONTRAST]: false,
+        },
+        auditing: FILTERS.CONTRAST,
+      }},
+    });
+
+    const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const button = wrapper.find("button");
+    expect(button.prop("aria-pressed")).toBe(false);
+    expect(button.hasClass("checked")).toBe(false);
+    expect(button.prop("aria-busy")).toBe(true);
+    expect(button.hasClass("devtools-throbber")).toBe(true);
+  });
+
+  it("audit filter filtered auditing", () => {
+    const store = setupStore({
+      preloadedState: { audit: {
+        filters: {
+          [FILTERS.CONTRAST]: true,
+        },
+        auditing: FILTERS.CONTRAST,
+      }},
+    });
+
+    const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const button = wrapper.find("button");
+    expect(button.prop("aria-pressed")).toBe(true);
+    expect(button.hasClass("checked")).toBe(true);
+    expect(button.prop("aria-busy")).toBe(true);
+    expect(button.hasClass("devtools-throbber")).toBe(true);
+  });
+
+  it("toggle filter", () => {
+    const store = setupStore();
+    const wrapper = mount(Provider({store}, AccessibilityTreeFilter()));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const filterInstance = wrapper.find(AccessibilityTreeFilterClass).instance();
+    filterInstance.toggleFilter = jest.fn();
+    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: " " });
+    expect(filterInstance.toggleFilter.mock.calls.length).toBe(1);
+
+    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: "Enter" });
+    expect(filterInstance.toggleFilter.mock.calls.length).toBe(2);
+
+    wrapper.find("button.audit-badge.badge").simulate("click", { clientX: 1 });
+    expect(filterInstance.toggleFilter.mock.calls.length).toBe(3);
+  });
+});
--- a/devtools/client/locales/en-US/accessibility.properties
+++ b/devtools/client/locales/en-US/accessibility.properties
@@ -163,8 +163,13 @@ accessibility.badges=Accessibility check
 # contrast.
 accessibility.badge.contrast=contrast
 
 # LOCALIZATION NOTE (accessibility.badge.contrast.tooltip): A title text for the
 # badge tooltip that is rendered on mouse hover over the badge in the accessible
 # row in the accessibility tree for a given accessible object that does not
 # satisfy the WCAG guideline for colour contrast.
 accessibility.badge.contrast.tooltip=Does not meet WCAG standards for accessible text.
+
+# LOCALIZATION NOTE (accessibility.tree.filters): A title text for the toolbar
+# within the main accessibility panel that contains a list of filters to be for
+# accessibility audit.
+accessibility.tree.filters=Check for issues: