Bug 1539231 - making audit badges interactive. r=nchevobbe
authorYura Zenevich <yura.zenevich@gmail.com>
Tue, 16 Apr 2019 14:53:15 +0000
changeset 469725 cfc3b3981766
parent 469724 dfe99ccd41f0
child 469726 75957624201b
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
bugs1539231
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 1539231 - making audit badges interactive. r=nchevobbe Differential Revision: https://phabricator.services.mozilla.com/D26850
devtools/client/accessibility/accessibility.css
devtools/client/accessibility/actions/audit.js
devtools/client/accessibility/actions/moz.build
devtools/client/accessibility/components/AccessibilityRow.js
devtools/client/accessibility/components/AccessibilityRowValue.js
devtools/client/accessibility/components/AccessibilityTree.js
devtools/client/accessibility/components/AuditFilter.js
devtools/client/accessibility/components/Badge.js
devtools/client/accessibility/components/Badges.js
devtools/client/accessibility/components/ContrastBadge.js
devtools/client/accessibility/components/moz.build
devtools/client/accessibility/constants.js
devtools/client/accessibility/reducers/accessibles.js
devtools/client/accessibility/reducers/audit.js
devtools/client/accessibility/reducers/index.js
devtools/client/accessibility/reducers/moz.build
devtools/client/accessibility/reducers/ui.js
devtools/client/accessibility/test/browser/browser.ini
devtools/client/accessibility/test/browser/browser_accessibility_tree_audit.js
devtools/client/accessibility/test/browser/head.js
devtools/client/accessibility/test/jest/components/__snapshots__/audit-filter.test.js.snap
devtools/client/accessibility/test/jest/components/__snapshots__/badge.test.js.snap
devtools/client/accessibility/test/jest/components/audit-filter.test.js
devtools/client/accessibility/test/jest/components/badge.test.js
devtools/client/accessibility/test/jest/components/badges.test.js
devtools/client/accessibility/test/jest/components/contrast-badge.test.js
devtools/client/accessibility/test/mochitest/test_accessible_row_context_menu.html
devtools/client/accessibility/utils/audit.js
devtools/client/accessibility/utils/moz.build
devtools/client/shared/components/tree/LabelCell.js
devtools/client/shared/components/tree/TreeView.css
--- a/devtools/client/accessibility/accessibility.css
+++ b/devtools/client/accessibility/accessibility.css
@@ -228,16 +228,27 @@ body {
 .split-box.horz .treeTable {
   width: 100%;
 }
 
 .treeTable .treeRow.highlighted:not(.selected) {
   background-color: var(--theme-selection-background-hover);
 }
 
+.treeTable.filtered .treeRow .treeLabelCell {
+  /* Unset row indentation when the tree is filtered. */
+  padding-inline-start: var(--accessibility-arrow-horizontal-padding);
+}
+
+/* When the accesibility tree is filtered, we flatten the tree and want to hide
+   the expander icon (▶) for expandable tree rows. */
+.treeTable.filtered .treeRow .treeLabelCell .treeIcon {
+  display: none;
+}
+
 .treeTable .treeLabelCell {
   min-width: 50%;
 }
 
 .treeTable:focus {
   outline: 0;
 }
 
@@ -308,16 +319,23 @@ body {
 }
 
 .mainFrame .treeTable:focus .treeRow.selected .badges .badge {
   background-color: var(--badge-interactive-background-color);
   border-color: var(--accessible-label-border-color);
   color: var(--badge-interactive-color);
 }
 
+.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);
+}
+
 /* Right Sidebar */
 .right-sidebar {
   display: flex;
   flex-direction: column;
   flex: 1;
   white-space: nowrap;
   font: message-box;
   font-size: var(--accessibility-font-size);
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/actions/audit.js
@@ -0,0 +1,15 @@
+/* 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");
+
+exports.filterToggle = filter =>
+  dispatch => dispatch({ filter, type: FILTER_TOGGLE });
+
+exports.audit = walker =>
+  dispatch => walker.audit()
+    .then(response => dispatch({ type: AUDIT, response }))
+    .catch(error => dispatch({ type: AUDIT, error }));
--- a/devtools/client/accessibility/actions/moz.build
+++ b/devtools/client/accessibility/actions/moz.build
@@ -1,9 +1,10 @@
 # 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(
     'accessibles.js',
+    'audit.js',
     'details.js',
     'ui.js'
 )
--- a/devtools/client/accessibility/components/AccessibilityRow.js
+++ b/devtools/client/accessibility/components/AccessibilityRow.js
@@ -7,16 +7,18 @@
 
 // React & Redux
 const { Component, createFactory } = require("devtools/client/shared/vendor/react");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
 const TreeRow = require("devtools/client/shared/components/tree/TreeRow");
+const AuditFilter = createFactory(require("./AuditFilter"));
+const AuditController = createFactory(require("./AuditController"));
 
 // Utils
 const {flashElementOn, flashElementOff} =
   require("devtools/client/inspector/markup/utils");
 const { openDocLink } = require("devtools/client/shared/link");
 const { VALUE_FLASHING_DURATION, VALUE_HIGHLIGHT_DURATION } = require("../constants");
 
 // Actions
@@ -32,25 +34,23 @@ const JSON_URL_PREFIX = "data:applicatio
 
 const TELEMETRY_ACCESSIBLE_CONTEXT_MENU_OPENED =
   "devtools.accessibility.accessible_context_menu_opened";
 const TELEMETRY_ACCESSIBLE_CONTEXT_MENU_ITEM_ACTIVATED =
   "devtools.accessibility.accessible_context_menu_item_activated";
 
 class HighlightableTreeRowClass extends TreeRow {
   shouldComponentUpdate(nextProps) {
-    const props = ["name", "open", "value", "loading", "selected", "hasChildren"];
-
-    for (const p of props) {
-      if (nextProps.member[p] !== this.props.member[p]) {
-        return true;
-      }
+    const shouldTreeRowUpdate = super.shouldComponentUpdate(nextProps);
+    if (shouldTreeRowUpdate) {
+      return shouldTreeRowUpdate;
     }
 
-    if (nextProps.highlighted !== this.props.highlighted) {
+    if (nextProps.highlighted !== this.props.highlighted ||
+        nextProps.filtered !== this.props.filtered) {
       return true;
     }
 
     return false;
   }
 }
 
 const HighlightableTreeRow = createFactory(HighlightableTreeRowClass);
@@ -99,32 +99,44 @@ class AccessibilityRow extends Component
 
     if (!selected && prevProps.member.value !== this.props.member.value) {
       this.flashValue();
     }
   }
 
   scrollIntoView() {
     const row = findDOMNode(this);
+    // Row might not be rendered in the DOM tree if it is filtered out during
+    // audit.
+    if (!row) {
+      return;
+    }
+
     row.scrollIntoView({ block: "center" });
   }
 
   updateAndScrollIntoViewIfNeeded() {
     const { dispatch, member: { object }, supports } = this.props;
     if (!gToolbox || !object.actorID) {
       return;
     }
 
     dispatch(updateDetails(gToolbox.walker, object, supports));
     this.scrollIntoView();
     window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, object);
   }
 
   flashValue() {
     const row = findDOMNode(this);
+    // Row might not be rendered in the DOM tree if it is filtered out during
+    // audit.
+    if (!row) {
+      return;
+    }
+
     const value = row.querySelector(".objectBox");
 
     flashElementOn(value);
     if (this._flashMutationTimer) {
       clearTimeout(this._flashMutationTimer);
       this._flashMutationTimer = null;
     }
     this._flashMutationTimer = setTimeout(() => {
@@ -206,17 +218,21 @@ class AccessibilityRow extends Component
     const props = {
       ...this.props,
       onContextMenu: this.props.hasContextMenu && (e => this.onContextMenu(e)),
       onMouseOver: () => this.highlight(member.object),
       onMouseOut: () => this.unhighlight(),
       key: `${member.path}-${member.active ? "active" : "inactive"}`,
     };
 
-    return (HighlightableTreeRow(props));
+    return AuditController({
+      accessible: member.object,
+    },
+      AuditFilter({},
+        HighlightableTreeRow(props)));
   }
 }
 
 const mapStateToProps = ({ ui }) => ({
   supports: ui.supports,
 });
 
 module.exports =
--- a/devtools/client/accessibility/components/AccessibilityRowValue.js
+++ b/devtools/client/accessibility/components/AccessibilityRowValue.js
@@ -19,33 +19,35 @@ const Rep = createFactory(REPS.Rep);
 
 class AccessibilityRowValue extends Component {
   static get propTypes() {
     return {
       member: PropTypes.shape({
         object: PropTypes.object,
       }).isRequired,
       supports: PropTypes.object.isRequired,
+      walker: PropTypes.object.isRequired,
     };
   }
 
   render() {
-    const { member, supports: { audit } } = this.props;
+    const { member, supports: { audit }, walker } = this.props;
+
     return span({
       role: "presentation",
     },
       Rep({
         ...this.props,
         defaultRep: Grip,
         cropLimit: 50,
       }),
       audit && AuditController({
         accessible: member.object,
       },
-        Badges()),
+        Badges({ walker })),
     );
   }
 }
 
 const mapStateToProps = ({ ui: { supports } }) => {
   return { supports };
 };
 
--- a/devtools/client/accessibility/components/AccessibilityTree.js
+++ b/devtools/client/accessibility/components/AccessibilityTree.js
@@ -12,16 +12,17 @@ const { connect } = require("devtools/cl
 
 const TreeView = createFactory(require("devtools/client/shared/components/tree/TreeView"));
 // Reps
 const { MODE } = require("devtools/client/shared/components/reps/reps");
 
 const { fetchChildren } = require("../actions/accessibles");
 
 const { L10N } = require("../utils/l10n");
+const { isFiltered } = require("../utils/audit");
 const AccessibilityRow = createFactory(require("./AccessibilityRow"));
 const AccessibilityRowValue = createFactory(require("./AccessibilityRowValue"));
 const { Provider } = require("../provider");
 
 /**
  * Renders Accessibility panel tree.
  */
 class AccessibilityTree extends Component {
@@ -29,16 +30,17 @@ class AccessibilityTree extends Componen
     return {
       walker: PropTypes.object,
       dispatch: PropTypes.func.isRequired,
       accessibles: PropTypes.object,
       expanded: PropTypes.object,
       selected: PropTypes.string,
       highlighted: PropTypes.object,
       supports: PropTypes.object,
+      filtered: PropTypes.bool,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.onNameChange = this.onNameChange.bind(this);
     this.onReorder = this.onReorder.bind(this);
@@ -116,17 +118,22 @@ class AccessibilityTree extends Componen
   onTextChange(accessible) {
     const { accessibles, dispatch } = this.props;
     if (accessibles.has(accessible.actorID)) {
       dispatch(fetchChildren(accessible));
     }
   }
 
   renderValue(props) {
-    return AccessibilityRowValue(props);
+    const { walker } = this.props;
+
+    return AccessibilityRowValue({
+      ...props,
+      walker,
+    });
   }
 
   /**
    * Render Accessibility panel content
    */
   render() {
     const columns = [{
       "id": "default",
@@ -139,16 +146,17 @@ class AccessibilityTree extends Componen
     const {
       accessibles,
       dispatch,
       expanded,
       selected,
       highlighted: highlightedItem,
       supports,
       walker,
+      filtered,
     } = this.props;
 
     // Historically, the first context menu item is snapshot function and it is available
     // for all accessible object.
     const hasContextMenu = supports.snapshot;
 
     const renderRow = rowProps => {
       const { object } = rowProps.member;
@@ -159,23 +167,25 @@ class AccessibilityTree extends Componen
         highlighted,
         decorator: {
           getRowClass: function() {
             return highlighted ? ["highlighted"] : [];
           },
         },
       }));
     };
+    const className = filtered ? "filtered" : undefined;
 
     return (
       TreeView({
         object: walker,
         mode: MODE.SHORT,
         provider: new Provider(accessibles, dispatch),
         columns: columns,
+        className,
         renderValue: this.renderValue,
         renderRow,
         label: L10N.getStr("accessibility.treeName"),
         header: true,
         expandedNodes: expanded,
         selected,
         onClickRow(nodePath, event) {
           if (event.target.classList.contains("theme-twisty")) {
@@ -194,17 +204,22 @@ class AccessibilityTree extends Componen
           row = row.getWrappedInstance();
           row.onContextMenu(e);
         },
       })
     );
   }
 }
 
-const mapStateToProps = ({ accessibles, ui }) => ({
+const mapStateToProps = ({
+  accessibles,
+  ui: { expanded, selected, supports, highlighted },
+  audit: { filters },
+}) => ({
   accessibles,
-  expanded: ui.expanded,
-  selected: ui.selected,
-  supports: ui.supports,
-  highlighted: ui.highlighted,
+  expanded,
+  selected,
+  supports,
+  highlighted,
+  filtered: isFiltered(filters),
 });
 // Exports from this module
 module.exports = connect(mapStateToProps)(AccessibilityTree);
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/components/AuditFilter.js
@@ -0,0 +1,76 @@
+/* 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 React = 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 { getContrastRatioScore } = require("./ColorContrastAccessibility");
+const { isFiltered } = require("../utils/audit");
+const { FILTERS } = require("../constants");
+const { accessibility: { AUDIT_TYPE, ColorContrastScores } } =
+  require("devtools/shared/constants");
+
+function validateContrast({ error, value, min, isLargeText }) {
+  if (error) {
+    return false;
+  }
+
+  const score = getContrastRatioScore(value || min, isLargeText);
+  return score === ColorContrastScores.FAIL;
+}
+
+const AUDIT_TYPE_TO_FILTER = {
+  [AUDIT_TYPE.CONTRAST]: {
+     filterKey: FILTERS.CONTRAST,
+     validator: validateContrast,
+  },
+};
+
+class AuditFilter extends React.Component {
+  static get propTypes() {
+    return {
+      checks: PropTypes.object,
+      children: PropTypes.any,
+      filters: PropTypes.object.isRequired,
+    };
+  }
+
+  isVisible(filters) {
+    return !isFiltered(filters);
+  }
+
+  shouldHide() {
+    const { filters, checks } = this.props;
+    if (this.isVisible(filters)) {
+      return false;
+    }
+
+    if (!checks || Object.values(checks).every(check => check == null)) {
+      return true;
+    }
+
+    for (const type in checks) {
+      if (AUDIT_TYPE_TO_FILTER[type] &&
+          filters[AUDIT_TYPE_TO_FILTER[type].filterKey] &&
+          AUDIT_TYPE_TO_FILTER[type].validator(checks[type])) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  render() {
+    return this.shouldHide() ? null : this.props.children;
+  }
+}
+
+const mapStateToProps = ({ audit: { filters } }) => {
+  return { filters };
+};
+
+module.exports = connect(mapStateToProps)(AuditFilter);
--- a/devtools/client/accessibility/components/Badge.js
+++ b/devtools/client/accessibility/components/Badge.js
@@ -1,31 +1,94 @@
 /* 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 { 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 { 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,
       label: PropTypes.string.isRequired,
       tooltip: PropTypes.string,
+      walker: PropTypes.object.isRequired,
     };
   }
 
+  constructor(props) {
+    super(props);
+
+    this.toggleFilter = this.toggleFilter.bind(this);
+    this.onClick = this.onClick.bind(this);
+    this.onKeyDown = this.onKeyDown.bind(this);
+  }
+
+  shouldComponentUpdate(nextProps) {
+    return nextProps.active !== this.props.active;
+  }
+
+  toggleFilter() {
+    const { dispatch, filterKey, walker, active } = this.props;
+    dispatch(filterToggle(filterKey));
+
+    if (!active) {
+      dispatch(audit(walker));
+    }
+  }
+
+  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;
+    }
+
+    this.toggleFilter();
+  }
+
+  onKeyDown(e) {
+    // We explicitely handle "click" and "keydown" events this way here because
+    // there seem to be a difference in the sequence of keyboard/click events
+    // fired when Space/Enter is pressed. When Space is pressed the sequence of
+    // events is keydown->keyup->click but when Enter is pressed the sequence is
+    // keydown->click->keyup. This results in an unwanted badge click (when
+    // pressing Space) within the accessibility tree row when activating it
+    // because it gets focused before the click event is dispatched.
+    if (![" ", "Enter"].includes(e.key)) {
+      return;
+    }
+
+    preventDefaultAndStopPropagation(e);
+    this.toggleFilter();
+  }
+
   render() {
-    const { label, tooltip } = this.props;
+    const { active, label, tooltip } = this.props;
 
     return ToggleButton({
       className: "audit-badge badge",
       label,
+      active,
       tooltip,
+      onClick: this.onClick,
+      onKeyDown: this.onKeyDown,
     });
   }
 }
 
-module.exports = Badge;
+const mapStateToProps = ({ audit: { filters } }, { filterKey }) => ({
+  active: filters[filterKey],
+});
+
+module.exports = connect(mapStateToProps)(Badge);
--- a/devtools/client/accessibility/components/Badges.js
+++ b/devtools/client/accessibility/components/Badges.js
@@ -22,30 +22,34 @@ function getComponentForAuditType(type) 
 
   return auditTypeToComponentMap[type];
 }
 
 class Badges extends Component {
   static get propTypes() {
     return {
       checks: PropTypes.object,
+      walker: PropTypes.object.isRequired,
     };
   }
 
   render() {
-    const { checks } = this.props;
+    const { checks, walker } = this.props;
     if (!checks) {
       return null;
     }
 
     const items = [];
     for (const type in checks) {
       const component = getComponentForAuditType(type);
       if (checks[type] && component) {
-        items.push(component(checks[type]));
+        items.push(component({
+          ...checks[type],
+          walker,
+        }));
       }
     }
 
     if (items.length === 0) {
       return null;
     }
 
     return (
--- a/devtools/client/accessibility/components/ContrastBadge.js
+++ b/devtools/client/accessibility/components/ContrastBadge.js
@@ -9,43 +9,48 @@ const PropTypes = require("devtools/clie
 
 const { L10N } = require("../utils/l10n");
 
 const { getContrastRatioScore } = require("./ColorContrastAccessibility");
 const { accessibility: { ColorContrastScores } } = require("devtools/shared/constants");
 
 loader.lazyGetter(this, "Badge", () => createFactory(require("./Badge")));
 
+const { FILTERS } = require("../constants");
+
 /**
 * Component for rendering a badge for contrast accessibliity check
 * failures association with a given accessibility object in the accessibility
 * tree.
 */
 
 class ContrastBadge extends Component {
   static get propTypes() {
     return {
       error: PropTypes.string,
       isLargeText: PropTypes.bool.isRequired,
       value: PropTypes.number,
       min: PropTypes.number,
+      walker: PropTypes.object.isRequired,
     };
   }
 
   render() {
-    const { error, value, min, isLargeText } = this.props;
+    const { error, value, min, isLargeText, walker } = this.props;
     if (error) {
       return null;
     }
 
     const score = getContrastRatioScore(value || min, isLargeText);
     if (score !== ColorContrastScores.FAIL) {
       return null;
     }
 
     return Badge({
       label: L10N.getStr("accessibility.badge.contrast"),
       tooltip: L10N.getStr("accessibility.badge.contrast.tooltip"),
+      filterKey: FILTERS.CONTRAST,
+      walker,
     });
   }
 }
 
 module.exports = ContrastBadge;
--- a/devtools/client/accessibility/components/moz.build
+++ b/devtools/client/accessibility/components/moz.build
@@ -3,16 +3,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'AccessibilityRow.js',
     'AccessibilityRowValue.js',
     'AccessibilityTree.js',
     'Accessible.js',
     'AuditController.js',
+    'AuditFilter.js',
     'Badge.js',
     'Badges.js',
     'Button.js',
     'Checks.js',
     'ColorContrastAccessibility.js',
     'ContrastBadge.js',
     'Description.js',
     'LearnMoreLink.js',
--- a/devtools/client/accessibility/constants.js
+++ b/devtools/client/accessibility/constants.js
@@ -1,13 +1,15 @@
 /* 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 { accessibility: { AUDIT_TYPE } } = require("devtools/shared/constants");
+
 // Used in accessible component for properties tree rendering.
 exports.TREE_ROW_HEIGHT = 21;
 
 // Initial sidebar width.
 exports.SIDEBAR_WIDTH = "350px";
 
 // When value is updated either in the tree or sidebar.
 exports.VALUE_FLASHING_DURATION = 500;
@@ -24,16 +26,23 @@ exports.UPDATE_DETAILS = "UPDATE_DETAILS
 exports.RESET = "RESET";
 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";
+
+// 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 = [
   "name",
   "role",
   "actions",
   "value",
   "DOMNode",
--- a/devtools/client/accessibility/reducers/accessibles.js
+++ b/devtools/client/accessibility/reducers/accessibles.js
@@ -1,14 +1,15 @@
 /* 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,
   FETCH_CHILDREN,
   HIGHLIGHT,
   RESET,
   SELECT,
 } = require("../constants");
 
 /**
  * Initial state definition
@@ -22,16 +23,18 @@ function getInitialState() {
  */
 function accessibles(state = getInitialState(), action) {
   switch (action.type) {
     case FETCH_CHILDREN:
       return onReceiveChildren(state, action);
     case HIGHLIGHT:
     case SELECT:
       return onReceiveAncestry(state, action);
+    case AUDIT:
+      return onAudit(state, action);
     case RESET:
       return getInitialState();
     default:
       return state;
   }
 }
 
 function getActorID(accessible) {
@@ -133,9 +136,22 @@ function onReceiveAncestry(cache, action
   if (error) {
     console.warn(`Error fetching ancestry: `, error);
     return cache;
   }
 
   return updateAncestry(new Map(cache), ancestry);
 }
 
+function onAudit(cache, action) {
+  const { error, response: ancestries } = action;
+  if (error) {
+    console.warn(`Error performing an audit: `, error);
+    return cache;
+  }
+
+  const newCache = new Map(cache);
+  ancestries.forEach(ancestry => updateAncestry(newCache, ancestry));
+
+  return newCache;
+}
+
 exports.accessibles = accessibles;
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/reducers/audit.js
@@ -0,0 +1,41 @@
+/* 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");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+  return {
+    filters: {
+      [FILTERS.CONTRAST]: false,
+    },
+  };
+}
+
+function audit(state = getInitialState(), action) {
+  switch (action.type) {
+    case FILTER_TOGGLE:
+      const { filter } = action;
+      let { filters } = state;
+      const active = !filters[filter];
+      filters = {
+        ...filters,
+        [filter]: active,
+      };
+
+      return {
+        ...state,
+        filters,
+      };
+    case RESET:
+      return getInitialState();
+    default:
+      return state;
+  }
+}
+
+exports.audit = audit;
--- a/devtools/client/accessibility/reducers/index.js
+++ b/devtools/client/accessibility/reducers/index.js
@@ -1,14 +1,16 @@
 /* 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 { accessibles } = require("./accessibles");
+const { audit } = require("./audit");
 const { details } = require("./details");
 const { ui } = require("./ui");
 
 exports.reducers = {
   accessibles,
+  audit,
   details,
   ui,
 };
--- a/devtools/client/accessibility/reducers/moz.build
+++ b/devtools/client/accessibility/reducers/moz.build
@@ -1,10 +1,11 @@
 # 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(
     'accessibles.js',
+    'audit.js',
     'details.js',
     'index.js',
     'ui.js'
 )
--- a/devtools/client/accessibility/reducers/ui.js
+++ b/devtools/client/accessibility/reducers/ui.js
@@ -1,14 +1,15 @@
 /* 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,
   ENABLE,
   DISABLE,
   RESET,
   SELECT,
   HIGHLIGHT,
   UNHIGHLIGHT,
   UPDATE_CAN_BE_DISABLED,
   UPDATE_CAN_BE_ENABLED,
@@ -43,16 +44,18 @@ function ui(state = getInitialState(), a
     case UPDATE_CAN_BE_DISABLED:
       return onCanBeDisabledChange(state, action);
     case UPDATE_CAN_BE_ENABLED:
       return onCanBeEnabledChange(state, action);
     case UPDATE_DETAILS:
       return onUpdateDetails(state, action);
     case HIGHLIGHT:
       return onHighlight(state, action);
+    case AUDIT:
+      return onAudit(state, action);
     case UNHIGHLIGHT:
       return onUnhighlight(state, action);
     case SELECT:
       return onSelect(state, action);
     case RESET:
       return onReset(state, action);
     default:
       return state;
@@ -68,44 +71,61 @@ function onUpdateDetails(state) {
   // performed.
   return Object.assign({}, state, { selected: null });
 }
 
 function onUnhighlight(state) {
   return Object.assign({}, state, { highlighted: null });
 }
 
-function updateExpandedNodes(state, ancestry) {
-  const expanded = new Set(state.expanded);
+function updateExpandedNodes(expanded, ancestry) {
+  expanded = new Set(expanded);
   const path = ancestry.reduceRight((accPath, { accessible }) => {
     accPath = TreeView.subPath(accPath, accessible.actorID);
     expanded.add(accPath);
     return accPath;
   }, "");
 
   return { path, expanded };
 }
 
+function onAudit(state, { response: ancestries, error }) {
+  if (error) {
+    console.warn("Error running audit", error);
+    return state;
+  }
+
+  let expanded = new Set(state.expanded);
+  for (const ancestry of ancestries) {
+    ({ expanded } = updateExpandedNodes(expanded, ancestry));
+  }
+
+  return {
+    ...state,
+    expanded,
+  };
+}
+
 function onHighlight(state, { accessible, response: ancestry, error }) {
   if (error) {
     console.warn("Error fetching ancestry", accessible, error);
     return state;
   }
 
-  const { expanded } = updateExpandedNodes(state, ancestry);
+  const { expanded } = updateExpandedNodes(state.expanded, ancestry);
   return Object.assign({}, state, { expanded, highlighted: accessible });
 }
 
 function onSelect(state, { accessible, response: ancestry, error }) {
   if (error) {
     console.warn("Error fetching ancestry", accessible, error);
     return state;
   }
 
-  const { path, expanded } = updateExpandedNodes(state, ancestry);
+  const { path, expanded } = updateExpandedNodes(state.expanded, ancestry);
   const selected = TreeView.subPath(path, accessible.actorID);
 
   return Object.assign({}, state, { expanded, selected });
 }
 
 /**
  * Handle "canBeDisabled" flag update for accessibility service
  * @param  {Object}  state   Current ui state
--- a/devtools/client/accessibility/test/browser/browser.ini
+++ b/devtools/client/accessibility/test/browser/browser.ini
@@ -19,11 +19,12 @@ 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.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.js
@@ -0,0 +1,102 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* global toggleRow, toggleBadge */
+
+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: "Expand first and second tree nodes.",
+  setup: async ({ doc }) => {
+    await toggleRow(doc, 0);
+    await toggleRow(doc, 1);
+  },
+  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"`,
+    }],
+  },
+}, {
+  desc: "Click on the badge.",
+  setup: async ({ doc }) => {
+    await toggleBadge(doc, 2, 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 badge 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 one of
+ * the tree rows has a "contrast" badge and auditing is activated via badge.
+ */
+addA11yPanelTestsTask(tests, TEST_URI,
+  "Test Accessibility panel tree with contrast badge audit activation.");
--- a/devtools/client/accessibility/test/browser/head.js
+++ b/devtools/client/accessibility/test/browser/head.js
@@ -1,17 +1,17 @@
 /* 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, addA11yPanelTestsTask, reload, navigate */
+            toggleRow, toggleBadge, 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);
 
@@ -372,16 +372,32 @@ async function toggleRow(doc, rowNumber)
 
   EventUtils.sendMouseEvent({ type: "click" }, twisty, win);
   await BrowserTestUtils.waitForCondition(() =>
     !twisty.classList.contains("devtools-throbber") &&
     expected === twisty.classList.contains("open"), "Twisty updated.");
 }
 
 /**
+ * Toggle an accessibility audit badge based on its index in the badges group.
+ * @param  {document} doc        panel documnent.
+ * @param  {Number}   badgeIndex index of the badge to be toggled.
+ */
+async function toggleBadge(doc, rowNumber, badgeIndex) {
+  const win = doc.defaultView;
+  const row = doc.querySelectorAll(".treeRow")[rowNumber];
+  const badge = row.querySelectorAll(".audit-badge.badge")[badgeIndex];
+  const expected = !badge.classList.contains("checked");
+
+  EventUtils.synthesizeMouseAtCenter(badge, {}, win);
+  await BrowserTestUtils.waitForCondition(() =>
+    expected === badge.classList.contains("checked"), "Badge 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__/audit-filter.test.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AuditController component: audit filter filtered contrast checks fail 1`] = `"<span></span>"`;
+
+exports[`AuditController component: audit filter filtered contrast checks fail range 1`] = `"<span></span>"`;
+
+exports[`AuditController component: audit filter filtered contrast checks success 1`] = `null`;
+
+exports[`AuditController component: audit filter filtered no checks 1`] = `null`;
+
+exports[`AuditController component: audit filter filtered unknown checks 1`] = `null`;
+
+exports[`AuditController component: audit filter not filtered 1`] = `"<span></span>"`;
--- a/devtools/client/accessibility/test/jest/components/__snapshots__/badge.test.js.snap
+++ b/devtools/client/accessibility/test/jest/components/__snapshots__/badge.test.js.snap
@@ -1,3 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Badge component: basic render 1`] = `"<button aria-pressed=\\"false\\" class=\\"audit-badge badge toggle-button\\">Contrast</button>"`;
+exports[`Badge component: basic render active 1`] = `"<button aria-pressed=\\"true\\" class=\\"audit-badge badge toggle-button checked\\">Contrast</button>"`;
+
+exports[`Badge component: basic render inactive 1`] = `"<button aria-pressed=\\"false\\" class=\\"audit-badge badge toggle-button\\">Contrast</button>"`;
+
+exports[`Badge component: toggle filter 1`] = `"<button aria-pressed=\\"false\\" class=\\"audit-badge badge toggle-button\\">Contrast</button>"`;
new file mode 100644
--- /dev/null
+++ b/devtools/client/accessibility/test/jest/components/audit-filter.test.js
@@ -0,0 +1,115 @@
+/* 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 { span } = require("devtools/client/shared/vendor/react-dom-factories");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+
+const ConnectedAuditFilterClass =
+  require("devtools/client/accessibility/components/AuditFilter");
+const AuditFilterClass = ConnectedAuditFilterClass.WrappedComponent;
+const AuditFilter = createFactory(ConnectedAuditFilterClass);
+const {
+  setupStore,
+} = require("devtools/client/accessibility/test/jest/helpers");
+const { FILTERS } = require("devtools/client/accessibility/constants");
+
+describe("AuditController component:", () => {
+  it("audit filter not filtered", () => {
+    const store = setupStore();
+
+    const wrapper = mount(Provider({store}, AuditFilter({}, span())));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const filter = wrapper.find(AuditFilterClass);
+    expect(filter.children().length).toBe(1);
+    expect(filter.childAt(0).is("span")).toBe(true);
+  });
+
+  it("audit filter filtered no checks", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
+    });
+
+    const wrapper = mount(Provider({store}, AuditFilter({}, span())));
+    expect(wrapper.html()).toMatchSnapshot();
+    expect(wrapper.isEmptyRender()).toBe(true);
+  });
+
+  it("audit filter filtered unknown checks", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { tbd: true }}},
+    });
+
+    const wrapper = mount(Provider({store}, AuditFilter({}, span())));
+    expect(wrapper.html()).toMatchSnapshot();
+    expect(wrapper.isEmptyRender()).toBe(true);
+  });
+
+  it("audit filter filtered contrast checks success", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
+    });
+
+    const wrapper = mount(Provider({store}, AuditFilter({
+      checks: {
+        "CONTRAST": {
+          "value": 5.11,
+          "color": [255, 0, 0, 1],
+          "backgroundColor": [255, 255, 255, 1],
+          "isLargeText": false,
+        },
+      },
+    }, span())));
+    expect(wrapper.html()).toMatchSnapshot();
+    expect(wrapper.isEmptyRender()).toBe(true);
+  });
+
+  it("audit filter filtered contrast checks fail", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
+    });
+
+    const CONTRAST = {
+      "value": 3.1,
+      "color": [255, 0, 0, 1],
+      "backgroundColor": [255, 255, 255, 1],
+      "isLargeText": false,
+    };
+
+    const wrapper = mount(Provider({store}, AuditFilter({
+      checks: { CONTRAST },
+    }, span())));
+    expect(wrapper.html()).toMatchSnapshot();
+    const filter = wrapper.find(AuditFilterClass);
+    expect(filter.children().length).toBe(1);
+    expect(filter.childAt(0).is("span")).toBe(true);
+  });
+
+  it("audit filter filtered contrast checks fail range", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
+    });
+
+    const CONTRAST = {
+      "min": 1.19,
+      "max": 1.39,
+      "color": [128, 128, 128, 1],
+      "backgroundColorMin": [219, 106, 116, 1],
+      "backgroundColorMax": [156, 145, 211, 1],
+      "isLargeText": false,
+    };
+
+    const wrapper = mount(Provider({store}, AuditFilter({
+      checks: { CONTRAST },
+    }, span())));
+    expect(wrapper.html()).toMatchSnapshot();
+    const filter = wrapper.find(AuditFilterClass);
+    expect(filter.children().length).toBe(1);
+    expect(filter.childAt(0).is("span")).toBe(true);
+  });
+});
--- a/devtools/client/accessibility/test/jest/components/badge.test.js
+++ b/devtools/client/accessibility/test/jest/components/badge.test.js
@@ -1,35 +1,76 @@
 /* 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 { setupStore } = require("devtools/client/accessibility/test/jest/helpers");
 
 const { ToggleButton } = require("devtools/client/accessibility/components/Button");
-const Badge = createFactory(require("devtools/client/accessibility/components/Badge"));
+const ConnectedBadgeClass = require("devtools/client/accessibility/components/Badge");
+const BadgeClass = ConnectedBadgeClass.WrappedComponent;
+const Badge = createFactory(ConnectedBadgeClass);
+
+const { FILTERS } = require("devtools/client/accessibility/constants");
 
 describe("Badge component:", () => {
   const props = {
     label: "Contrast",
+    filterKey: FILTERS.CONTRAST,
   };
 
-  it("basic render", () => {
-    const wrapper = mount(Badge(props));
+  it("basic render inactive", () => {
+    const store = setupStore();
+    const wrapper = mount(Provider({ store }, Badge(props)));
     expect(wrapper.html()).toMatchSnapshot();
-    expect(wrapper.children().length).toBe(1);
 
-    const toggleButton = wrapper.childAt(0);
+    const badge = wrapper.find(BadgeClass);
+    expect(badge.prop("active")).toBe(false);
+    expect(badge.children().length).toBe(1);
+
+    const toggleButton = badge.childAt(0);
     expect(toggleButton.type()).toBe(ToggleButton);
     expect(toggleButton.children().length).toBe(1);
 
     const button = toggleButton.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("Contrast");
   });
+
+  it("basic render active", () => {
+    const store = setupStore({
+      preloadedState: { audit: { filters: { [FILTERS.CONTRAST]: true }}},
+    });
+    const wrapper = mount(Provider({ store }, Badge(props)));
+    expect(wrapper.html()).toMatchSnapshot();
+    const badge = wrapper.find(BadgeClass);
+    expect(badge.prop("active")).toBe(true);
+
+    const button = wrapper.find("button");
+    expect(button.prop("aria-pressed")).toBe(true);
+  });
+
+  it("toggle filter", () => {
+    const store = setupStore();
+    const wrapper = mount(Provider({ store }, Badge(props)));
+    expect(wrapper.html()).toMatchSnapshot();
+
+    const badgeInstance = wrapper.find(BadgeClass).instance();
+    badgeInstance.toggleFilter = jest.fn();
+    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: " " });
+    expect(badgeInstance.toggleFilter.mock.calls.length).toBe(1);
+
+    wrapper.find("button.audit-badge.badge").simulate("keydown", { key: "Enter" });
+    expect(badgeInstance.toggleFilter.mock.calls.length).toBe(2);
+
+    wrapper.find("button.audit-badge.badge").simulate("click");
+    expect(badgeInstance.toggleFilter.mock.calls.length).toBe(3);
+  });
 });
--- a/devtools/client/accessibility/test/jest/components/badges.test.js
+++ b/devtools/client/accessibility/test/jest/components/badges.test.js
@@ -4,83 +4,89 @@
 
 "use strict";
 
 /* global L10N */
 
 const { mount } = require("enzyme");
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const { setupStore } = require("devtools/client/accessibility/test/jest/helpers");
 
 const Badge = require("devtools/client/accessibility/components/Badge");
 const Badges = createFactory(require("devtools/client/accessibility/components/Badges"));
 const ContrastBadge = require("devtools/client/accessibility/components/ContrastBadge");
 
 describe("Badges component:", () => {
+  const store = setupStore();
+
   it("no props render", () => {
-    const wrapper = mount(Badges({}));
+    const wrapper = mount(Provider({ store }, Badges()));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("null checks render", () => {
-    const wrapper = mount(Badges({ checks: null }));
+    const wrapper = mount(Provider({ store }, Badges({ checks: null })));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("empty checks render", () => {
-    const wrapper = mount(Badges({ checks: {} }));
+    const wrapper = mount(Provider({ store }, Badges({ checks: {} })));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("unsupported checks render", () => {
-    const wrapper = mount(Badges({ checks: { tbd: {} } }));
+    const wrapper = mount(Provider({ store }, Badges({ checks: { tbd: {} } })));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("contrast ratio success render", () => {
-    const wrapper = mount(Badges({
+    const wrapper = mount(Provider({ store }, Badges({
       checks: {
         "CONTRAST": {
           "value": 5.11,
           "color": [255, 0, 0, 1],
           "backgroundColor": [255, 255, 255, 1],
           "isLargeText": false,
         },
       },
-    }));
+    })));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("contrast ratio fail render", () => {
     const CONTRAST = {
       "value": 3.1,
       "color": [255, 0, 0, 1],
       "backgroundColor": [255, 255, 255, 1],
       "isLargeText": false,
     };
-    const wrapper = mount(Badges({ checks: { CONTRAST } }));
+    const wrapper = mount(Provider({ store }, Badges({ checks: { CONTRAST }})));
+
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.find(Badge).length).toBe(1);
     expect(wrapper.find(ContrastBadge).length).toBe(1);
     expect(wrapper.find(ContrastBadge).first().props()).toMatchObject(CONTRAST);
   });
 
   it("contrast ratio fail range render", () => {
     const CONTRAST = {
       "min": 1.19,
       "max": 1.39,
       "color": [128, 128, 128, 1],
       "backgroundColorMin": [219, 106, 116, 1],
       "backgroundColorMax": [156, 145, 211, 1],
       "isLargeText": false,
     };
-    const wrapper = mount(Badges({ checks: { CONTRAST } }));
+    const wrapper = mount(Provider({ store }, Badges({ checks: { CONTRAST }})));
+
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.find(Badge).length).toBe(1);
     expect(wrapper.find(ContrastBadge).length).toBe(1);
     expect(wrapper.find(ContrastBadge).first().props()).toMatchObject(CONTRAST);
   });
 });
--- a/devtools/client/accessibility/test/jest/components/contrast-badge.test.js
+++ b/devtools/client/accessibility/test/jest/components/contrast-badge.test.js
@@ -3,20 +3,27 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { shallow, mount } = require("enzyme");
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
 
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const { setupStore } = require("devtools/client/accessibility/test/jest/helpers");
+
 const Badge = require("devtools/client/accessibility/components/Badge");
-const ContrastBadge = createFactory(require("devtools/client/accessibility/components/ContrastBadge"));
+const ContrastBadgeClass = require("devtools/client/accessibility/components/ContrastBadge");
+const ContrastBadge = createFactory(ContrastBadgeClass);
+const { FILTERS } = require("devtools/client/accessibility/constants");
 
 describe("ContrastBadge component:", () => {
+  const store = setupStore();
+
   it("error render", () => {
     const wrapper = shallow(ContrastBadge({ error: true }));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("success render", () => {
     const wrapper = shallow(ContrastBadge({
@@ -42,22 +49,24 @@ describe("ContrastBadge component:", () 
       value: 3.77,
       isLargeText: true,
     }));
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.isEmptyRender()).toBe(true);
   });
 
   it("fail render", () => {
-    const wrapper = mount(ContrastBadge({
+    const wrapper = mount(Provider({ store }, ContrastBadge({
       value: 3.77,
       isLargeText: false,
-    }));
+    })));
 
     expect(wrapper.html()).toMatchSnapshot();
     expect(wrapper.children().length).toBe(1);
-    const badge = wrapper.childAt(0);
+    const contrastBadge = wrapper.find(ContrastBadgeClass);
+    const badge = contrastBadge.childAt(0);
     expect(badge.type()).toBe(Badge);
     expect(badge.props()).toMatchObject({
       label: "accessibility.badge.contrast",
+      filterKey: FILTERS.CONTRAST,
     });
   });
 });
--- a/devtools/client/accessibility/test/mochitest/test_accessible_row_context_menu.html
+++ b/devtools/client/accessibility/test/mochitest/test_accessible_row_context_menu.html
@@ -28,16 +28,17 @@ window.onload = async function() {
     const { createFactory, createElement } =
       browserRequire("devtools/client/shared/vendor/react");
     const { Provider } = require("devtools/client/shared/vendor/react-redux");
     const createStore = require("devtools/client/shared/redux/create-store")();
     const { Simulate } =
       browserRequire("devtools/client/shared/vendor/react-dom-test-utils");
     const AccessibilityRow = createFactory(
       browserRequire("devtools/client/accessibility/components/AccessibilityRow"));
+    const { FILTERS } = browserRequire("devtools/client/accessibility/constants");
 
     async function withMockEnv(func) {
       const { gToolbox: originalToolbox, gTelemetry: originalTelemetry } = window;
       window.gToolbox = { doc: document };
       window.gTelemetry = null;
 
       await func();
 
@@ -70,16 +71,18 @@ window.onload = async function() {
       member: {
         object: {
           name: "test",
           value: "test",
           loading: false,
           selected: false,
           hasChildren: false,
           snapshot: async () => SNAPSHOT,
+          on: () => {},
+          off: () => {},
         },
       },
       columns: [
         { "id": "default", "title": "role" },
         { "id": "value", "title": "name" },
       ],
       provider: {
         getValue: (object, id) => object[id],
@@ -87,18 +90,26 @@ window.onload = async function() {
       hasContextMenu: true,
     };
 
     const mockProps = {
       ...defaultProps,
       hasContextMenu: null,
     };
 
-    const defaultState = { ui: { supports: { snapshot: true }}};
-    const mockState = { ui: { supports: {}}};
+    const auditState = { audit: { filters: { [FILTERS.CONTRAST]: false }}};
+
+    const defaultState = {
+      ui: { supports: { snapshot: true }},
+      ...auditState,
+    };
+    const mockState = {
+      ui: { supports: {}},
+      ...auditState,
+    };
 
     info("Check contextmenu default behaviour.");
     renderAccessibilityRow(defaultProps, defaultState);
     let row = document.getElementById(ROW_ID);
 
     await withMockEnv(async function() {
       Simulate.contextMenu(row);
 
copy from devtools/client/accessibility/reducers/index.js
copy to devtools/client/accessibility/utils/audit.js
--- a/devtools/client/accessibility/reducers/index.js
+++ b/devtools/client/accessibility/utils/audit.js
@@ -1,14 +1,7 @@
 /* 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 { accessibles } = require("./accessibles");
-const { details } = require("./details");
-const { ui } = require("./ui");
-
-exports.reducers = {
-  accessibles,
-  details,
-  ui,
-};
+exports.isFiltered = filters => Object.values(filters).some(active => active);
--- a/devtools/client/accessibility/utils/moz.build
+++ b/devtools/client/accessibility/utils/moz.build
@@ -1,7 +1,8 @@
 # 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(
+    'audit.js',
     'l10n.js'
 )
--- a/devtools/client/shared/components/tree/LabelCell.js
+++ b/devtools/client/shared/components/tree/LabelCell.js
@@ -24,37 +24,35 @@ define(function(require, exports, module
       };
     }
 
     render() {
       const id = this.props.id;
       const member = this.props.member;
       const level = member.level || 0;
 
-      // Compute indentation dynamically. The deeper the item is
-      // inside the hierarchy, the bigger is the left padding.
-      const rowStyle = {
-        "paddingInlineStart": (level * 16) + "px",
-      };
-
       const iconClassList = ["treeIcon"];
       if (member.hasChildren && member.loading) {
         iconClassList.push("devtools-throbber");
       } else if (member.hasChildren) {
         iconClassList.push("theme-twisty");
       }
       if (member.open) {
         iconClassList.push("open");
       }
 
       return (
         dom.td({
           className: "treeLabelCell",
+          style: {
+            // Compute indentation dynamically. The deeper the item is
+            // inside the hierarchy, the bigger is the left padding.
+            "--tree-label-cell-indent": `${level * 16}px`,
+          },
           key: "default",
-          style: rowStyle,
           role: "presentation"},
           dom.span({
             className: iconClassList.join(" "),
             role: "presentation",
           }),
           dom.span({
             className: "treeLabel " + member.type + "Label",
             "aria-labelledby": id,
--- a/devtools/client/shared/components/tree/TreeView.css
+++ b/devtools/client/shared/components/tree/TreeView.css
@@ -27,16 +27,17 @@
   padding-inline-start: 4px;
   vertical-align: top;
   overflow: hidden;
 }
 
 .treeTable .treeLabelCell {
   white-space: nowrap;
   cursor: default;
+  padding-inline-start: var(--tree-label-cell-indent);
 }
 
 .treeTable .treeLabelCell::after {
   content: ":";
   color: var(--object-color);
 }
 
 .treeTable .treeValueCell.inputEnabled {