chrome-devtools-frontend
Version:
Chrome DevTools UI
587 lines (504 loc) • 17.9 kB
text/typescript
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
import {AccessibilitySidebarView} from './AccessibilitySidebarView.js'; // eslint-disable-line no-unused-vars
import {AccessibilitySubPane} from './AccessibilitySubPane.js';
export const UIStrings = {
/**
*@description Text in AXBreadcrumbs Pane of the Accessibility panel
*/
accessibilityTree: 'Accessibility Tree',
/**
*@description Text to scroll the displayed content into view
*/
scrollIntoView: 'Scroll into view',
/**
*@description Ignored node element text content in AXBreadcrumbs Pane of the Accessibility panel
*/
ignored: 'Ignored',
};
const str_ = i18n.i18n.registerUIStrings('accessibility/AXBreadcrumbsPane.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class AXBreadcrumbsPane extends AccessibilitySubPane {
_axSidebarView: AccessibilitySidebarView;
_preselectedBreadcrumb: AXBreadcrumb|null;
_inspectedNodeBreadcrumb: AXBreadcrumb|null;
_collapsingBreadcrumbId: number;
_hoveredBreadcrumb: AXBreadcrumb|null;
_rootElement: HTMLElement;
constructor(axSidebarView: AccessibilitySidebarView) {
super(i18nString(UIStrings.accessibilityTree));
this.element.classList.add('ax-subpane');
UI.ARIAUtils.markAsTree(this.element);
this.element.tabIndex = -1;
this._axSidebarView = axSidebarView;
this._preselectedBreadcrumb = null;
this._inspectedNodeBreadcrumb = null;
this._collapsingBreadcrumbId = -1;
this._hoveredBreadcrumb = null;
this._rootElement = this.element.createChild('div', 'ax-breadcrumbs');
this._rootElement.addEventListener('keydown', this._onKeyDown.bind(this), true);
this._rootElement.addEventListener('mousemove', this._onMouseMove.bind(this), false);
this._rootElement.addEventListener('mouseleave', this._onMouseLeave.bind(this), false);
this._rootElement.addEventListener('click', this._onClick.bind(this), false);
this._rootElement.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false);
this._rootElement.addEventListener('focusout', this._onFocusOut.bind(this), false);
this.registerRequiredCSS('accessibility/axBreadcrumbs.css', {enableLegacyPatching: false});
}
focus(): void {
if (this._inspectedNodeBreadcrumb) {
this._inspectedNodeBreadcrumb.nodeElement().focus();
} else {
this.element.focus();
}
}
setAXNode(axNode: SDK.AccessibilityModel.AccessibilityNode|null): void {
const hadFocus = this.element.hasFocus();
super.setAXNode(axNode);
this._rootElement.removeChildren();
if (!axNode) {
return;
}
const ancestorChain = [];
let ancestor: (SDK.AccessibilityModel.AccessibilityNode|null)|SDK.AccessibilityModel.AccessibilityNode = axNode;
while (ancestor) {
ancestorChain.push(ancestor);
ancestor = ancestor.parentNode();
}
ancestorChain.reverse();
let depth = 0;
let parent: AXBreadcrumb|null = null;
this._inspectedNodeBreadcrumb = null;
for (ancestor of ancestorChain) {
const breadcrumb = new AXBreadcrumb(ancestor, depth, (ancestor === axNode));
if (parent) {
parent.appendChild(breadcrumb);
} else {
this._rootElement.appendChild(breadcrumb.element());
}
parent = breadcrumb;
depth++;
this._inspectedNodeBreadcrumb = breadcrumb;
}
if (this._inspectedNodeBreadcrumb) {
this._inspectedNodeBreadcrumb.setPreselected(true, hadFocus);
}
this._setPreselectedBreadcrumb(this._inspectedNodeBreadcrumb);
function append(
parentBreadcrumb: AXBreadcrumb, axNode: SDK.AccessibilityModel.AccessibilityNode, localDepth: number): void {
const childBreadcrumb = new AXBreadcrumb(axNode, localDepth, false);
parentBreadcrumb.appendChild(childBreadcrumb);
// In most cases there will be no children here, but there are some special cases.
for (const child of axNode.children()) {
append(childBreadcrumb, child, localDepth + 1);
}
}
if (this._inspectedNodeBreadcrumb) {
for (const child of axNode.children()) {
append(this._inspectedNodeBreadcrumb, child, depth);
if (child.backendDOMNodeId() === this._collapsingBreadcrumbId) {
this._setPreselectedBreadcrumb(this._inspectedNodeBreadcrumb.lastChild());
}
}
}
this._collapsingBreadcrumbId = -1;
}
willHide(): void {
this._setPreselectedBreadcrumb(null);
}
_onKeyDown(event: Event): void {
const preselectedBreadcrumb = this._preselectedBreadcrumb;
if (!preselectedBreadcrumb) {
return;
}
const keyboardEvent = event as KeyboardEvent;
if (!keyboardEvent.composedPath().some(element => element === preselectedBreadcrumb.element())) {
return;
}
if (keyboardEvent.shiftKey || keyboardEvent.metaKey || keyboardEvent.ctrlKey) {
return;
}
let handled = false;
if (keyboardEvent.key === 'ArrowUp' && !keyboardEvent.altKey) {
handled = this._preselectPrevious();
} else if ((keyboardEvent.key === 'ArrowDown') && !keyboardEvent.altKey) {
handled = this._preselectNext();
} else if (keyboardEvent.key === 'ArrowLeft' && !keyboardEvent.altKey) {
if (preselectedBreadcrumb.hasExpandedChildren()) {
this._collapseBreadcrumb(preselectedBreadcrumb);
} else {
handled = this._preselectParent();
}
} else if ((keyboardEvent.key === 'Enter' ||
(keyboardEvent.key === 'ArrowRight' && !keyboardEvent.altKey &&
preselectedBreadcrumb.axNode().hasOnlyUnloadedChildren()))) {
handled = this._inspectDOMNode(preselectedBreadcrumb.axNode());
}
if (handled) {
keyboardEvent.consume(true);
}
}
_preselectPrevious(): boolean {
if (!this._preselectedBreadcrumb) {
return false;
}
const previousBreadcrumb = this._preselectedBreadcrumb.previousBreadcrumb();
if (!previousBreadcrumb) {
return false;
}
this._setPreselectedBreadcrumb(previousBreadcrumb);
return true;
}
_preselectNext(): boolean {
if (!this._preselectedBreadcrumb) {
return false;
}
const nextBreadcrumb = this._preselectedBreadcrumb.nextBreadcrumb();
if (!nextBreadcrumb) {
return false;
}
this._setPreselectedBreadcrumb(nextBreadcrumb);
return true;
}
_preselectParent(): boolean {
if (!this._preselectedBreadcrumb) {
return false;
}
const parentBreadcrumb = this._preselectedBreadcrumb.parentBreadcrumb();
if (!parentBreadcrumb) {
return false;
}
this._setPreselectedBreadcrumb(parentBreadcrumb);
return true;
}
_setPreselectedBreadcrumb(breadcrumb: AXBreadcrumb|null): void {
if (breadcrumb === this._preselectedBreadcrumb) {
return;
}
const hadFocus = this.element.hasFocus();
if (this._preselectedBreadcrumb) {
this._preselectedBreadcrumb.setPreselected(false, hadFocus);
}
if (breadcrumb) {
this._preselectedBreadcrumb = breadcrumb;
} else {
this._preselectedBreadcrumb = this._inspectedNodeBreadcrumb;
}
if (this._preselectedBreadcrumb) {
this._preselectedBreadcrumb.setPreselected(true, hadFocus);
}
if (!breadcrumb && hadFocus) {
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
}
}
_collapseBreadcrumb(breadcrumb: AXBreadcrumb): void {
if (!breadcrumb.parentBreadcrumb()) {
return;
}
const backendNodeId = breadcrumb.axNode().backendDOMNodeId();
if (backendNodeId !== null) {
this._collapsingBreadcrumbId = backendNodeId;
}
const parentBreadcrumb = breadcrumb.parentBreadcrumb();
if (parentBreadcrumb) {
this._inspectDOMNode(parentBreadcrumb.axNode());
}
}
_onMouseLeave(_event: Event): void {
this._setHoveredBreadcrumb(null);
}
_onMouseMove(event: Event): void {
const target = event.target as Element | null;
if (!target) {
return;
}
const breadcrumbElement = target.enclosingNodeOrSelfWithClass('ax-breadcrumb');
if (!breadcrumbElement) {
this._setHoveredBreadcrumb(null);
return;
}
const breadcrumb = elementsToAXBreadcrumb.get(breadcrumbElement);
if (!breadcrumb || !breadcrumb.isDOMNode()) {
return;
}
this._setHoveredBreadcrumb(breadcrumb);
}
_onFocusOut(event: Event): void {
if (!this._preselectedBreadcrumb || event.target !== this._preselectedBreadcrumb.nodeElement()) {
return;
}
this._setPreselectedBreadcrumb(null);
}
_onClick(event: Event): void {
const target = event.target as Element | null;
if (!target) {
return;
}
const breadcrumbElement = target.enclosingNodeOrSelfWithClass('ax-breadcrumb');
if (!breadcrumbElement) {
this._setHoveredBreadcrumb(null);
return;
}
const breadcrumb = elementsToAXBreadcrumb.get(breadcrumbElement);
if (!breadcrumb) {
return;
}
if (breadcrumb.inspected()) {
// This will collapse and preselect/focus the breadcrumb.
this._collapseBreadcrumb(breadcrumb);
breadcrumb.nodeElement().focus();
return;
}
if (!breadcrumb.isDOMNode()) {
return;
}
this._inspectDOMNode(breadcrumb.axNode());
}
_setHoveredBreadcrumb(breadcrumb: AXBreadcrumb|null): void {
if (breadcrumb === this._hoveredBreadcrumb) {
return;
}
if (this._hoveredBreadcrumb) {
this._hoveredBreadcrumb.setHovered(false);
}
const node = this.node();
if (breadcrumb) {
breadcrumb.setHovered(true);
} else if (node && node.id) {
// Highlight and scroll into view the currently inspected node.
node.domModel().overlayModel().nodeHighlightRequested({nodeId: node.id});
}
this._hoveredBreadcrumb = breadcrumb;
}
_inspectDOMNode(axNode: SDK.AccessibilityModel.AccessibilityNode): boolean {
if (!axNode.isDOMNode()) {
return false;
}
const deferredNode = axNode.deferredDOMNode();
if (deferredNode) {
deferredNode.resolve(domNode => {
this._axSidebarView.setNode(domNode, true /* fromAXTree */);
Common.Revealer.reveal(domNode, true /* omitFocus */);
});
}
return true;
}
_contextMenuEventFired(event: Event): void {
const target = event.target as Element | null;
if (!target) {
return;
}
const breadcrumbElement = target.enclosingNodeOrSelfWithClass('ax-breadcrumb');
if (!breadcrumbElement) {
return;
}
const breadcrumb = elementsToAXBreadcrumb.get(breadcrumbElement);
if (!breadcrumb) {
return;
}
const axNode = breadcrumb.axNode();
if (!axNode.isDOMNode() || !axNode.deferredDOMNode()) {
return;
}
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.viewSection().appendItem(i18nString(UIStrings.scrollIntoView), () => {
const deferredNode = axNode.deferredDOMNode();
if (!deferredNode) {
return;
}
deferredNode.resolvePromise().then(domNode => {
if (!domNode) {
return;
}
domNode.scrollIntoView();
});
});
const deferredNode = axNode.deferredDOMNode();
if (deferredNode) {
contextMenu.appendApplicableItems(deferredNode);
}
contextMenu.show();
}
}
const elementsToAXBreadcrumb = new WeakMap<Element, AXBreadcrumb>();
export class AXBreadcrumb {
_axNode: SDK.AccessibilityModel.AccessibilityNode;
_element: HTMLDivElement;
_nodeElement: HTMLDivElement;
_nodeWrapper: HTMLDivElement;
_selectionElement: HTMLDivElement;
_childrenGroupElement: HTMLDivElement;
_children: AXBreadcrumb[];
_hovered: boolean;
_preselected: boolean;
_parent: AXBreadcrumb|null;
_inspected: boolean;
constructor(axNode: SDK.AccessibilityModel.AccessibilityNode, depth: number, inspected: boolean) {
this._axNode = axNode;
this._element = document.createElement('div');
this._element.classList.add('ax-breadcrumb');
elementsToAXBreadcrumb.set(this._element, this);
this._nodeElement = document.createElement('div');
this._nodeElement.classList.add('ax-node');
UI.ARIAUtils.markAsTreeitem(this._nodeElement);
this._nodeElement.tabIndex = -1;
this._element.appendChild(this._nodeElement);
this._nodeWrapper = document.createElement('div');
this._nodeWrapper.classList.add('wrapper');
this._nodeElement.appendChild(this._nodeWrapper);
this._selectionElement = document.createElement('div');
this._selectionElement.classList.add('selection');
this._selectionElement.classList.add('fill');
this._nodeElement.appendChild(this._selectionElement);
this._childrenGroupElement = document.createElement('div');
this._childrenGroupElement.classList.add('children');
UI.ARIAUtils.markAsGroup(this._childrenGroupElement);
this._element.appendChild(this._childrenGroupElement);
this._children = [];
this._hovered = false;
this._preselected = false;
this._parent = null;
this._inspected = inspected;
this._nodeElement.classList.toggle('inspected', inspected);
this._nodeElement.style.paddingLeft = (16 * depth + 4) + 'px';
if (this._axNode.ignored()) {
this._appendIgnoredNodeElement();
} else {
this._appendRoleElement(this._axNode.role());
const axNodeName = this._axNode.name();
if (axNodeName && axNodeName.value) {
this._nodeWrapper.createChild('span', 'separator').textContent = '\xA0';
this._appendNameElement(axNodeName.value as string);
}
}
if (this._axNode.hasOnlyUnloadedChildren()) {
this._nodeElement.classList.add('children-unloaded');
UI.ARIAUtils.setExpanded(this._nodeElement, false);
}
if (!this._axNode.isDOMNode()) {
this._nodeElement.classList.add('no-dom-node');
}
}
element(): HTMLElement {
return /** @type {!HTMLElement} */ this._element as HTMLElement;
}
nodeElement(): HTMLElement {
return /** @type {!HTMLElement} */ this._nodeElement as HTMLElement;
}
appendChild(breadcrumb: AXBreadcrumb): void {
this._children.push(breadcrumb);
breadcrumb.setParent(this);
this._nodeElement.classList.add('parent');
UI.ARIAUtils.setExpanded(this._nodeElement, true);
this._childrenGroupElement.appendChild(breadcrumb.element());
}
hasExpandedChildren(): number {
return this._children.length;
}
setParent(breadcrumb: AXBreadcrumb): void {
this._parent = breadcrumb;
}
preselected(): boolean {
return this._preselected;
}
setPreselected(preselected: boolean, selectedByUser: boolean): void {
if (this._preselected === preselected) {
return;
}
this._preselected = preselected;
this._nodeElement.classList.toggle('preselected', preselected);
if (preselected) {
this._nodeElement.tabIndex = 0;
} else {
this._nodeElement.tabIndex = -1;
}
if (this._preselected) {
if (selectedByUser) {
this._nodeElement.focus();
}
if (!this._inspected) {
this._axNode.highlightDOMNode();
} else {
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
}
}
}
setHovered(hovered: boolean): void {
if (this._hovered === hovered) {
return;
}
this._hovered = hovered;
this._nodeElement.classList.toggle('hovered', hovered);
if (this._hovered) {
this._nodeElement.classList.toggle('hovered', true);
this._axNode.highlightDOMNode();
}
}
axNode(): SDK.AccessibilityModel.AccessibilityNode {
return this._axNode;
}
inspected(): boolean {
return this._inspected;
}
isDOMNode(): boolean {
return this._axNode.isDOMNode();
}
nextBreadcrumb(): AXBreadcrumb|null {
if (this._children.length) {
return this._children[0];
}
const nextSibling = this.element().nextSibling;
if (nextSibling) {
return elementsToAXBreadcrumb.get(nextSibling as HTMLElement) || null;
}
return null;
}
previousBreadcrumb(): AXBreadcrumb|null {
const previousSibling = this.element().previousSibling;
if (previousSibling) {
return elementsToAXBreadcrumb.get(previousSibling as HTMLElement) || null;
}
return this._parent;
}
parentBreadcrumb(): AXBreadcrumb|null {
return this._parent;
}
lastChild(): AXBreadcrumb {
return this._children[this._children.length - 1];
}
_appendNameElement(name: string): void {
const nameElement = document.createElement('span');
nameElement.textContent = '"' + name + '"';
nameElement.classList.add('ax-readable-string');
this._nodeWrapper.appendChild(nameElement);
}
_appendRoleElement(role: Protocol.Accessibility.AXValue|null): void {
if (!role) {
return;
}
const roleElement = document.createElement('span');
roleElement.classList.add('monospace');
roleElement.classList.add(RoleStyles[role.type]);
roleElement.setTextContentTruncatedIfNeeded(role.value || '');
this._nodeWrapper.appendChild(roleElement);
}
_appendIgnoredNodeElement(): void {
const ignoredNodeElement = document.createElement('span');
ignoredNodeElement.classList.add('monospace');
ignoredNodeElement.textContent = i18nString(UIStrings.ignored);
ignoredNodeElement.classList.add('ax-breadcrumbs-ignored-node');
this._nodeWrapper.appendChild(ignoredNodeElement);
}
}
type RoleStyles = {
[type: string]: string,
};
export const RoleStyles: RoleStyles = {
internalRole: 'ax-internal-role',
role: 'ax-role',
};