зеркало из https://github.com/mozilla/gecko-dev.git
548 строки
15 KiB
JavaScript
548 строки
15 KiB
JavaScript
/* 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("devtools/client/shared/vendor/react");
|
|
const {
|
|
div,
|
|
span,
|
|
} = require("devtools/client/shared/vendor/react-dom-factories");
|
|
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
|
|
const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
|
|
const { connect } = require("devtools/client/shared/vendor/react-redux");
|
|
|
|
const {
|
|
TREE_ROW_HEIGHT,
|
|
ORDERED_PROPS,
|
|
ACCESSIBLE_EVENTS,
|
|
VALUE_FLASHING_DURATION,
|
|
} = require("devtools/client/accessibility/constants");
|
|
const { L10N } = require("devtools/client/accessibility/utils/l10n");
|
|
const {
|
|
flashElementOn,
|
|
flashElementOff,
|
|
} = require("devtools/client/inspector/markup/utils");
|
|
const {
|
|
updateDetails,
|
|
} = require("devtools/client/accessibility/actions/details");
|
|
const {
|
|
select,
|
|
unhighlight,
|
|
} = require("devtools/client/accessibility/actions/accessibles");
|
|
|
|
const Tree = createFactory(
|
|
require("devtools/client/shared/components/VirtualizedTree")
|
|
);
|
|
// Reps
|
|
const { REPS, MODE } = require("devtools/client/shared/components/reps/index");
|
|
const { Rep, ElementNode, Accessible: AccessibleRep, Obj } = REPS;
|
|
|
|
const {
|
|
translateNodeFrontToGrip,
|
|
} = require("devtools/client/inspector/shared/utils");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"openContentLink",
|
|
"devtools/client/shared/link",
|
|
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.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.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 Tree({
|
|
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 Tree 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 > 0) {
|
|
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);
|