summaryrefslogtreecommitdiffstats
path: root/devtools/client/accessibility/components
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/accessibility/components
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/accessibility/components')
-rw-r--r--devtools/client/accessibility/components/AccessibilityPrefs.js109
-rw-r--r--devtools/client/accessibility/components/AccessibilityRow.js328
-rw-r--r--devtools/client/accessibility/components/AccessibilityRowValue.js58
-rw-r--r--devtools/client/accessibility/components/AccessibilityTree.js298
-rw-r--r--devtools/client/accessibility/components/AccessibilityTreeFilter.js171
-rw-r--r--devtools/client/accessibility/components/Accessible.js563
-rw-r--r--devtools/client/accessibility/components/AuditController.js90
-rw-r--r--devtools/client/accessibility/components/AuditFilter.js91
-rw-r--r--devtools/client/accessibility/components/AuditProgressOverlay.js92
-rw-r--r--devtools/client/accessibility/components/Badge.js40
-rw-r--r--devtools/client/accessibility/components/Badges.js88
-rw-r--r--devtools/client/accessibility/components/Button.js112
-rw-r--r--devtools/client/accessibility/components/Check.js157
-rw-r--r--devtools/client/accessibility/components/Checks.js117
-rw-r--r--devtools/client/accessibility/components/ColorContrastAccessibility.js229
-rw-r--r--devtools/client/accessibility/components/ContrastBadge.js55
-rw-r--r--devtools/client/accessibility/components/Description.js56
-rw-r--r--devtools/client/accessibility/components/DisplayTabbingOrder.js79
-rw-r--r--devtools/client/accessibility/components/KeyboardBadge.js56
-rw-r--r--devtools/client/accessibility/components/KeyboardCheck.js94
-rw-r--r--devtools/client/accessibility/components/LearnMoreLink.js76
-rw-r--r--devtools/client/accessibility/components/MainFrame.js245
-rw-r--r--devtools/client/accessibility/components/RightSidebar.js67
-rw-r--r--devtools/client/accessibility/components/SimulationMenuButton.js167
-rw-r--r--devtools/client/accessibility/components/TextLabelBadge.js57
-rw-r--r--devtools/client/accessibility/components/TextLabelCheck.js225
-rw-r--r--devtools/client/accessibility/components/Toolbar.js74
-rw-r--r--devtools/client/accessibility/components/moz.build33
28 files changed, 3827 insertions, 0 deletions
diff --git a/devtools/client/accessibility/components/AccessibilityPrefs.js b/devtools/client/accessibility/components/AccessibilityPrefs.js
new file mode 100644
index 0000000000..8c5de5f15f
--- /dev/null
+++ b/devtools/client/accessibility/components/AccessibilityPrefs.js
@@ -0,0 +1,109 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ hr,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+loader.lazyGetter(this, "MenuButton", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuButton.js")
+ );
+});
+loader.lazyGetter(this, "MenuItem", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuItem.js")
+ );
+});
+loader.lazyGetter(this, "MenuList", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuList.js")
+ );
+});
+
+const {
+ A11Y_LEARN_MORE_LINK,
+} = require("resource://devtools/client/accessibility/constants.js");
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const {
+ updatePref,
+} = require("resource://devtools/client/accessibility/actions/ui.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ PREFS,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+class AccessibilityPrefs extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ [PREFS.SCROLL_INTO_VIEW]: PropTypes.bool.isRequired,
+ toolboxDoc: PropTypes.object.isRequired,
+ };
+ }
+
+ togglePref(prefKey) {
+ this.props.dispatch(updatePref(prefKey, !this.props[prefKey]));
+ }
+
+ onPrefClick(prefKey) {
+ this.togglePref(prefKey);
+ }
+
+ onLearnMoreClick() {
+ openDocLink(A11Y_LEARN_MORE_LINK);
+ }
+
+ render() {
+ return MenuButton(
+ {
+ menuId: "accessibility-tree-filters-prefs-menu",
+ toolboxDoc: this.props.toolboxDoc,
+ className: `devtools-button badge toolbar-menu-button prefs`,
+ title: L10N.getStr("accessibility.tree.filters.prefs"),
+ },
+ MenuList({}, [
+ MenuItem({
+ key: "pref-scroll-into-view",
+ checked: this.props[PREFS.SCROLL_INTO_VIEW],
+ className: `pref ${PREFS.SCROLL_INTO_VIEW}`,
+ label: L10N.getStr("accessibility.pref.scroll.into.view.label"),
+ tooltip: L10N.getStr("accessibility.pref.scroll.into.view.title"),
+ onClick: this.onPrefClick.bind(this, PREFS.SCROLL_INTO_VIEW),
+ }),
+ hr({ key: "hr" }),
+ MenuItem({
+ role: "link",
+ key: "accessibility-tree-filters-prefs-menu-help",
+ className: "help",
+ label: L10N.getStr("accessibility.documentation.label"),
+ tooltip: L10N.getStr("accessibility.learnMore"),
+ onClick: this.onLearnMoreClick,
+ }),
+ ])
+ );
+ }
+}
+
+const mapStateToProps = ({ ui }) => ({
+ [PREFS.SCROLL_INTO_VIEW]: ui[PREFS.SCROLL_INTO_VIEW],
+});
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(AccessibilityPrefs);
diff --git a/devtools/client/accessibility/components/AccessibilityRow.js b/devtools/client/accessibility/components/AccessibilityRow.js
new file mode 100644
index 0000000000..ffca5c74ef
--- /dev/null
+++ b/devtools/client/accessibility/components/AccessibilityRow.js
@@ -0,0 +1,328 @@
+/* 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";
+
+/* global gTelemetry, EVENTS */
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ findDOMNode,
+} = require("resource://devtools/client/shared/vendor/react-dom.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const TreeRow = require("resource://devtools/client/shared/components/tree/TreeRow.js");
+const AuditFilter = createFactory(
+ require("resource://devtools/client/accessibility/components/AuditFilter.js")
+);
+const AuditController = createFactory(
+ require("resource://devtools/client/accessibility/components/AuditController.js")
+);
+
+// Utils
+const {
+ flashElementOn,
+ flashElementOff,
+} = require("resource://devtools/client/inspector/markup/utils.js");
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+const {
+ PREFS,
+ VALUE_FLASHING_DURATION,
+ VALUE_HIGHLIGHT_DURATION,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
+
+// Actions
+const {
+ updateDetails,
+} = require("resource://devtools/client/accessibility/actions/details.js");
+const {
+ unhighlight,
+} = require("resource://devtools/client/accessibility/actions/accessibles.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+loader.lazyRequireGetter(
+ this,
+ "Menu",
+ "resource://devtools/client/framework/menu.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MenuItem",
+ "resource://devtools/client/framework/menu-item.js"
+);
+
+const {
+ scrollIntoView,
+} = require("resource://devtools/client/shared/scroll.js");
+
+const JSON_URL_PREFIX = "data:application/json;charset=UTF-8,";
+
+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 shouldTreeRowUpdate = super.shouldComponentUpdate(nextProps);
+ if (shouldTreeRowUpdate) {
+ return shouldTreeRowUpdate;
+ }
+
+ if (
+ nextProps.highlighted !== this.props.highlighted ||
+ nextProps.filtered !== this.props.filtered
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+}
+
+const HighlightableTreeRow = createFactory(HighlightableTreeRowClass);
+
+// Component that expands TreeView's own TreeRow and is responsible for
+// rendering an accessible object.
+class AccessibilityRow extends Component {
+ static get propTypes() {
+ return {
+ ...TreeRow.propTypes,
+ dispatch: PropTypes.func.isRequired,
+ toolboxDoc: PropTypes.object.isRequired,
+ scrollContentNodeIntoView: PropTypes.bool.isRequired,
+ highlightAccessible: PropTypes.func.isRequired,
+ unhighlightAccessible: PropTypes.func.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ const {
+ member: { selected, object },
+ scrollContentNodeIntoView,
+ } = this.props;
+ if (selected) {
+ this.unhighlight(object);
+ this.update();
+ this.highlight(
+ object,
+ { duration: VALUE_HIGHLIGHT_DURATION },
+ scrollContentNodeIntoView
+ );
+ }
+
+ if (this.props.highlighted) {
+ this.scrollIntoView();
+ }
+ }
+
+ /**
+ * Update accessible object details that are going to be rendered inside the
+ * accessible panel sidebar.
+ */
+ componentDidUpdate(prevProps) {
+ const {
+ member: { selected, object },
+ scrollContentNodeIntoView,
+ } = this.props;
+ // If row is selected, update corresponding accessible details.
+ if (!prevProps.member.selected && selected) {
+ this.unhighlight(object);
+ this.update();
+ this.highlight(
+ object,
+ { duration: VALUE_HIGHLIGHT_DURATION },
+ scrollContentNodeIntoView
+ );
+ }
+
+ if (this.props.highlighted) {
+ this.scrollIntoView();
+ }
+
+ 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;
+ }
+
+ scrollIntoView(row);
+ }
+
+ update() {
+ const {
+ dispatch,
+ member: { object },
+ } = this.props;
+ if (!object.actorID) {
+ return;
+ }
+
+ dispatch(updateDetails(object));
+ 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(() => {
+ flashElementOff(value);
+ }, VALUE_FLASHING_DURATION);
+ }
+
+ /**
+ * Scroll the node that corresponds to a current accessible object into view.
+ * @param {Object}
+ * Accessible front that is rendered for this node.
+ *
+ * @returns {Promise}
+ * Promise that resolves when the node is scrolled into view if
+ * possible.
+ */
+ async scrollNodeIntoViewIfNeeded(accessibleFront) {
+ if (accessibleFront.isDestroyed()) {
+ return;
+ }
+
+ const domWalker = (await accessibleFront.targetFront.getFront("inspector"))
+ .walker;
+ if (accessibleFront.isDestroyed()) {
+ return;
+ }
+
+ const node = await domWalker.getNodeFromActor(accessibleFront.actorID, [
+ "rawAccessible",
+ "DOMNode",
+ ]);
+ if (!node) {
+ return;
+ }
+
+ if (node.nodeType == nodeConstants.ELEMENT_NODE) {
+ await node.scrollIntoView();
+ } else if (node.nodeType != nodeConstants.DOCUMENT_NODE) {
+ // scrollIntoView method is only part of the Element interface, in cases
+ // where node is a text node (and not a document node) scroll into view
+ // its parent.
+ await node.parentNode().scrollIntoView();
+ }
+ }
+
+ async highlight(accessibleFront, options, scrollContentNodeIntoView) {
+ this.props.dispatch(unhighlight());
+ // If necessary scroll the node into view before showing the accessibility
+ // highlighter.
+ if (scrollContentNodeIntoView) {
+ await this.scrollNodeIntoViewIfNeeded(accessibleFront);
+ }
+
+ this.props.highlightAccessible(accessibleFront, options);
+ }
+
+ unhighlight(accessibleFront) {
+ this.props.dispatch(unhighlight());
+ this.props.unhighlightAccessible(accessibleFront);
+ }
+
+ async printToJSON() {
+ if (gTelemetry) {
+ gTelemetry.keyedScalarAdd(
+ TELEMETRY_ACCESSIBLE_CONTEXT_MENU_ITEM_ACTIVATED,
+ "print-to-json",
+ 1
+ );
+ }
+
+ const snapshot = await this.props.member.object.snapshot();
+ openDocLink(
+ `${JSON_URL_PREFIX}${encodeURIComponent(JSON.stringify(snapshot))}`
+ );
+ }
+
+ onContextMenu(e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (!this.props.toolboxDoc) {
+ return;
+ }
+
+ const menu = new Menu({ id: "accessibility-row-contextmenu" });
+ menu.append(
+ new MenuItem({
+ id: "menu-printtojson",
+ label: L10N.getStr("accessibility.tree.menu.printToJSON"),
+ click: () => this.printToJSON(),
+ })
+ );
+
+ menu.popup(e.screenX, e.screenY, this.props.toolboxDoc);
+
+ if (gTelemetry) {
+ gTelemetry.scalarAdd(TELEMETRY_ACCESSIBLE_CONTEXT_MENU_OPENED, 1);
+ }
+ }
+
+ /**
+ * Render accessible row component.
+ * @returns acecssible-row React component.
+ */
+ render() {
+ const { member } = this.props;
+ const props = {
+ ...this.props,
+ onContextMenu: e => this.onContextMenu(e),
+ onMouseOver: () => this.highlight(member.object),
+ onMouseOut: () => this.unhighlight(member.object),
+ key: `${member.path}-${member.active ? "active" : "inactive"}`,
+ };
+
+ return AuditController(
+ {
+ accessibleFront: member.object,
+ },
+ AuditFilter({}, HighlightableTreeRow(props))
+ );
+ }
+}
+
+const mapStateToProps = ({
+ ui: { [PREFS.SCROLL_INTO_VIEW]: scrollContentNodeIntoView },
+}) => ({
+ scrollContentNodeIntoView,
+});
+
+module.exports = connect(mapStateToProps, null, null, { withRef: true })(
+ AccessibilityRow
+);
diff --git a/devtools/client/accessibility/components/AccessibilityRowValue.js b/devtools/client/accessibility/components/AccessibilityRowValue.js
new file mode 100644
index 0000000000..ab631fedb4
--- /dev/null
+++ b/devtools/client/accessibility/components/AccessibilityRowValue.js
@@ -0,0 +1,58 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Badges = createFactory(
+ require("resource://devtools/client/accessibility/components/Badges.js")
+);
+const AuditController = createFactory(
+ require("resource://devtools/client/accessibility/components/AuditController.js")
+);
+
+const {
+ REPS,
+} = require("resource://devtools/client/shared/components/reps/index.js");
+const { Grip } = REPS;
+const Rep = createFactory(REPS.Rep);
+
+class AccessibilityRowValue extends Component {
+ static get propTypes() {
+ return {
+ member: PropTypes.shape({
+ object: PropTypes.object,
+ }).isRequired,
+ };
+ }
+
+ render() {
+ return span(
+ {
+ role: "presentation",
+ },
+ Rep({
+ ...this.props,
+ defaultRep: Grip,
+ cropLimit: 50,
+ }),
+ AuditController(
+ {
+ accessibleFront: this.props.member.object,
+ },
+ Badges()
+ )
+ );
+ }
+}
+
+module.exports = AccessibilityRowValue;
diff --git a/devtools/client/accessibility/components/AccessibilityTree.js b/devtools/client/accessibility/components/AccessibilityTree.js
new file mode 100644
index 0000000000..df67377283
--- /dev/null
+++ b/devtools/client/accessibility/components/AccessibilityTree.js
@@ -0,0 +1,298 @@
+/* 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";
+
+/* global EVENTS */
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const TreeView = createFactory(
+ require("resource://devtools/client/shared/components/tree/TreeView.js")
+);
+// Reps
+const {
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/index.js");
+
+const {
+ fetchChildren,
+} = require("resource://devtools/client/accessibility/actions/accessibles.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+const {
+ isFiltered,
+} = require("resource://devtools/client/accessibility/utils/audit.js");
+const AccessibilityRow = createFactory(
+ require("resource://devtools/client/accessibility/components/AccessibilityRow.js")
+);
+const AccessibilityRowValue = createFactory(
+ require("resource://devtools/client/accessibility/components/AccessibilityRowValue.js")
+);
+const {
+ Provider,
+} = require("resource://devtools/client/accessibility/provider.js");
+
+const {
+ scrollIntoView,
+} = require("resource://devtools/client/shared/scroll.js");
+
+/**
+ * Renders Accessibility panel tree.
+ */
+class AccessibilityTree extends Component {
+ static get propTypes() {
+ return {
+ toolboxDoc: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ accessibles: PropTypes.object,
+ expanded: PropTypes.object,
+ selected: PropTypes.string,
+ highlighted: PropTypes.object,
+ filtered: PropTypes.bool,
+ getAccessibilityTreeRoot: PropTypes.func.isRequired,
+ startListeningForAccessibilityEvents: PropTypes.func.isRequired,
+ stopListeningForAccessibilityEvents: PropTypes.func.isRequired,
+ highlightAccessible: PropTypes.func.isRequired,
+ unhighlightAccessible: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onNameChange = this.onNameChange.bind(this);
+ this.onReorder = this.onReorder.bind(this);
+ this.onTextChange = this.onTextChange.bind(this);
+ this.renderValue = this.renderValue.bind(this);
+ this.scrollSelectedRowIntoView = this.scrollSelectedRowIntoView.bind(this);
+ }
+
+ /**
+ * Add accessibility event listeners that affect tree rendering and updates.
+ */
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.props.startListeningForAccessibilityEvents({
+ reorder: this.onReorder,
+ "name-change": this.onNameChange,
+ "text-change": this.onTextChange,
+ });
+ window.on(
+ EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
+ this.scrollSelectedRowIntoView
+ );
+ return null;
+ }
+
+ componentDidUpdate(prevProps) {
+ // When filtering is toggled, make sure that the selected row remains in
+ // view.
+ if (this.props.filtered !== prevProps.filtered) {
+ this.scrollSelectedRowIntoView();
+ }
+
+ window.emit(EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED);
+ }
+
+ /**
+ * Remove accessible event listeners.
+ */
+ componentWillUnmount() {
+ this.props.stopListeningForAccessibilityEvents({
+ reorder: this.onReorder,
+ "name-change": this.onNameChange,
+ "text-change": this.onTextChange,
+ });
+
+ window.off(
+ EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
+ this.scrollSelectedRowIntoView
+ );
+ }
+
+ /**
+ * Handle accessible reorder event. If the accessible is cached and rendered
+ * within the accessibility tree, re-fetch its children and re-render the
+ * corresponding subtree.
+ * @param {Object} accessibleFront
+ * accessible front that had its subtree reordered.
+ */
+ onReorder(accessibleFront) {
+ if (this.props.accessibles.has(accessibleFront.actorID)) {
+ this.props.dispatch(fetchChildren(accessibleFront));
+ }
+ }
+
+ scrollSelectedRowIntoView() {
+ const { treeview } = this.refs;
+ if (!treeview) {
+ return;
+ }
+
+ const treeEl = treeview.treeRef.current;
+ if (!treeEl) {
+ return;
+ }
+
+ const selected = treeEl.ownerDocument.querySelector(
+ ".treeTable .treeRow.selected"
+ );
+ if (selected) {
+ scrollIntoView(selected, { center: true });
+ }
+ }
+
+ /**
+ * Handle accessible name change event. If the name of an accessible changes
+ * and that accessible is cached and rendered within the accessibility tree,
+ * re-fetch its parent's children and re-render the corresponding subtree.
+ * @param {Object} accessibleFront
+ * accessible front that had its name changed.
+ * @param {Object} parentFront
+ * optional parent accessible front. Note: if it parent is not
+ * present, we assume that the top level document's name has changed
+ * and use accessible walker as a parent.
+ */
+ onNameChange(accessibleFront, parentFront) {
+ const { accessibles, dispatch } = this.props;
+ const accessibleWalkerFront = accessibleFront.getParent();
+ parentFront = parentFront || accessibleWalkerFront;
+
+ if (
+ accessibles.has(accessibleFront.actorID) ||
+ accessibles.has(parentFront.actorID)
+ ) {
+ dispatch(fetchChildren(parentFront));
+ }
+ }
+
+ /**
+ * Handle accessible text change (change/insert/remove) event. If the text of
+ * an accessible changes and that accessible is cached and rendered within the
+ * accessibility tree, re-fetch its children and re-render the corresponding
+ * subtree.
+ * @param {Object} accessibleFront
+ * accessible front that had its child text changed.
+ */
+ onTextChange(accessibleFront) {
+ const { accessibles, dispatch } = this.props;
+ if (accessibles.has(accessibleFront.actorID)) {
+ dispatch(fetchChildren(accessibleFront));
+ }
+ }
+
+ renderValue(props) {
+ return AccessibilityRowValue(props);
+ }
+
+ /**
+ * Render Accessibility panel content
+ */
+ render() {
+ const columns = [
+ {
+ id: "default",
+ title: L10N.getStr("accessibility.role"),
+ },
+ {
+ id: "value",
+ title: L10N.getStr("accessibility.name"),
+ },
+ ];
+
+ const {
+ accessibles,
+ dispatch,
+ expanded,
+ selected,
+ highlighted: highlightedItem,
+ toolboxDoc,
+ filtered,
+ getAccessibilityTreeRoot,
+ highlightAccessible,
+ unhighlightAccessible,
+ } = this.props;
+
+ const renderRow = rowProps => {
+ const { object } = rowProps.member;
+ const highlighted = object === highlightedItem;
+ return AccessibilityRow(
+ Object.assign({}, rowProps, {
+ toolboxDoc,
+ highlighted,
+ decorator: {
+ getRowClass() {
+ return highlighted ? ["highlighted"] : [];
+ },
+ },
+ highlightAccessible,
+ unhighlightAccessible,
+ })
+ );
+ };
+ const className = filtered ? "filtered" : undefined;
+
+ return TreeView({
+ ref: "treeview",
+ object: getAccessibilityTreeRoot(),
+ mode: MODE.SHORT,
+ provider: new Provider(accessibles, filtered, dispatch),
+ 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")) {
+ this.toggle(nodePath);
+ }
+
+ this.selectRow(
+ this.rows.find(row => row.props.member.path === nodePath),
+ { preventAutoScroll: true }
+ );
+
+ return true;
+ },
+ onContextMenuTree(e) {
+ // If context menu event is triggered on (or bubbled to) the TreeView, it was
+ // done via keyboard. Open context menu for currently selected row.
+ let row = this.getSelectedRow();
+ if (!row) {
+ return;
+ }
+
+ row = row.getWrappedInstance();
+ row.onContextMenu(e);
+ },
+ });
+ }
+}
+
+const mapStateToProps = ({
+ accessibles,
+ ui: { expanded, selected, highlighted },
+ audit: { filters },
+}) => ({
+ accessibles,
+ expanded,
+ selected,
+ highlighted,
+ filtered: isFiltered(filters),
+});
+// Exports from this module
+module.exports = connect(mapStateToProps)(AccessibilityTree);
diff --git a/devtools/client/accessibility/components/AccessibilityTreeFilter.js b/devtools/client/accessibility/components/AccessibilityTreeFilter.js
new file mode 100644
index 0000000000..90eabd350a
--- /dev/null
+++ b/devtools/client/accessibility/components/AccessibilityTreeFilter.js
@@ -0,0 +1,171 @@
+/* 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";
+
+/* global gTelemetry */
+
+// React
+const {
+ createFactory,
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ hr,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+loader.lazyGetter(this, "MenuButton", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuButton.js")
+ );
+});
+loader.lazyGetter(this, "MenuItem", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuItem.js")
+ );
+});
+loader.lazyGetter(this, "MenuList", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuList.js")
+ );
+});
+
+const actions = require("resource://devtools/client/accessibility/actions/audit.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ FILTERS,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+const TELEMETRY_AUDIT_ACTIVATED = "devtools.accessibility.audit_activated";
+const FILTER_LABELS = {
+ [FILTERS.NONE]: "accessibility.filter.none",
+ [FILTERS.ALL]: "accessibility.filter.all2",
+ [FILTERS.CONTRAST]: "accessibility.filter.contrast",
+ [FILTERS.KEYBOARD]: "accessibility.filter.keyboard",
+ [FILTERS.TEXT_LABEL]: "accessibility.filter.textLabel",
+};
+
+class AccessibilityTreeFilter extends Component {
+ static get propTypes() {
+ return {
+ auditing: PropTypes.array.isRequired,
+ filters: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ describedby: PropTypes.string,
+ toolboxDoc: PropTypes.object.isRequired,
+ audit: PropTypes.func.isRequired,
+ };
+ }
+
+ async toggleFilter(filterKey) {
+ const { audit: auditFunc, dispatch, filters } = this.props;
+
+ if (filterKey !== FILTERS.NONE && !filters[filterKey]) {
+ if (gTelemetry) {
+ gTelemetry.keyedScalarAdd(TELEMETRY_AUDIT_ACTIVATED, filterKey, 1);
+ }
+
+ dispatch(actions.auditing(filterKey));
+ await dispatch(actions.audit(auditFunc, 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) {
+ this.toggleFilter(filterKey);
+ }
+
+ render() {
+ const { auditing, filters, describedby, toolboxDoc } = this.props;
+ const toolbarLabelID = "accessibility-tree-filters-label";
+ const filterNoneChecked = !Object.values(filters).includes(true);
+ const items = [
+ MenuItem({
+ key: FILTERS.NONE,
+ checked: filterNoneChecked,
+ className: `filter ${FILTERS.NONE}`,
+ label: L10N.getStr(FILTER_LABELS[FILTERS.NONE]),
+ onClick: this.onClick.bind(this, FILTERS.NONE),
+ disabled: !!auditing.length,
+ }),
+ hr({ key: "hr-1" }),
+ ];
+
+ const { [FILTERS.ALL]: filterAllChecked, ...filtersWithoutAll } = filters;
+ items.push(
+ MenuItem({
+ key: FILTERS.ALL,
+ checked: filterAllChecked,
+ className: `filter ${FILTERS.ALL}`,
+ label: L10N.getStr(FILTER_LABELS[FILTERS.ALL]),
+ onClick: this.onClick.bind(this, FILTERS.ALL),
+ disabled: !!auditing.length,
+ }),
+ hr({ key: "hr-2" }),
+ Object.entries(filtersWithoutAll).map(([filterKey, active]) =>
+ MenuItem({
+ key: filterKey,
+ checked: active,
+ className: `filter ${filterKey}`,
+ label: L10N.getStr(FILTER_LABELS[filterKey]),
+ onClick: this.onClick.bind(this, filterKey),
+ disabled: !!auditing.length,
+ })
+ )
+ );
+
+ let label;
+ if (filterNoneChecked) {
+ label = L10N.getStr(FILTER_LABELS[FILTERS.NONE]);
+ } else if (filterAllChecked) {
+ label = L10N.getStr(FILTER_LABELS[FILTERS.ALL]);
+ } else {
+ label = Object.keys(filtersWithoutAll)
+ .filter(filterKey => filtersWithoutAll[filterKey])
+ .map(filterKey => L10N.getStr(FILTER_LABELS[filterKey]))
+ .join(", ");
+ }
+
+ return div(
+ {
+ role: "group",
+ className: "accessibility-tree-filters",
+ "aria-labelledby": toolbarLabelID,
+ "aria-describedby": describedby,
+ },
+ span(
+ { id: toolbarLabelID, role: "presentation" },
+ L10N.getStr("accessibility.tree.filters")
+ ),
+ MenuButton(
+ {
+ menuId: "accessibility-tree-filters-menu",
+ toolboxDoc,
+ className: `devtools-button badge toolbar-menu-button filters`,
+ label,
+ },
+ MenuList({}, items)
+ )
+ );
+ }
+}
+
+const mapStateToProps = ({ audit: { filters, auditing } }) => {
+ return { filters, auditing };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(AccessibilityTreeFilter);
diff --git a/devtools/client/accessibility/components/Accessible.js b/devtools/client/accessibility/components/Accessible.js
new file mode 100644
index 0000000000..02467ccaab
--- /dev/null
+++ b/devtools/client/accessibility/components/Accessible.js
@@ -0,0 +1,563 @@
+/* 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";
+
+/* global EVENTS, gTelemetry */
+
+// React & Redux
+const {
+ createFactory,
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ findDOMNode,
+} = require("resource://devtools/client/shared/vendor/react-dom.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ TREE_ROW_HEIGHT,
+ ORDERED_PROPS,
+ ACCESSIBLE_EVENTS,
+ VALUE_FLASHING_DURATION,
+} = require("resource://devtools/client/accessibility/constants.js");
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+const {
+ flashElementOn,
+ flashElementOff,
+} = require("resource://devtools/client/inspector/markup/utils.js");
+const {
+ updateDetails,
+} = require("resource://devtools/client/accessibility/actions/details.js");
+const {
+ select,
+ unhighlight,
+} = require("resource://devtools/client/accessibility/actions/accessibles.js");
+
+const VirtualizedTree = createFactory(
+ require("resource://devtools/client/shared/components/VirtualizedTree.js")
+);
+// Reps
+const {
+ REPS,
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/index.js");
+const { Rep, ElementNode, Accessible: AccessibleRep, Obj } = REPS;
+
+const {
+ translateNodeFrontToGrip,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+
+loader.lazyRequireGetter(
+ this,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+
+const TELEMETRY_NODE_INSPECTED_COUNT =
+ "devtools.accessibility.node_inspected_count";
+
+const TREE_DEPTH_PADDING_INCREMENT = 20;
+
+class AccessiblePropertyClass extends Component {
+ static get propTypes() {
+ return {
+ accessibleFrontActorID: PropTypes.string,
+ object: PropTypes.any,
+ focused: PropTypes.bool,
+ children: PropTypes.func,
+ };
+ }
+
+ componentDidUpdate({
+ object: prevObject,
+ accessibleFrontActorID: prevAccessibleFrontActorID,
+ }) {
+ const { accessibleFrontActorID, object, focused } = this.props;
+ // Fast check if row is focused or if the value did not update.
+ if (
+ focused ||
+ accessibleFrontActorID !== prevAccessibleFrontActorID ||
+ prevObject === object ||
+ (object && prevObject && typeof object === "object")
+ ) {
+ return;
+ }
+
+ this.flashRow();
+ }
+
+ flashRow() {
+ const row = findDOMNode(this);
+ flashElementOn(row);
+ if (this._flashMutationTimer) {
+ clearTimeout(this._flashMutationTimer);
+ this._flashMutationTimer = null;
+ }
+ this._flashMutationTimer = setTimeout(() => {
+ flashElementOff(row);
+ }, VALUE_FLASHING_DURATION);
+ }
+
+ render() {
+ return this.props.children();
+ }
+}
+
+const AccessibleProperty = createFactory(AccessiblePropertyClass);
+
+class Accessible extends Component {
+ static get propTypes() {
+ return {
+ accessibleFront: PropTypes.object,
+ dispatch: PropTypes.func.isRequired,
+ nodeFront: PropTypes.object,
+ items: PropTypes.array,
+ labelledby: PropTypes.string.isRequired,
+ parents: PropTypes.object,
+ relations: PropTypes.object,
+ toolbox: PropTypes.object.isRequired,
+ toolboxHighlighter: PropTypes.object.isRequired,
+ highlightAccessible: PropTypes.func.isRequired,
+ unhighlightAccessible: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ expanded: new Set(),
+ active: null,
+ focused: null,
+ };
+
+ this.onAccessibleInspected = this.onAccessibleInspected.bind(this);
+ this.renderItem = this.renderItem.bind(this);
+ this.update = this.update.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ window.on(
+ EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
+ this.onAccessibleInspected
+ );
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps({ accessibleFront }) {
+ const oldAccessibleFront = this.props.accessibleFront;
+
+ if (oldAccessibleFront) {
+ if (
+ accessibleFront &&
+ accessibleFront.actorID === oldAccessibleFront.actorID
+ ) {
+ return;
+ }
+ ACCESSIBLE_EVENTS.forEach(event =>
+ oldAccessibleFront.off(event, this.update)
+ );
+ }
+
+ if (accessibleFront) {
+ ACCESSIBLE_EVENTS.forEach(event =>
+ accessibleFront.on(event, this.update)
+ );
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (
+ this.props.accessibleFront &&
+ !this.props.accessibleFront.isDestroyed() &&
+ this.props.accessibleFront !== prevProps.accessibleFront
+ ) {
+ window.emit(EVENTS.PROPERTIES_UPDATED);
+ }
+ }
+
+ componentWillUnmount() {
+ window.off(
+ EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
+ this.onAccessibleInspected
+ );
+
+ const { accessibleFront } = this.props;
+ if (accessibleFront) {
+ ACCESSIBLE_EVENTS.forEach(event =>
+ accessibleFront.off(event, this.update)
+ );
+ }
+ }
+
+ onAccessibleInspected() {
+ const { props } = this.refs;
+ if (props) {
+ props.refs.tree.focus();
+ }
+ }
+
+ update() {
+ const { dispatch, accessibleFront } = this.props;
+ if (accessibleFront.isDestroyed()) {
+ return;
+ }
+
+ dispatch(updateDetails(accessibleFront));
+ }
+
+ setExpanded(item, isExpanded) {
+ const { expanded } = this.state;
+
+ if (isExpanded) {
+ expanded.add(item.path);
+ } else {
+ expanded.delete(item.path);
+ }
+
+ this.setState({ expanded });
+ }
+
+ async showHighlighter(nodeFront) {
+ if (!this.props.toolboxHighlighter) {
+ return;
+ }
+
+ await this.props.toolboxHighlighter.highlight(nodeFront);
+ }
+
+ async hideHighlighter() {
+ if (!this.props.toolboxHighlighter) {
+ return;
+ }
+
+ await this.props.toolboxHighlighter.unhighlight();
+ }
+
+ showAccessibleHighlighter(accessibleFront) {
+ this.props.dispatch(unhighlight());
+ this.props.highlightAccessible(accessibleFront);
+ }
+
+ hideAccessibleHighlighter(accessibleFront) {
+ this.props.dispatch(unhighlight());
+ this.props.unhighlightAccessible(accessibleFront);
+ }
+
+ async selectNode(nodeFront, reason = "accessibility") {
+ if (gTelemetry) {
+ gTelemetry.scalarAdd(TELEMETRY_NODE_INSPECTED_COUNT, 1);
+ }
+
+ if (!this.props.toolbox) {
+ return;
+ }
+
+ const inspector = await this.props.toolbox.selectTool("inspector");
+ inspector.selection.setNodeFront(nodeFront, reason);
+ }
+
+ async selectAccessible(accessibleFront) {
+ if (!accessibleFront) {
+ return;
+ }
+
+ await this.props.dispatch(select(accessibleFront));
+
+ const { props } = this.refs;
+ if (props) {
+ props.refs.tree.blur();
+ }
+ await this.setState({ active: null, focused: null });
+
+ window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED);
+ }
+
+ openLink(link, e) {
+ openContentLink(link);
+ }
+
+ renderItem(item, depth, focused, arrow, expanded) {
+ const object = item.contents;
+ const valueProps = {
+ object,
+ mode: MODE.TINY,
+ title: "Object",
+ openLink: this.openLink,
+ };
+
+ if (isNodeFront(object)) {
+ valueProps.defaultRep = ElementNode;
+ valueProps.onDOMNodeMouseOut = () => this.hideHighlighter();
+ valueProps.onDOMNodeMouseOver = () =>
+ this.showHighlighter(this.props.nodeFront);
+
+ valueProps.inspectIconTitle = L10N.getStr(
+ "accessibility.accessible.selectNodeInInspector.title"
+ );
+ valueProps.onInspectIconClick = () =>
+ this.selectNode(this.props.nodeFront);
+ } else if (isAccessibleFront(object)) {
+ const target = findAccessibleTarget(this.props.relations, object.actor);
+ valueProps.defaultRep = AccessibleRep;
+ valueProps.onAccessibleMouseOut = () =>
+ this.hideAccessibleHighlighter(target);
+ valueProps.onAccessibleMouseOver = () =>
+ this.showAccessibleHighlighter(target);
+ valueProps.inspectIconTitle = L10N.getStr(
+ "accessibility.accessible.selectElement.title"
+ );
+ valueProps.onInspectIconClick = (obj, e) => {
+ e.stopPropagation();
+ this.selectAccessible(target);
+ };
+ valueProps.separatorText = "";
+ } else if (item.name === "relations") {
+ valueProps.defaultRep = Obj;
+ } else {
+ valueProps.noGrip = true;
+ }
+
+ const classList = ["node", "object-node"];
+ if (focused) {
+ classList.push("focused");
+ }
+
+ const depthPadding = depth * TREE_DEPTH_PADDING_INCREMENT;
+
+ return AccessibleProperty(
+ {
+ object,
+ focused,
+ accessibleFrontActorID: this.props.accessibleFront.actorID,
+ },
+ () =>
+ div(
+ {
+ className: classList.join(" "),
+ style: {
+ paddingInlineStart: depthPadding,
+ inlineSize: `calc(var(--accessibility-properties-item-width) - ${depthPadding}px)`,
+ },
+ onClick: e => {
+ if (e.target.classList.contains("theme-twisty")) {
+ this.setExpanded(item, !expanded);
+ }
+ },
+ },
+ arrow,
+ span({ className: "object-label" }, item.name),
+ span({ className: "object-delimiter" }, ":"),
+ span({ className: "object-value" }, Rep(valueProps) || "")
+ )
+ );
+ }
+
+ render() {
+ const { expanded, active, focused } = this.state;
+ const { items, parents, accessibleFront, labelledby } = this.props;
+
+ if (accessibleFront) {
+ return VirtualizedTree({
+ ref: "props",
+ key: "accessible-properties",
+ itemHeight: TREE_ROW_HEIGHT,
+ getRoots: () => items,
+ getKey: item => item.path,
+ getParent: item => parents.get(item),
+ getChildren: item => item.children,
+ isExpanded: item => expanded.has(item.path),
+ onExpand: item => this.setExpanded(item, true),
+ onCollapse: item => this.setExpanded(item, false),
+ onFocus: item => {
+ if (this.state.focused !== item.path) {
+ this.setState({ focused: item.path });
+ }
+ },
+ onActivate: item => {
+ if (item == null) {
+ this.setState({ active: null });
+ } else if (this.state.active !== item.path) {
+ this.setState({ active: item.path });
+ }
+ },
+ focused: findByPath(focused, items),
+ active: findByPath(active, items),
+ renderItem: this.renderItem,
+ labelledby,
+ });
+ }
+
+ return div(
+ { className: "info" },
+ L10N.getStr("accessibility.accessible.notAvailable")
+ );
+ }
+}
+
+/**
+ * Match accessibility object from relations targets to the grip that's being activated.
+ * @param {Object} relations Object containing relations grouped by type and targets.
+ * @param {String} actorID Actor ID to match to the relation target.
+ * @return {Object} Accessible front that matches the relation target.
+ */
+const findAccessibleTarget = (relations, actorID) => {
+ for (const relationType in relations) {
+ let targets = relations[relationType];
+ targets = Array.isArray(targets) ? targets : [targets];
+ for (const target of targets) {
+ if (target.actorID === actorID) {
+ return target;
+ }
+ }
+ }
+
+ return null;
+};
+
+/**
+ * Find an item based on a given path.
+ * @param {String} path
+ * Key of the item to be looked up.
+ * @param {Array} items
+ * Accessibility properties array.
+ * @return {Object?}
+ * Possibly found item.
+ */
+const findByPath = (path, items) => {
+ for (const item of items) {
+ if (item.path === path) {
+ return item;
+ }
+
+ const found = findByPath(path, item.children);
+ if (found) {
+ return found;
+ }
+ }
+ return null;
+};
+
+/**
+ * Check if a given property is a DOMNode front.
+ * @param {Object?} value A property to check for being a DOMNode.
+ * @return {Boolean} A flag that indicates whether a property is a DOMNode.
+ */
+const isNodeFront = value => value && value.typeName === "domnode";
+
+/**
+ * Check if a given property is an Accessible front.
+ * @param {Object?} value A property to check for being an Accessible.
+ * @return {Boolean} A flag that indicates whether a property is an Accessible.
+ */
+const isAccessibleFront = value => value && value.typeName === "accessible";
+
+/**
+ * While waiting for a reps fix in https://github.com/firefox-devtools/reps/issues/92,
+ * translate accessibleFront to a grip-like object that can be used with an Accessible
+ * rep.
+ *
+ * @params {accessibleFront} accessibleFront
+ * The AccessibleFront for which we want to create a grip-like object.
+ * @returns {Object} a grip-like object that can be used with Reps.
+ */
+const translateAccessibleFrontToGrip = accessibleFront => ({
+ actor: accessibleFront.actorID,
+ typeName: accessibleFront.typeName,
+ preview: {
+ name: accessibleFront.name,
+ role: accessibleFront.role,
+ // All the grid containers are assumed to be in the Accessibility tree.
+ isConnected: true,
+ },
+});
+
+const translateNodeFrontToGripWrapper = nodeFront => ({
+ ...translateNodeFrontToGrip(nodeFront),
+ typeName: nodeFront.typeName,
+});
+
+/**
+ * Build props ingestible by VirtualizedTree component.
+ * @param {Object} props Component properties to be processed.
+ * @param {String} parentPath Unique path that is used to identify a Tree Node.
+ * @return {Object} Processed properties.
+ */
+const makeItemsForDetails = (props, parentPath) =>
+ Object.getOwnPropertyNames(props).map(name => {
+ let children = [];
+ const path = `${parentPath}/${name}`;
+ let contents = props[name];
+
+ if (contents) {
+ if (isNodeFront(contents)) {
+ contents = translateNodeFrontToGripWrapper(contents);
+ name = "DOMNode";
+ } else if (isAccessibleFront(contents)) {
+ contents = translateAccessibleFrontToGrip(contents);
+ } else if (Array.isArray(contents) || typeof contents === "object") {
+ children = makeItemsForDetails(contents, path);
+ }
+ }
+
+ return { name, path, contents, children };
+ });
+
+const makeParentMap = items => {
+ const map = new WeakMap();
+
+ function _traverse(item) {
+ if (item.children.length) {
+ for (const child of item.children) {
+ map.set(child, item);
+ _traverse(child);
+ }
+ }
+ }
+
+ items.forEach(_traverse);
+ return map;
+};
+
+const mapStateToProps = ({ details }) => {
+ const {
+ accessible: accessibleFront,
+ DOMNode: nodeFront,
+ relations,
+ } = details;
+ if (!accessibleFront || !nodeFront) {
+ return {};
+ }
+
+ const items = makeItemsForDetails(
+ ORDERED_PROPS.reduce((props, key) => {
+ if (key === "DOMNode") {
+ props.nodeFront = nodeFront;
+ } else if (key === "relations") {
+ props.relations = relations;
+ } else {
+ props[key] = accessibleFront[key];
+ }
+
+ return props;
+ }, {}),
+ ""
+ );
+ const parents = makeParentMap(items);
+
+ return { accessibleFront, nodeFront, items, parents, relations };
+};
+
+module.exports = connect(mapStateToProps)(Accessible);
diff --git a/devtools/client/accessibility/components/AuditController.js b/devtools/client/accessibility/components/AuditController.js
new file mode 100644
index 0000000000..e6ada9a0a9
--- /dev/null
+++ b/devtools/client/accessibility/components/AuditController.js
@@ -0,0 +1,90 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+class AuditController extends React.Component {
+ static get propTypes() {
+ return {
+ accessibleFront: PropTypes.object.isRequired,
+ children: PropTypes.any,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ const {
+ accessibleFront: { checks },
+ } = props;
+ this.state = {
+ checks,
+ };
+
+ this.onAudited = this.onAudited.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ const { accessibleFront } = this.props;
+ accessibleFront.on("audited", this.onAudited);
+ }
+
+ componentDidMount() {
+ this.maybeRequestAudit();
+ }
+
+ componentDidUpdate() {
+ this.maybeRequestAudit();
+ }
+
+ componentWillUnmount() {
+ const { accessibleFront } = this.props;
+ accessibleFront.off("audited", this.onAudited);
+ }
+
+ onAudited() {
+ const { accessibleFront } = this.props;
+ if (accessibleFront.isDestroyed()) {
+ // Accessible front is being removed, stop listening for 'audited' events.
+ accessibleFront.off("audited", this.onAudited);
+ return;
+ }
+
+ this.setState({ checks: accessibleFront.checks });
+ }
+
+ maybeRequestAudit() {
+ const { accessibleFront } = this.props;
+ if (accessibleFront.isDestroyed()) {
+ // Accessible front is being removed, stop listening for 'audited' events.
+ accessibleFront.off("audited", this.onAudited);
+ return;
+ }
+
+ if (accessibleFront.checks) {
+ return;
+ }
+
+ accessibleFront.audit().catch(error => {
+ // If the actor was destroyed (due to a connection closed for instance) do
+ // nothing, otherwise log a warning
+ if (!accessibleFront.isDestroyed()) {
+ console.warn(error);
+ }
+ });
+ }
+
+ render() {
+ const { children } = this.props;
+ const { checks } = this.state;
+
+ return React.Children.only(React.cloneElement(children, { checks }));
+ }
+}
+
+module.exports = AuditController;
diff --git a/devtools/client/accessibility/components/AuditFilter.js b/devtools/client/accessibility/components/AuditFilter.js
new file mode 100644
index 0000000000..361a1c30a7
--- /dev/null
+++ b/devtools/client/accessibility/components/AuditFilter.js
@@ -0,0 +1,91 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ isFiltered,
+} = require("resource://devtools/client/accessibility/utils/audit.js");
+const {
+ FILTERS,
+} = require("resource://devtools/client/accessibility/constants.js");
+const {
+ accessibility: {
+ AUDIT_TYPE,
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+function validateCheck({ error, score }) {
+ return !error && [BEST_PRACTICES, FAIL, WARNING].includes(score);
+}
+
+const AUDIT_TYPE_TO_FILTER = {
+ [AUDIT_TYPE.CONTRAST]: {
+ filterKey: FILTERS.CONTRAST,
+ validator: validateCheck,
+ },
+ [AUDIT_TYPE.KEYBOARD]: {
+ filterKey: FILTERS.KEYBOARD,
+ validator: validateCheck,
+ },
+ [AUDIT_TYPE.TEXT_LABEL]: {
+ filterKey: FILTERS.TEXT_LABEL,
+ validator: validateCheck,
+ },
+};
+
+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] &&
+ checks[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);
diff --git a/devtools/client/accessibility/components/AuditProgressOverlay.js b/devtools/client/accessibility/components/AuditProgressOverlay.js
new file mode 100644
index 0000000000..3c16998bcf
--- /dev/null
+++ b/devtools/client/accessibility/components/AuditProgressOverlay.js
@@ -0,0 +1,92 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = React.createFactory(FluentReact.Localized);
+
+/**
+ * Helper functional component to render an accessible text progressbar.
+ * @param {Object} props
+ * - id for the progressbar element
+ * - fluentId: localized string id
+ */
+function TextProgressBar({ id, fluentId }) {
+ return Localized(
+ {
+ id: fluentId,
+ attrs: { "aria-valuetext": true },
+ },
+ ReactDOM.span({
+ id,
+ key: id,
+ role: "progressbar",
+ })
+ );
+}
+
+class AuditProgressOverlay extends React.Component {
+ static get propTypes() {
+ return {
+ auditing: PropTypes.array.isRequired,
+ total: PropTypes.number,
+ percentage: PropTypes.number,
+ };
+ }
+
+ render() {
+ const { auditing, percentage, total } = this.props;
+ if (auditing.length === 0) {
+ return null;
+ }
+
+ const id = "audit-progress-container";
+
+ if (total == null) {
+ return TextProgressBar({
+ id,
+ fluentId: "accessibility-progress-initializing",
+ });
+ }
+
+ if (percentage === 100) {
+ return TextProgressBar({
+ id,
+ fluentId: "accessibility-progress-finishing",
+ });
+ }
+
+ return ReactDOM.span(
+ {
+ id,
+ key: id,
+ },
+ Localized({
+ id: "accessibility-progress-progressbar",
+ $nodeCount: total,
+ }),
+ ReactDOM.progress({
+ max: 100,
+ value: percentage,
+ className: "audit-progress-progressbar",
+ "aria-labelledby": id,
+ })
+ );
+ }
+}
+
+const mapStateToProps = ({ audit: { auditing, progress } }) => {
+ const { total, percentage } = progress || {};
+ return { auditing, total, percentage };
+};
+
+module.exports = connect(mapStateToProps)(AuditProgressOverlay);
diff --git a/devtools/client/accessibility/components/Badge.js b/devtools/client/accessibility/components/Badge.js
new file mode 100644
index 0000000000..207bb96f55
--- /dev/null
+++ b/devtools/client/accessibility/components/Badge.js
@@ -0,0 +1,40 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+class Badge extends Component {
+ static get propTypes() {
+ return {
+ score: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ ariaLabel: PropTypes.string,
+ tooltip: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { score, label, ariaLabel, tooltip } = this.props;
+
+ return span(
+ {
+ className: `audit-badge badge`,
+ "data-score": score,
+ title: tooltip,
+ "aria-label": ariaLabel || label,
+ },
+ label
+ );
+ }
+}
+
+module.exports = Badge;
diff --git a/devtools/client/accessibility/components/Badges.js b/devtools/client/accessibility/components/Badges.js
new file mode 100644
index 0000000000..00079d21b6
--- /dev/null
+++ b/devtools/client/accessibility/components/Badges.js
@@ -0,0 +1,88 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ accessibility: { AUDIT_TYPE },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyGetter(this, "ContrastBadge", () =>
+ createFactory(
+ require("resource://devtools/client/accessibility/components/ContrastBadge.js")
+ )
+);
+
+loader.lazyGetter(this, "KeyboardBadge", () =>
+ createFactory(
+ require("resource://devtools/client/accessibility/components/KeyboardBadge.js")
+ )
+);
+
+loader.lazyGetter(this, "TextLabelBadge", () =>
+ createFactory(
+ require("resource://devtools/client/accessibility/components/TextLabelBadge.js")
+ )
+);
+
+function getComponentForAuditType(type) {
+ const auditTypeToComponentMap = {
+ [AUDIT_TYPE.CONTRAST]: ContrastBadge,
+ [AUDIT_TYPE.KEYBOARD]: KeyboardBadge,
+ [AUDIT_TYPE.TEXT_LABEL]: TextLabelBadge,
+ };
+
+ return auditTypeToComponentMap[type];
+}
+
+class Badges extends Component {
+ static get propTypes() {
+ return {
+ checks: PropTypes.object,
+ };
+ }
+
+ render() {
+ const { checks } = this.props;
+ if (!checks) {
+ return null;
+ }
+
+ const items = [];
+ for (const type in checks) {
+ const component = getComponentForAuditType(type);
+ if (checks[type] && component) {
+ items.push(component({ key: type, ...checks[type] }));
+ }
+ }
+
+ if (items.length === 0) {
+ return null;
+ }
+
+ return span(
+ {
+ className: "badges",
+ role: "group",
+ "aria-label": L10N.getStr("accessibility.badges"),
+ },
+ items
+ );
+ }
+}
+
+module.exports = Badges;
diff --git a/devtools/client/accessibility/components/Button.js b/devtools/client/accessibility/components/Button.js
new file mode 100644
index 0000000000..92cd90a56c
--- /dev/null
+++ b/devtools/client/accessibility/components/Button.js
@@ -0,0 +1,112 @@
+/* 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 {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ button,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const defaultProps = {
+ disabled: false,
+ busy: false,
+ title: null,
+ children: null,
+ className: "",
+};
+
+/**
+ * Button component that handles keyboard in an accessible way. When user
+ * uses the mouse to hover/click on the button, there is no focus
+ * highlighting. However if the user uses a keyboard to focus on the button,
+ * it will have focus highlighting in the form of an outline.
+ */
+class Button extends Component {
+ static get propTypes() {
+ return {
+ disabled: PropTypes.bool,
+ busy: PropTypes.bool,
+ title: PropTypes.string,
+ children: PropTypes.string,
+ className: PropTypes.string,
+ };
+ }
+
+ static get defaultProps() {
+ return defaultProps;
+ }
+
+ render() {
+ const className = [
+ ...this.props.className.split(" "),
+ "devtools-button",
+ ].join(" ");
+ const props = Object.assign({}, this.props, {
+ className,
+ "aria-busy": this.props.busy.toString(),
+ busy: this.props.busy.toString(),
+ });
+
+ const classList = ["btn-content"];
+ if (this.props.busy) {
+ classList.push("devtools-throbber");
+ }
+
+ return button(
+ props,
+ span(
+ {
+ className: classList.join(" "),
+ tabIndex: -1,
+ },
+ this.props.children
+ )
+ );
+ }
+}
+
+function ToggleButton(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 = {
+ Button,
+ ToggleButton,
+};
diff --git a/devtools/client/accessibility/components/Check.js b/devtools/client/accessibility/components/Check.js
new file mode 100644
index 0000000000..3ca6c9c39a
--- /dev/null
+++ b/devtools/client/accessibility/components/Check.js
@@ -0,0 +1,157 @@
+/* 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,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const {
+ accessibility: {
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+/**
+ * A map of accessibility scores to the text descriptions of check icons.
+ */
+const SCORE_TO_ICON_MAP = {
+ [BEST_PRACTICES]: {
+ l10nId: "accessibility-best-practices",
+ src: "chrome://devtools/skin/images/info.svg",
+ },
+ [FAIL]: {
+ l10nId: "accessibility-fail",
+ src: "chrome://devtools/skin/images/error.svg",
+ },
+ [WARNING]: {
+ l10nId: "accessibility-warning",
+ src: "chrome://devtools/skin/images/alert.svg",
+ },
+};
+
+/**
+ * Localized "Learn more" link that opens a new tab with relevant documentation.
+ */
+class LearnMoreClass extends PureComponent {
+ static get propTypes() {
+ return {
+ href: PropTypes.string,
+ l10nId: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ href: "#",
+ l10nId: null,
+ onClick: LearnMoreClass.openDocOnClick,
+ };
+ }
+
+ static openDocOnClick(event) {
+ event.preventDefault();
+ openDocLink(event.target.href);
+ }
+
+ render() {
+ const { href, l10nId, onClick } = this.props;
+ const className = "link";
+
+ return Localized({ id: l10nId }, ReactDOM.a({ className, href, onClick }));
+ }
+}
+
+const LearnMore = createFactory(LearnMoreClass);
+
+/**
+ * Renders icon with text description for the accessibility check.
+ *
+ * @param {Object}
+ * Options:
+ * - score: value from SCORES from "devtools/shared/constants"
+ */
+function Icon({ score }) {
+ const { l10nId, src } = SCORE_TO_ICON_MAP[score];
+
+ return Localized(
+ { id: l10nId, attrs: { alt: true } },
+ ReactDOM.img({ src, "data-score": score, className: "icon" })
+ );
+}
+
+/**
+ * Renders text description of the accessibility check.
+ *
+ * @param {Object}
+ * Options:
+ * - args: arguments for fluent localized string
+ * - href: url for the learn more link pointing to MDN
+ * - l10nId: fluent localization id
+ */
+function Annotation({ args, href, l10nId }) {
+ return Localized(
+ {
+ id: l10nId,
+ a: LearnMore({ l10nId: "accessibility-learn-more", href }),
+ ...args,
+ },
+ ReactDOM.p({ className: "accessibility-check-annotation" })
+ );
+}
+
+/**
+ * Component for rendering a check for accessibliity checks section,
+ * warnings and best practices suggestions association with a given
+ * accessibility object in the accessibility tree.
+ */
+class Check extends Component {
+ static get propTypes() {
+ return {
+ getAnnotation: PropTypes.func.isRequired,
+ id: PropTypes.string.isRequired,
+ issue: PropTypes.string.isRequired,
+ score: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { getAnnotation, id, issue, score } = this.props;
+
+ return ReactDOM.div(
+ {
+ role: "presentation",
+ tabIndex: "-1",
+ className: "accessibility-check",
+ },
+ Localized(
+ {
+ id,
+ },
+ ReactDOM.h3({ className: "accessibility-check-header" })
+ ),
+ ReactDOM.div(
+ {
+ role: "presentation",
+ tabIndex: "-1",
+ },
+ Icon({ score }),
+ Annotation({ ...getAnnotation(issue) })
+ )
+ );
+ }
+}
+
+module.exports = Check;
diff --git a/devtools/client/accessibility/components/Checks.js b/devtools/client/accessibility/components/Checks.js
new file mode 100644
index 0000000000..f2b4e4835a
--- /dev/null
+++ b/devtools/client/accessibility/components/Checks.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";
+
+// React
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const List = createFactory(
+ require("resource://devtools/client/shared/components/List.js").List
+);
+const ColorContrastCheck = createFactory(
+ require("resource://devtools/client/accessibility/components/ColorContrastAccessibility.js")
+ .ColorContrastCheck
+);
+const TextLabelCheck = createFactory(
+ require("resource://devtools/client/accessibility/components/TextLabelCheck.js")
+);
+const KeyboardCheck = createFactory(
+ require("resource://devtools/client/accessibility/components/KeyboardCheck.js")
+);
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ accessibility: { AUDIT_TYPE },
+} = require("resource://devtools/shared/constants.js");
+
+function EmptyChecks() {
+ return div(
+ {
+ className: "checks-empty",
+ role: "presentation",
+ tabIndex: "-1",
+ },
+ L10N.getStr("accessibility.checks.empty2")
+ );
+}
+
+// Component that is responsible for rendering accessible audit data in the a11y panel
+// sidebar.
+class Checks extends Component {
+ static get propTypes() {
+ return {
+ audit: PropTypes.object,
+ labelledby: PropTypes.string.isRequired,
+ };
+ }
+
+ [AUDIT_TYPE.CONTRAST](contrastRatio) {
+ return ColorContrastCheck(contrastRatio);
+ }
+
+ [AUDIT_TYPE.KEYBOARD](keyboardCheck) {
+ return KeyboardCheck(keyboardCheck);
+ }
+
+ [AUDIT_TYPE.TEXT_LABEL](textLabelCheck) {
+ return TextLabelCheck(textLabelCheck);
+ }
+
+ render() {
+ const { audit, labelledby } = this.props;
+ if (!audit) {
+ return EmptyChecks();
+ }
+
+ const items = [];
+ for (const name in audit) {
+ // There are going to be various audit reports for this object, sent by the server.
+ // Iterate over them and delegate rendering to the method with the corresponding
+ // name.
+ if (audit[name] && this[name]) {
+ items.push({
+ component: this[name](audit[name]),
+ className: name,
+ key: name,
+ });
+ }
+ }
+
+ if (items.length === 0) {
+ return EmptyChecks();
+ }
+
+ return div(
+ {
+ className: "checks",
+ role: "presentation",
+ tabIndex: "-1",
+ },
+ List({ items, labelledby })
+ );
+ }
+}
+
+const mapStateToProps = ({ details, ui }) => {
+ const { audit } = details;
+ if (!audit) {
+ return {};
+ }
+
+ return { audit };
+};
+
+module.exports = connect(mapStateToProps)(Checks);
diff --git a/devtools/client/accessibility/components/ColorContrastAccessibility.js b/devtools/client/accessibility/components/ColorContrastAccessibility.js
new file mode 100644
index 0000000000..117ab67593
--- /dev/null
+++ b/devtools/client/accessibility/components/ColorContrastAccessibility.js
@@ -0,0 +1,229 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ div,
+ span,
+ h3,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const LearnMoreLink = createFactory(
+ require("resource://devtools/client/accessibility/components/LearnMoreLink.js")
+);
+
+const {
+ A11Y_CONTRAST_LEARN_MORE_LINK,
+} = require("resource://devtools/client/accessibility/constants.js");
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+/**
+ * Component that renders a colour contrast value along with a swatch preview of what the
+ * text and background colours are.
+ */
+class ContrastValueClass extends Component {
+ static get propTypes() {
+ return {
+ backgroundColor: PropTypes.array.isRequired,
+ color: PropTypes.array.isRequired,
+ value: PropTypes.number.isRequired,
+ score: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { backgroundColor, color, value, score } = this.props;
+
+ const className = ["accessibility-contrast-value", score].join(" ");
+
+ return span(
+ {
+ className,
+ role: "presentation",
+ style: {
+ "--accessibility-contrast-color": `rgba(${color})`,
+ "--accessibility-contrast-bg": `rgba(${backgroundColor})`,
+ },
+ },
+ value.toFixed(2)
+ );
+ }
+}
+
+const ContrastValue = createFactory(ContrastValueClass);
+
+/**
+ * Component that renders labeled colour contrast values together with the large text
+ * indiscator.
+ */
+class ColorContrastAccessibilityClass extends Component {
+ static get propTypes() {
+ return {
+ error: PropTypes.string,
+ isLargeText: PropTypes.bool.isRequired,
+ color: PropTypes.array.isRequired,
+ value: PropTypes.number,
+ min: PropTypes.number,
+ max: PropTypes.number,
+ backgroundColor: PropTypes.array,
+ backgroundColorMin: PropTypes.array,
+ backgroundColorMax: PropTypes.array,
+ score: PropTypes.string,
+ scoreMin: PropTypes.string,
+ scoreMax: PropTypes.string,
+ };
+ }
+
+ render() {
+ const {
+ error,
+ isLargeText,
+ color,
+ value,
+ backgroundColor,
+ score,
+ min,
+ backgroundColorMin,
+ scoreMin,
+ max,
+ backgroundColorMax,
+ scoreMax,
+ } = this.props;
+
+ const children = [];
+
+ if (error) {
+ children.push(
+ span(
+ {
+ className: "accessibility-color-contrast-error",
+ role: "presentation",
+ },
+ L10N.getStr("accessibility.contrast.error")
+ )
+ );
+
+ return div(
+ {
+ role: "presentation",
+ tabIndex: "-1",
+ className: "accessibility-color-contrast",
+ },
+ ...children
+ );
+ }
+
+ if (value) {
+ children.push(ContrastValue({ score, color, backgroundColor, value }));
+ } else {
+ children.push(
+ ContrastValue({
+ score: scoreMin,
+ color,
+ backgroundColor: backgroundColorMin,
+ value: min,
+ }),
+ div({
+ role: "presentation",
+ tabIndex: "-1",
+ className: "accessibility-color-contrast-separator",
+ }),
+ ContrastValue({
+ score: scoreMax,
+ color,
+ backgroundColor: backgroundColorMax,
+ value: max,
+ })
+ );
+ }
+
+ if (isLargeText) {
+ children.push(
+ span(
+ {
+ className: "accessibility-color-contrast-large-text",
+ role: "presentation",
+ title: L10N.getStr("accessibility.contrast.large.title"),
+ },
+ L10N.getStr("accessibility.contrast.large.text")
+ )
+ );
+ }
+
+ return div(
+ {
+ role: "presentation",
+ tabIndex: "-1",
+ className: "accessibility-color-contrast",
+ },
+ ...children
+ );
+ }
+}
+
+const ColorContrastAccessibility = createFactory(
+ ColorContrastAccessibilityClass
+);
+
+class ContrastAnnotationClass extends Component {
+ static get propTypes() {
+ return {
+ score: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { score } = this.props;
+
+ return LearnMoreLink({
+ className: "accessibility-check-annotation",
+ href: A11Y_CONTRAST_LEARN_MORE_LINK,
+ learnMoreStringKey: "accessibility.learnMore",
+ l10n: L10N,
+ messageStringKey: `accessibility.contrast.annotation.${score}`,
+ });
+ }
+}
+
+const ContrastAnnotation = createFactory(ContrastAnnotationClass);
+
+class ColorContrastCheck extends Component {
+ static get propTypes() {
+ return {
+ error: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { error } = this.props;
+
+ return div(
+ {
+ role: "presentation",
+ tabIndex: "-1",
+ className: "accessibility-check",
+ },
+ h3(
+ {
+ className: "accessibility-check-header",
+ },
+ L10N.getStr("accessibility.contrast.header")
+ ),
+ ColorContrastAccessibility(this.props),
+ !error && ContrastAnnotation(this.props)
+ );
+ }
+}
+
+module.exports = {
+ ColorContrastAccessibility: ColorContrastAccessibilityClass,
+ ColorContrastCheck,
+};
diff --git a/devtools/client/accessibility/components/ContrastBadge.js b/devtools/client/accessibility/components/ContrastBadge.js
new file mode 100644
index 0000000000..ee37958f1e
--- /dev/null
+++ b/devtools/client/accessibility/components/ContrastBadge.js
@@ -0,0 +1,55 @@
+/* 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,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ accessibility: { SCORES },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyGetter(this, "Badge", () =>
+ createFactory(
+ require("resource://devtools/client/accessibility/components/Badge.js")
+ )
+);
+
+/**
+ * Component for rendering a badge for contrast accessibliity check
+ * failures association with a given accessibility object in the accessibility
+ * tree.
+ */
+class ContrastBadge extends PureComponent {
+ static get propTypes() {
+ return {
+ error: PropTypes.string,
+ score: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { error, score } = this.props;
+ if (error || score !== SCORES.FAIL) {
+ return null;
+ }
+
+ return Badge({
+ score,
+ label: L10N.getStr("accessibility.badge.contrast"),
+ ariaLabel: L10N.getStr("accessibility.badge.contrast.warning"),
+ tooltip: L10N.getStr("accessibility.badge.contrast.tooltip"),
+ });
+ }
+}
+
+module.exports = ContrastBadge;
diff --git a/devtools/client/accessibility/components/Description.js b/devtools/client/accessibility/components/Description.js
new file mode 100644
index 0000000000..a31dbcbfa6
--- /dev/null
+++ b/devtools/client/accessibility/components/Description.js
@@ -0,0 +1,56 @@
+/* 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 & Redux
+const {
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+ p,
+ img,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const LearnMoreLink = createFactory(
+ require("resource://devtools/client/accessibility/components/LearnMoreLink.js")
+);
+
+// Localization
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ A11Y_LEARN_MORE_LINK,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+/**
+ * Landing UI for the accessibility panel when Accessibility features are
+ * deactivated.
+ */
+function Description() {
+ return div(
+ { className: "description", role: "presentation", tabIndex: "-1" },
+ div(
+ { className: "general", role: "presentation", tabIndex: "-1" },
+ img({
+ src: "chrome://devtools/skin/images/accessibility.svg",
+ alt: L10N.getStr("accessibility.logo"),
+ }),
+ div(
+ { role: "presentation", tabIndex: "-1" },
+ LearnMoreLink({
+ href: A11Y_LEARN_MORE_LINK,
+ learnMoreStringKey: "accessibility.learnMore",
+ l10n: L10N,
+ messageStringKey: "accessibility.description.general.p1",
+ }),
+ p({}, L10N.getStr("accessibility.enable.disabledTitle"))
+ )
+ )
+ );
+}
+
+// Exports from this module
+exports.Description = Description;
diff --git a/devtools/client/accessibility/components/DisplayTabbingOrder.js b/devtools/client/accessibility/components/DisplayTabbingOrder.js
new file mode 100644
index 0000000000..f6e3fdf453
--- /dev/null
+++ b/devtools/client/accessibility/components/DisplayTabbingOrder.js
@@ -0,0 +1,79 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ label,
+ input,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ updateDisplayTabbingOrder,
+} = require("resource://devtools/client/accessibility/actions/ui.js");
+
+class DisplayTabbingOrder extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ tabbingOrderDisplayed: PropTypes.bool.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ disabled: false,
+ };
+
+ this.onChange = this.onChange.bind(this);
+ }
+
+ async onChange() {
+ const { dispatch, tabbingOrderDisplayed } = this.props;
+
+ this.setState({ disabled: true });
+ await dispatch(updateDisplayTabbingOrder(!tabbingOrderDisplayed));
+ this.setState({ disabled: false });
+ }
+
+ render() {
+ const { tabbingOrderDisplayed } = this.props;
+ return label(
+ {
+ className:
+ "accessibility-tabbing-order devtools-checkbox-label devtools-ellipsis-text",
+ htmlFor: "devtools-display-tabbing-order-checkbox",
+ title: L10N.getStr("accessibility.toolbar.displayTabbingOrder.tooltip"),
+ },
+ input({
+ id: "devtools-display-tabbing-order-checkbox",
+ className: "devtools-checkbox",
+ type: "checkbox",
+ checked: tabbingOrderDisplayed,
+ disabled: this.state.disabled,
+ onChange: this.onChange,
+ }),
+ L10N.getStr("accessibility.toolbar.displayTabbingOrder.label")
+ );
+ }
+}
+
+const mapStateToProps = ({ ui: { tabbingOrderDisplayed } }) => ({
+ tabbingOrderDisplayed,
+});
+
+module.exports = connect(mapStateToProps)(DisplayTabbingOrder);
diff --git a/devtools/client/accessibility/components/KeyboardBadge.js b/devtools/client/accessibility/components/KeyboardBadge.js
new file mode 100644
index 0000000000..ac6b6b8841
--- /dev/null
+++ b/devtools/client/accessibility/components/KeyboardBadge.js
@@ -0,0 +1,56 @@
+/* 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,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ accessibility: {
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyGetter(this, "Badge", () =>
+ createFactory(
+ require("resource://devtools/client/accessibility/components/Badge.js")
+ )
+);
+
+/**
+ * Component for rendering a badge for keyboard accessibliity check failures
+ * association with a given accessibility object in the accessibility tree.
+ */
+class KeyboardBadge extends PureComponent {
+ static get propTypes() {
+ return {
+ error: PropTypes.string,
+ score: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { error, score } = this.props;
+ if (error || ![BEST_PRACTICES, FAIL, WARNING].includes(score)) {
+ return null;
+ }
+
+ return Badge({
+ score,
+ label: L10N.getStr("accessibility.badge.keyboard"),
+ tooltip: L10N.getStr("accessibility.badge.keyboard.tooltip"),
+ });
+ }
+}
+
+module.exports = KeyboardBadge;
diff --git a/devtools/client/accessibility/components/KeyboardCheck.js b/devtools/client/accessibility/components/KeyboardCheck.js
new file mode 100644
index 0000000000..0109dd22b0
--- /dev/null
+++ b/devtools/client/accessibility/components/KeyboardCheck.js
@@ -0,0 +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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Check = createFactory(
+ require("resource://devtools/client/accessibility/components/Check.js")
+);
+
+const {
+ A11Y_KEYBOARD_LINKS,
+} = require("resource://devtools/client/accessibility/constants.js");
+const {
+ accessibility: {
+ AUDIT_TYPE: { KEYBOARD },
+ ISSUE_TYPE: {
+ [KEYBOARD]: {
+ FOCUSABLE_NO_SEMANTICS,
+ FOCUSABLE_POSITIVE_TABINDEX,
+ INTERACTIVE_NO_ACTION,
+ INTERACTIVE_NOT_FOCUSABLE,
+ MOUSE_INTERACTIVE_ONLY,
+ NO_FOCUS_VISIBLE,
+ },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+/**
+ * A map from text label issues to annotation component properties.
+ */
+const ISSUE_TO_ANNOTATION_MAP = {
+ [FOCUSABLE_NO_SEMANTICS]: {
+ href: A11Y_KEYBOARD_LINKS.FOCUSABLE_NO_SEMANTICS,
+ l10nId: "accessibility-keyboard-issue-semantics",
+ },
+ [FOCUSABLE_POSITIVE_TABINDEX]: {
+ href: A11Y_KEYBOARD_LINKS.FOCUSABLE_POSITIVE_TABINDEX,
+ l10nId: "accessibility-keyboard-issue-tabindex",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "tabindex");
+ },
+ },
+ },
+ [INTERACTIVE_NO_ACTION]: {
+ href: A11Y_KEYBOARD_LINKS.INTERACTIVE_NO_ACTION,
+ l10nId: "accessibility-keyboard-issue-action",
+ },
+ [INTERACTIVE_NOT_FOCUSABLE]: {
+ href: A11Y_KEYBOARD_LINKS.INTERACTIVE_NOT_FOCUSABLE,
+ l10nId: "accessibility-keyboard-issue-focusable",
+ },
+ [MOUSE_INTERACTIVE_ONLY]: {
+ href: A11Y_KEYBOARD_LINKS.MOUSE_INTERACTIVE_ONLY,
+ l10nId: "accessibility-keyboard-issue-mouse-only",
+ },
+ [NO_FOCUS_VISIBLE]: {
+ href: A11Y_KEYBOARD_LINKS.NO_FOCUS_VISIBLE,
+ l10nId: "accessibility-keyboard-issue-focus-visible",
+ },
+};
+
+/**
+ * Component for rendering a check for text label accessibliity check failures,
+ * warnings and best practices suggestions association with a given
+ * accessibility object in the accessibility tree.
+ */
+class KeyboardCheck extends PureComponent {
+ static get propTypes() {
+ return {
+ issue: PropTypes.string.isRequired,
+ score: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ return Check({
+ ...this.props,
+ getAnnotation: issue => ISSUE_TO_ANNOTATION_MAP[issue],
+ id: "accessibility-keyboard-header",
+ });
+ }
+}
+
+module.exports = KeyboardCheck;
diff --git a/devtools/client/accessibility/components/LearnMoreLink.js b/devtools/client/accessibility/components/LearnMoreLink.js
new file mode 100644
index 0000000000..2a21df4ea5
--- /dev/null
+++ b/devtools/client/accessibility/components/LearnMoreLink.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 {
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ p,
+ a,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+/**
+ * Localization friendly component for rendering a block of text with a "Learn more" link
+ * as a part of it.
+ */
+class LearnMoreLink extends Component {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ href: PropTypes.string,
+ learnMoreStringKey: PropTypes.string.isRequired,
+ l10n: PropTypes.object.isRequired,
+ messageStringKey: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ href: "#",
+ learnMoreStringKey: null,
+ l10n: null,
+ messageStringKey: null,
+ onClick: LearnMoreLink.openDocOnClick,
+ };
+ }
+
+ static openDocOnClick(event) {
+ event.preventDefault();
+ openDocLink(event.target.href);
+ }
+
+ render() {
+ const {
+ className,
+ href,
+ learnMoreStringKey,
+ l10n,
+ messageStringKey,
+ onClick,
+ } = this.props;
+ const learnMoreString = l10n.getStr(learnMoreStringKey);
+ const messageString = l10n.getFormatStr(messageStringKey, learnMoreString);
+
+ // Split the paragraph string with the link as a separator, and include the link into
+ // results.
+ const re = new RegExp(`(\\b${learnMoreString}\\b)`);
+ const contents = messageString.split(re);
+ contents[1] = a({ className: "link", href, onClick }, contents[1]);
+
+ return p(
+ {
+ className,
+ },
+ ...contents
+ );
+ }
+}
+
+module.exports = LearnMoreLink;
diff --git a/devtools/client/accessibility/components/MainFrame.js b/devtools/client/accessibility/components/MainFrame.js
new file mode 100644
index 0000000000..d918dc7be0
--- /dev/null
+++ b/devtools/client/accessibility/components/MainFrame.js
@@ -0,0 +1,245 @@
+/* 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 & Redux
+const {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ span,
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ enable,
+ reset,
+ updateCanBeEnabled,
+ updateCanBeDisabled,
+} = require("resource://devtools/client/accessibility/actions/ui.js");
+
+// Localization
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
+
+// Constants
+const {
+ SIDEBAR_WIDTH,
+ PORTRAIT_MODE_WIDTH,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+// Accessibility Panel
+const AccessibilityTree = createFactory(
+ require("resource://devtools/client/accessibility/components/AccessibilityTree.js")
+);
+const AuditProgressOverlay = createFactory(
+ require("resource://devtools/client/accessibility/components/AuditProgressOverlay.js")
+);
+const Description = createFactory(
+ require("resource://devtools/client/accessibility/components/Description.js")
+ .Description
+);
+const RightSidebar = createFactory(
+ require("resource://devtools/client/accessibility/components/RightSidebar.js")
+);
+const Toolbar = createFactory(
+ require("resource://devtools/client/accessibility/components/Toolbar.js")
+ .Toolbar
+);
+const SplitBox = createFactory(
+ require("resource://devtools/client/shared/components/splitter/SplitBox.js")
+);
+
+/**
+ * Renders basic layout of the Accessibility panel. The Accessibility panel
+ * content consists of two main parts: tree and sidebar.
+ */
+class MainFrame extends Component {
+ static get propTypes() {
+ return {
+ fluentBundles: PropTypes.array.isRequired,
+ enabled: PropTypes.bool.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ auditing: PropTypes.array.isRequired,
+ supports: PropTypes.object,
+ toolbox: PropTypes.object.isRequired,
+ getAccessibilityTreeRoot: PropTypes.func.isRequired,
+ startListeningForAccessibilityEvents: PropTypes.func.isRequired,
+ stopListeningForAccessibilityEvents: PropTypes.func.isRequired,
+ audit: PropTypes.func.isRequired,
+ simulate: PropTypes.func,
+ enableAccessibility: PropTypes.func.isRequired,
+ resetAccessiblity: PropTypes.func.isRequired,
+ startListeningForLifecycleEvents: PropTypes.func.isRequired,
+ stopListeningForLifecycleEvents: PropTypes.func.isRequired,
+ startListeningForParentLifecycleEvents: PropTypes.func.isRequired,
+ stopListeningForParentLifecycleEvents: PropTypes.func.isRequired,
+ highlightAccessible: PropTypes.func.isRequired,
+ unhighlightAccessible: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.resetAccessibility = this.resetAccessibility.bind(this);
+ this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
+ this.onCanBeEnabledChange = this.onCanBeEnabledChange.bind(this);
+ this.onCanBeDisabledChange = this.onCanBeDisabledChange.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.props.startListeningForLifecycleEvents({
+ init: this.resetAccessibility,
+ shutdown: this.resetAccessibility,
+ });
+ this.props.startListeningForParentLifecycleEvents({
+ "can-be-enabled-change": this.onCanBeEnabledChange,
+ "can-be-disabled-change": this.onCanBeDisabledChange,
+ });
+ this.props.startListeningForAccessibilityEvents({
+ "top-level-document-ready": this.resetAccessibility,
+ });
+ window.addEventListener("resize", this.onPanelWindowResize, true);
+ }
+
+ componentWillUnmount() {
+ this.props.stopListeningForLifecycleEvents({
+ init: this.resetAccessibility,
+ shutdown: this.resetAccessibility,
+ });
+ this.props.stopListeningForParentLifecycleEvents({
+ "can-be-enabled-change": this.onCanBeEnabledChange,
+ "can-be-disabled-change": this.onCanBeDisabledChange,
+ });
+ this.props.stopListeningForAccessibilityEvents({
+ "top-level-document-ready": this.resetAccessibility,
+ });
+ window.removeEventListener("resize", this.onPanelWindowResize, true);
+ }
+
+ resetAccessibility() {
+ const { dispatch, resetAccessiblity, supports } = this.props;
+ dispatch(reset(resetAccessiblity, supports));
+ }
+
+ onCanBeEnabledChange(canBeEnabled) {
+ const { enableAccessibility, dispatch } = this.props;
+ dispatch(updateCanBeEnabled(canBeEnabled));
+ if (canBeEnabled) {
+ dispatch(enable(enableAccessibility));
+ }
+ }
+
+ onCanBeDisabledChange(canBeDisabled) {
+ this.props.dispatch(updateCanBeDisabled(canBeDisabled));
+ }
+
+ get useLandscapeMode() {
+ const { clientWidth } = document.getElementById("content");
+ return clientWidth > PORTRAIT_MODE_WIDTH;
+ }
+
+ /**
+ * If panel width is less than PORTRAIT_MODE_WIDTH px, the splitter changes
+ * its mode to `horizontal` to support portrait view.
+ */
+ onPanelWindowResize() {
+ if (this.refs.splitBox) {
+ this.refs.splitBox.setState({ vert: this.useLandscapeMode });
+ }
+ }
+
+ /**
+ * Render Accessibility panel content
+ */
+ render() {
+ const {
+ fluentBundles,
+ enabled,
+ auditing,
+ simulate,
+ toolbox,
+ getAccessibilityTreeRoot,
+ startListeningForAccessibilityEvents,
+ stopListeningForAccessibilityEvents,
+ audit,
+ highlightAccessible,
+ unhighlightAccessible,
+ } = this.props;
+
+ if (!enabled) {
+ return Description();
+ }
+
+ // Audit is currently running.
+ const isAuditing = !!auditing.length;
+
+ return LocalizationProvider(
+ { bundles: fluentBundles },
+ div(
+ { className: "mainFrame", role: "presentation", tabIndex: "-1" },
+ Toolbar({
+ audit,
+ simulate,
+ toolboxDoc: toolbox.doc,
+ }),
+ isAuditing && AuditProgressOverlay(),
+ span(
+ {
+ "aria-hidden": isAuditing,
+ role: "presentation",
+ style: { display: "contents" },
+ },
+ SplitBox({
+ ref: "splitBox",
+ initialSize: SIDEBAR_WIDTH,
+ minSize: "10%",
+ maxSize: "80%",
+ splitterSize: 1,
+ endPanelControl: true,
+ startPanel: div(
+ {
+ className: "main-panel",
+ role: "presentation",
+ tabIndex: "-1",
+ },
+ AccessibilityTree({
+ toolboxDoc: toolbox.doc,
+ getAccessibilityTreeRoot,
+ startListeningForAccessibilityEvents,
+ stopListeningForAccessibilityEvents,
+ highlightAccessible,
+ unhighlightAccessible,
+ })
+ ),
+ endPanel: RightSidebar({
+ highlightAccessible,
+ unhighlightAccessible,
+ toolbox,
+ }),
+ vert: this.useLandscapeMode,
+ })
+ )
+ )
+ );
+ }
+}
+
+const mapStateToProps = ({
+ ui: { enabled, supports },
+ audit: { auditing },
+}) => ({
+ enabled,
+ supports,
+ auditing,
+});
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(MainFrame);
diff --git a/devtools/client/accessibility/components/RightSidebar.js b/devtools/client/accessibility/components/RightSidebar.js
new file mode 100644
index 0000000000..f525b96294
--- /dev/null
+++ b/devtools/client/accessibility/components/RightSidebar.js
@@ -0,0 +1,67 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+const Accessible = createFactory(
+ require("resource://devtools/client/accessibility/components/Accessible.js")
+);
+const Accordion = createFactory(
+ require("resource://devtools/client/shared/components/Accordion.js")
+);
+const Checks = createFactory(
+ require("resource://devtools/client/accessibility/components/Checks.js")
+);
+
+// Component that is responsible for rendering accessible panel's sidebar.
+function RightSidebar({ highlightAccessible, unhighlightAccessible, toolbox }) {
+ const propertiesID = "accessibility-properties";
+ const checksID = "accessibility-checks";
+
+ return div(
+ {
+ className: "right-sidebar",
+ role: "presentation",
+ tabIndex: "-1",
+ },
+ Accordion({
+ items: [
+ {
+ className: "checks",
+ component: Checks,
+ componentProps: { labelledby: `${checksID}-header` },
+ header: L10N.getStr("accessibility.checks"),
+ id: checksID,
+ opened: true,
+ },
+ {
+ className: "accessible",
+ component: Accessible,
+ componentProps: {
+ highlightAccessible,
+ unhighlightAccessible,
+ toolboxHighlighter: toolbox.getHighlighter(),
+ toolbox,
+ labelledby: `${propertiesID}-header`,
+ },
+ header: L10N.getStr("accessibility.properties"),
+ id: propertiesID,
+ opened: true,
+ },
+ ],
+ })
+ );
+}
+
+module.exports = RightSidebar;
diff --git a/devtools/client/accessibility/components/SimulationMenuButton.js b/devtools/client/accessibility/components/SimulationMenuButton.js
new file mode 100644
index 0000000000..6fd21f4cc8
--- /dev/null
+++ b/devtools/client/accessibility/components/SimulationMenuButton.js
@@ -0,0 +1,167 @@
+/* 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";
+
+/* global gTelemetry */
+
+// React
+const {
+ createFactory,
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ hr,
+ span,
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const MenuButton = createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuButton.js")
+);
+const { openDocLink } = require("resource://devtools/client/shared/link.js");
+const {
+ A11Y_SIMULATION_DOCUMENTATION_LINK,
+} = require("resource://devtools/client/accessibility/constants.js");
+const {
+ accessibility: { SIMULATION_TYPE },
+} = require("resource://devtools/shared/constants.js");
+const actions = require("resource://devtools/client/accessibility/actions/simulation.js");
+
+loader.lazyGetter(this, "MenuItem", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuItem.js")
+ );
+});
+loader.lazyGetter(this, "MenuList", function () {
+ return createFactory(
+ require("resource://devtools/client/shared/components/menu/MenuList.js")
+ );
+});
+
+const TELEMETRY_SIMULATION_ACTIVATED =
+ "devtools.accessibility.simulation_activated";
+const SIMULATION_MENU_LABELS = {
+ NONE: "accessibility.filter.none",
+ [SIMULATION_TYPE.ACHROMATOPSIA]: "accessibility.simulation.achromatopsia",
+ [SIMULATION_TYPE.PROTANOPIA]: "accessibility.simulation.protanopia",
+ [SIMULATION_TYPE.DEUTERANOPIA]: "accessibility.simulation.deuteranopia",
+ [SIMULATION_TYPE.TRITANOPIA]: "accessibility.simulation.tritanopia",
+ [SIMULATION_TYPE.CONTRAST_LOSS]: "accessibility.simulation.contrastLoss",
+ DOCUMENTATION: "accessibility.documentation.label",
+};
+
+class SimulationMenuButton extends Component {
+ static get propTypes() {
+ return {
+ simulate: PropTypes.func,
+ simulation: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ toolboxDoc: PropTypes.object.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.disableSimulation = this.disableSimulation.bind(this);
+ }
+
+ disableSimulation() {
+ const { dispatch, simulate: simulateFunc } = this.props;
+
+ dispatch(actions.simulate(simulateFunc));
+ }
+
+ toggleSimulation(simKey) {
+ const { dispatch, simulation, simulate: simulateFunc } = this.props;
+
+ if (!simulation[simKey]) {
+ if (gTelemetry) {
+ gTelemetry.keyedScalarAdd(TELEMETRY_SIMULATION_ACTIVATED, simKey, 1);
+ }
+
+ dispatch(actions.simulate(simulateFunc, [simKey]));
+ return;
+ }
+
+ this.disableSimulation();
+ }
+
+ render() {
+ const { simulation, toolboxDoc } = this.props;
+ const simulationMenuButtonId = "simulation-menu-button";
+ const toolbarLabelID = "accessibility-simulation-label";
+ const currSimulation = Object.entries(simulation).find(
+ simEntry => simEntry[1]
+ );
+
+ const items = [
+ // No simulation option
+ MenuItem({
+ key: "simulation-none",
+ label: L10N.getStr(SIMULATION_MENU_LABELS.NONE),
+ checked: !currSimulation,
+ onClick: this.disableSimulation,
+ }),
+ hr({ key: "hr-1" }),
+ // Simulation options
+ ...Object.keys(SIMULATION_TYPE).map(simType =>
+ MenuItem({
+ key: `simulation-${simType}`,
+ label: L10N.getStr(SIMULATION_MENU_LABELS[simType]),
+ checked: simulation[simType],
+ onClick: this.toggleSimulation.bind(this, simType),
+ })
+ ),
+ hr({ key: "hr-2" }),
+ // Documentation link
+ MenuItem({
+ className: "link",
+ key: "simulation-documentation",
+ label: L10N.getStr(SIMULATION_MENU_LABELS.DOCUMENTATION),
+ role: "link",
+ onClick: () => openDocLink(A11Y_SIMULATION_DOCUMENTATION_LINK),
+ }),
+ ];
+
+ return div(
+ {
+ role: "group",
+ className: "accessibility-simulation",
+ "aria-labelledby": toolbarLabelID,
+ },
+ span(
+ { id: toolbarLabelID, role: "presentation" },
+ L10N.getStr("accessibility.simulation")
+ ),
+ MenuButton(
+ {
+ id: simulationMenuButtonId,
+ menuId: simulationMenuButtonId + "-menu",
+ className: `devtools-button toolbar-menu-button simulation${
+ currSimulation ? " active" : ""
+ }`,
+ toolboxDoc,
+ label: L10N.getStr(
+ SIMULATION_MENU_LABELS[currSimulation ? currSimulation[0] : "NONE"]
+ ),
+ },
+ MenuList({}, items)
+ )
+ );
+ }
+}
+
+const mapStateToProps = ({ simulation }) => {
+ return { simulation };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(SimulationMenuButton);
diff --git a/devtools/client/accessibility/components/TextLabelBadge.js b/devtools/client/accessibility/components/TextLabelBadge.js
new file mode 100644
index 0000000000..0ff54ef8e0
--- /dev/null
+++ b/devtools/client/accessibility/components/TextLabelBadge.js
@@ -0,0 +1,57 @@
+/* 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,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ L10N,
+} = require("resource://devtools/client/accessibility/utils/l10n.js");
+
+const {
+ accessibility: {
+ SCORES: { BEST_PRACTICES, FAIL, WARNING },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+loader.lazyGetter(this, "Badge", () =>
+ createFactory(
+ require("resource://devtools/client/accessibility/components/Badge.js")
+ )
+);
+
+/**
+ * Component for rendering a badge for text alternative accessibliity check
+ * failures association with a given accessibility object in the accessibility
+ * tree.
+ */
+class TextLabelBadge extends PureComponent {
+ static get propTypes() {
+ return {
+ error: PropTypes.string,
+ score: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { error, score } = this.props;
+ if (error || ![BEST_PRACTICES, FAIL, WARNING].includes(score)) {
+ return null;
+ }
+
+ return Badge({
+ score,
+ label: L10N.getStr("accessibility.badge.textLabel"),
+ tooltip: L10N.getStr("accessibility.badge.textLabel.tooltip"),
+ });
+ }
+}
+
+module.exports = TextLabelBadge;
diff --git a/devtools/client/accessibility/components/TextLabelCheck.js b/devtools/client/accessibility/components/TextLabelCheck.js
new file mode 100644
index 0000000000..adfbb4b412
--- /dev/null
+++ b/devtools/client/accessibility/components/TextLabelCheck.js
@@ -0,0 +1,225 @@
+/* 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,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Check = createFactory(
+ require("resource://devtools/client/accessibility/components/Check.js")
+);
+
+const {
+ A11Y_TEXT_LABEL_LINKS,
+} = require("resource://devtools/client/accessibility/constants.js");
+const {
+ accessibility: {
+ AUDIT_TYPE: { TEXT_LABEL },
+ ISSUE_TYPE: {
+ [TEXT_LABEL]: {
+ AREA_NO_NAME_FROM_ALT,
+ DIALOG_NO_NAME,
+ DOCUMENT_NO_TITLE,
+ EMBED_NO_NAME,
+ FIGURE_NO_NAME,
+ FORM_FIELDSET_NO_NAME,
+ FORM_FIELDSET_NO_NAME_FROM_LEGEND,
+ FORM_NO_NAME,
+ FORM_NO_VISIBLE_NAME,
+ FORM_OPTGROUP_NO_NAME_FROM_LABEL,
+ FRAME_NO_NAME,
+ HEADING_NO_CONTENT,
+ HEADING_NO_NAME,
+ IFRAME_NO_NAME_FROM_TITLE,
+ IMAGE_NO_NAME,
+ INTERACTIVE_NO_NAME,
+ MATHML_GLYPH_NO_NAME,
+ TOOLBAR_NO_NAME,
+ },
+ },
+ },
+} = require("resource://devtools/shared/constants.js");
+
+/**
+ * A map from text label issues to annotation component properties.
+ */
+const ISSUE_TO_ANNOTATION_MAP = {
+ [AREA_NO_NAME_FROM_ALT]: {
+ href: A11Y_TEXT_LABEL_LINKS.AREA_NO_NAME_FROM_ALT,
+ l10nId: "accessibility-text-label-issue-area",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "alt");
+ },
+ // Note: there is no way right now to use custom elements in privileged
+ // content. We have to use something like <div> since we can't provide
+ // three args with the same name.
+ get div() {
+ return ReactDOM.code({}, "area");
+ },
+ // Note: there is no way right now to use custom elements in privileged
+ // content. We have to use something like <span> since we can't provide
+ // three args with the same name.
+ get span() {
+ return ReactDOM.code({}, "href");
+ },
+ },
+ },
+ [DIALOG_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.DIALOG_NO_NAME,
+ l10nId: "accessibility-text-label-issue-dialog",
+ },
+ [DOCUMENT_NO_TITLE]: {
+ href: A11Y_TEXT_LABEL_LINKS.DOCUMENT_NO_TITLE,
+ l10nId: "accessibility-text-label-issue-document-title",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "title");
+ },
+ },
+ },
+ [EMBED_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.EMBED_NO_NAME,
+ l10nId: "accessibility-text-label-issue-embed",
+ },
+ [FIGURE_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.FIGURE_NO_NAME,
+ l10nId: "accessibility-text-label-issue-figure",
+ },
+ [FORM_FIELDSET_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.FORM_FIELDSET_NO_NAME,
+ l10nId: "accessibility-text-label-issue-fieldset",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "fieldset");
+ },
+ },
+ },
+ [FORM_FIELDSET_NO_NAME_FROM_LEGEND]: {
+ href: A11Y_TEXT_LABEL_LINKS.FORM_FIELDSET_NO_NAME_FROM_LEGEND,
+ l10nId: "accessibility-text-label-issue-fieldset-legend2",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "legend");
+ },
+ // Note: there is no way right now to use custom elements in privileged
+ // content. We have to use something like <span> since we can't provide
+ // two args with the same name.
+ get span() {
+ return ReactDOM.code({}, "fieldset");
+ },
+ },
+ },
+ [FORM_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.FORM_NO_NAME,
+ l10nId: "accessibility-text-label-issue-form",
+ },
+ [FORM_NO_VISIBLE_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.FORM_NO_VISIBLE_NAME,
+ l10nId: "accessibility-text-label-issue-form-visible",
+ },
+ [FORM_OPTGROUP_NO_NAME_FROM_LABEL]: {
+ href: A11Y_TEXT_LABEL_LINKS.FORM_OPTGROUP_NO_NAME_FROM_LABEL,
+ l10nId: "accessibility-text-label-issue-optgroup-label2",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "label");
+ },
+ // Note: there is no way right now to use custom elements in privileged
+ // content. We have to use something like <span> since we can't provide
+ // two args with the same name.
+ get span() {
+ return ReactDOM.code({}, "optgroup");
+ },
+ },
+ },
+ [FRAME_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.FRAME_NO_NAME,
+ l10nId: "accessibility-text-label-issue-frame",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "frame");
+ },
+ },
+ },
+ [HEADING_NO_CONTENT]: {
+ href: A11Y_TEXT_LABEL_LINKS.HEADING_NO_CONTENT,
+ l10nId: "accessibility-text-label-issue-heading-content",
+ },
+ [HEADING_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.HEADING_NO_NAME,
+ l10nId: "accessibility-text-label-issue-heading",
+ },
+ [IFRAME_NO_NAME_FROM_TITLE]: {
+ href: A11Y_TEXT_LABEL_LINKS.IFRAME_NO_NAME_FROM_TITLE,
+ l10nId: "accessibility-text-label-issue-iframe",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "title");
+ },
+ // Note: there is no way right now to use custom elements in privileged
+ // content. We have to use something like <span> since we can't provide
+ // two args with the same name.
+ get span() {
+ return ReactDOM.code({}, "iframe");
+ },
+ },
+ },
+ [IMAGE_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.IMAGE_NO_NAME,
+ l10nId: "accessibility-text-label-issue-image",
+ },
+ [INTERACTIVE_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.INTERACTIVE_NO_NAME,
+ l10nId: "accessibility-text-label-issue-interactive",
+ },
+ [MATHML_GLYPH_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.MATHML_GLYPH_NO_NAME,
+ l10nId: "accessibility-text-label-issue-glyph",
+ args: {
+ get code() {
+ return ReactDOM.code({}, "alt");
+ },
+ // Note: there is no way right now to use custom elements in privileged
+ // content. We have to use something like <span> since we can't provide
+ // two args with the same name.
+ get span() {
+ return ReactDOM.code({}, "mglyph");
+ },
+ },
+ },
+ [TOOLBAR_NO_NAME]: {
+ href: A11Y_TEXT_LABEL_LINKS.TOOLBAR_NO_NAME,
+ l10nId: "accessibility-text-label-issue-toolbar",
+ },
+};
+
+/**
+ * Component for rendering a check for text label accessibliity check failures,
+ * warnings and best practices suggestions association with a given
+ * accessibility object in the accessibility tree.
+ */
+class TextLabelCheck extends PureComponent {
+ static get propTypes() {
+ return {
+ issue: PropTypes.string.isRequired,
+ score: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ return Check({
+ ...this.props,
+ getAnnotation: issue => ISSUE_TO_ANNOTATION_MAP[issue],
+ id: "accessibility-text-label-header",
+ });
+ }
+}
+
+module.exports = TextLabelCheck;
diff --git a/devtools/client/accessibility/components/Toolbar.js b/devtools/client/accessibility/components/Toolbar.js
new file mode 100644
index 0000000000..b5f27485ed
--- /dev/null
+++ b/devtools/client/accessibility/components/Toolbar.js
@@ -0,0 +1,74 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const AccessibilityTreeFilter = createFactory(
+ require("resource://devtools/client/accessibility/components/AccessibilityTreeFilter.js")
+);
+const AccessibilityPrefs = createFactory(
+ require("resource://devtools/client/accessibility/components/AccessibilityPrefs.js")
+);
+loader.lazyGetter(this, "SimulationMenuButton", function () {
+ return createFactory(
+ require("resource://devtools/client/accessibility/components/SimulationMenuButton.js")
+ );
+});
+const DisplayTabbingOrder = createFactory(
+ require("resource://devtools/client/accessibility/components/DisplayTabbingOrder.js")
+);
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+function Toolbar({ audit, simulate, supportsTabbingOrder, toolboxDoc }) {
+ const optionalSimulationSection = simulate
+ ? [
+ div({
+ role: "separator",
+ className: "devtools-separator",
+ }),
+ SimulationMenuButton({ simulate, toolboxDoc }),
+ ]
+ : [];
+ const optionalDisplayTabbingOrderSection = supportsTabbingOrder
+ ? [
+ div({
+ role: "separator",
+ className: "devtools-separator",
+ }),
+ DisplayTabbingOrder(),
+ ]
+ : [];
+
+ return div(
+ {
+ className: "devtools-toolbar",
+ role: "toolbar",
+ },
+ AccessibilityTreeFilter({ audit, toolboxDoc }),
+ // Simulation section is shown if webrender is enabled
+ ...optionalSimulationSection,
+ ...optionalDisplayTabbingOrderSection,
+ AccessibilityPrefs({ toolboxDoc })
+ );
+}
+
+const mapStateToProps = ({
+ ui: {
+ supports: { tabbingOrder },
+ },
+}) => ({
+ supportsTabbingOrder: tabbingOrder,
+});
+
+// Exports from this module
+exports.Toolbar = connect(mapStateToProps)(Toolbar);
diff --git a/devtools/client/accessibility/components/moz.build b/devtools/client/accessibility/components/moz.build
new file mode 100644
index 0000000000..23e01d42c6
--- /dev/null
+++ b/devtools/client/accessibility/components/moz.build
@@ -0,0 +1,33 @@
+# 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(
+ "AccessibilityPrefs.js",
+ "AccessibilityRow.js",
+ "AccessibilityRowValue.js",
+ "AccessibilityTree.js",
+ "AccessibilityTreeFilter.js",
+ "Accessible.js",
+ "AuditController.js",
+ "AuditFilter.js",
+ "AuditProgressOverlay.js",
+ "Badge.js",
+ "Badges.js",
+ "Button.js",
+ "Check.js",
+ "Checks.js",
+ "ColorContrastAccessibility.js",
+ "ContrastBadge.js",
+ "Description.js",
+ "DisplayTabbingOrder.js",
+ "KeyboardBadge.js",
+ "KeyboardCheck.js",
+ "LearnMoreLink.js",
+ "MainFrame.js",
+ "RightSidebar.js",
+ "SimulationMenuButton.js",
+ "TextLabelBadge.js",
+ "TextLabelCheck.js",
+ "Toolbar.js",
+)