UNPKG

chrome-devtools-frontend

Version:
587 lines (504 loc) • 17.9 kB
// 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', };