summaryrefslogtreecommitdiffstats
path: root/devtools/client/accessibility/components/AccessibilityRow.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/accessibility/components/AccessibilityRow.js')
-rw-r--r--devtools/client/accessibility/components/AccessibilityRow.js328
1 files changed, 328 insertions, 0 deletions
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
+);