diff options
Diffstat (limited to 'devtools/client/accessibility/components/AccessibilityTree.js')
-rw-r--r-- | devtools/client/accessibility/components/AccessibilityTree.js | 298 |
1 files changed, 298 insertions, 0 deletions
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); |