Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"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.
*/
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);