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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* global gTelemetry, EVENTS */
// React & Redux
const {
Component,
createFactory,
const {
findDOMNode,
const {
connect,
const AuditFilter = createFactory(
);
const AuditController = createFactory(
);
// Utils
const {
flashElementOn,
flashElementOff,
const { openDocLink } = require("resource://devtools/client/shared/link.js");
const {
PREFS,
VALUE_FLASHING_DURATION,
VALUE_HIGHLIGHT_DURATION,
const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
// Actions
const {
updateDetails,
const {
unhighlight,
const {
L10N,
loader.lazyRequireGetter(
this,
"Menu",
);
loader.lazyRequireGetter(
this,
"MenuItem",
);
const {
scrollIntoView,
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
);