blob: 0f1d0ec4c5a30a5cd01dc87ca7526266a4d33e83 [file] [log] [blame]
// 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.
Accessibility.AXBreadcrumbsPane = class extends Accessibility.AccessibilitySubPane {
/**
* @param {!Accessibility.AccessibilitySidebarView} axSidebarView
*/
constructor(axSidebarView) {
super(Common.UIString('Accessibility Tree'));
this.element.classList.add('ax-subpane');
this._axSidebarView = axSidebarView;
/** @type {?Accessibility.AXBreadcrumb} */
this._preselectedBreadcrumb = null;
this._selectedByUser = true;
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);
}
/**
* @param {?Accessibility.AccessibilityNode} axNode
* @override
*/
setAXNode(axNode) {
super.setAXNode(axNode);
this._rootElement.removeChildren();
if (!axNode) {
this._selectedByUser = false;
return;
}
var ancestorChain = [];
var ancestor = axNode;
while (ancestor) {
ancestorChain.push(ancestor);
ancestor = ancestor.parentNode();
}
ancestorChain.reverse();
var depth = 0;
var breadcrumb = null;
for (ancestor of ancestorChain) {
breadcrumb = new Accessibility.AXBreadcrumb(ancestor, depth, (ancestor === axNode));
if (ancestor.children().length)
breadcrumb.element().classList.add('parent');
this._rootElement.appendChild(breadcrumb.element());
depth++;
}
var inspectedNodeBreadcrumb = breadcrumb;
inspectedNodeBreadcrumb.setPreselected(true, this._selectedByUser);
this._setPreselectedBreadcrumb(inspectedNodeBreadcrumb);
for (var child of axNode.children()) {
var childBreadcrumb = new Accessibility.AXBreadcrumb(child, depth, false);
this._rootElement.appendChild(childBreadcrumb.element());
}
this._selectedByUser = false;
}
/**
* @override
*/
wasShown() {
this._selectedByUser = true;
}
/**
* @override
*/
willHide() {
this._setPreselectedBreadcrumb(null);
}
/**
* @param {!Event} event
*/
_onKeyDown(event) {
if (!this._preselectedBreadcrumb)
return;
if (!event.path.some(element => element === this._preselectedBreadcrumb.element()))
return;
if (event.shiftKey || event.metaKey || event.ctrlKey)
return;
var handled = false;
if ((event.key === 'ArrowUp' || event.key === 'ArrowLeft') && !event.altKey)
handled = this._preselectPrevious();
else if ((event.key === 'ArrowDown' || event.key === 'ArrowRight') && !event.altKey)
handled = this._preselectNext();
else if (isEnterKey(event))
handled = this._inspectDOMNode(this._preselectedBreadcrumb.axNode());
if (handled)
event.consume(true);
}
/**
* @return {boolean}
*/
_preselectPrevious() {
var previousElement = this._preselectedBreadcrumb.element().previousSibling;
if (!previousElement)
return false;
this._selectedByUser = true;
this._setPreselectedBreadcrumb(previousElement.breadcrumb);
return true;
}
/**
* @return {boolean}
*/
_preselectNext() {
var nextElement = this._preselectedBreadcrumb.element().nextSibling;
if (!nextElement)
return false;
this._selectedByUser = true;
this._setPreselectedBreadcrumb(nextElement.breadcrumb);
return true;
}
/**
* @param {?Accessibility.AXBreadcrumb} breadcrumb
*/
_setPreselectedBreadcrumb(breadcrumb) {
if (breadcrumb === this._preselectedBreadcrumb)
return;
if (this._preselectedBreadcrumb)
this._preselectedBreadcrumb.setPreselected(false, this._selectedByUser);
this._preselectedBreadcrumb = breadcrumb;
if (this._preselectedBreadcrumb)
this._preselectedBreadcrumb.setPreselected(true, this._selectedByUser);
else if (this._selectedByUser)
SDK.OverlayModel.hideDOMNodeHighlight();
}
/**
* @param {!Event} event
*/
_onMouseLeave(event) {
this._setHoveredBreadcrumb(null);
}
/**
* @param {!Event} event
*/
_onMouseMove(event) {
var breadcrumbElement = event.target.enclosingNodeOrSelfWithClass('ax-node');
if (!breadcrumbElement) {
this._setHoveredBreadcrumb(null);
return;
}
var breadcrumb = breadcrumbElement.breadcrumb;
if (breadcrumb.preselected() || breadcrumb.inspected() || !breadcrumb.isDOMNode())
return;
this._setHoveredBreadcrumb(breadcrumb);
}
/**
* @param {!Event} event
*/
_onClick(event) {
var breadcrumbElement = event.target.enclosingNodeOrSelfWithClass('ax-node');
if (!breadcrumbElement) {
this._setHoveredBreadcrumb(null);
return;
}
var breadcrumb = breadcrumbElement.breadcrumb;
if (breadcrumb.inspected()) {
// If the user is clicking the inspected breadcrumb, they probably want to
// focus it.
breadcrumb.element().focus();
return;
}
if (!breadcrumb.isDOMNode())
return;
this._inspectDOMNode(breadcrumb.axNode());
}
/**
* @param {?Accessibility.AXBreadcrumb} breadcrumb
*/
_setHoveredBreadcrumb(breadcrumb) {
if (breadcrumb === this._hoveredBreadcrumb)
return;
if (this._hoveredBreadcrumb)
this._hoveredBreadcrumb.setHovered(false);
if (breadcrumb) {
breadcrumb.setHovered(true);
} else if (this.node()) {
// Highlight and scroll into view the currently inspected node.
this.node().domModel().overlayModel().nodeHighlightRequested(this.node().id);
}
this._hoveredBreadcrumb = breadcrumb;
}
/**
* @param {!Accessibility.AccessibilityNode} axNode
* @return {boolean}
*/
_inspectDOMNode(axNode) {
if (!axNode.isDOMNode())
return false;
this._selectedByUser = true;
axNode.deferredDOMNode().resolve(domNode => {
var inspectedDOMNode = UI.context.flavor(SDK.DOMNode);
// Special case the root accessibility node: set the node for the
// accessibility panel, not the Elements tree, as it maps to the Document
// node which is not shown in the DOM panel, causing the first child to be
// inspected instead.
if (axNode.parentNode() && domNode !== inspectedDOMNode)
Common.Revealer.reveal(domNode, true /* omitFocus */);
else
this._axSidebarView.setNode(domNode);
});
return true;
}
};
Accessibility.AXBreadcrumb = class {
/**
* @param {!Accessibility.AccessibilityNode} axNode
* @param {number} depth
* @param {boolean} inspected
*/
constructor(axNode, depth, inspected) {
/** @type {!Accessibility.AccessibilityNode} */
this._axNode = axNode;
this._element = createElementWithClass('div', 'ax-node');
this._element.breadcrumb = this;
this._selectionElement = createElementWithClass('div', 'selection fill');
this._element.appendChild(this._selectionElement);
this._nodeWrapper = createElementWithClass('span', 'wrapper');
this._element.appendChild(this._nodeWrapper);
this._hovered = false;
this._preselected = false;
this._inspected = inspected;
this.element().classList.toggle('inspected', inspected);
this._element.style.paddingLeft = (16 * depth + 4) + 'px';
if (this._axNode.ignored()) {
this._appendIgnoredNodeElement();
} else {
this._appendRoleElement(this._axNode.role());
if (this._axNode.name() && this._axNode.name().value) {
this._nodeWrapper.createChild('span', 'separator').textContent = '\u00A0';
this._appendNameElement(/** @type {string} */ (this._axNode.name().value));
}
}
if (this._axNode.hasOnlyUnloadedChildren())
this._element.classList.add('children-unloaded');
if (!this._axNode.isDOMNode())
this._element.classList.add('no-dom-node');
}
/**
* @return {!Element}
*/
element() {
return this._element;
}
/**
* @return {boolean}
*/
preselected() {
return this._preselected;
}
/**
* @param {boolean} preselected
* @param {boolean} selectedByUser
*/
setPreselected(preselected, selectedByUser) {
if (this._preselected === preselected)
return;
this._preselected = preselected;
this.element().classList.toggle('preselected', preselected);
if (preselected)
this.element().setAttribute('tabIndex', 0);
else
this.element().removeAttribute('tabIndex');
if (this._preselected) {
if (selectedByUser)
this.element().focus();
if (!this._inspected)
this._axNode.highlightDOMNode();
else
SDK.OverlayModel.hideDOMNodeHighlight();
}
}
/**
* @param {boolean} hovered
*/
setHovered(hovered) {
if (this._hovered === hovered)
return;
this._hovered = hovered;
this.element().classList.toggle('hovered', hovered);
if (this._hovered) {
this.element().classList.toggle('hovered', true);
this._axNode.highlightDOMNode();
}
}
/**
* @return {!Accessibility.AccessibilityNode}
*/
axNode() {
return this._axNode;
}
/**
* @return {boolean}
*/
inspected() {
return this._inspected;
}
/**
* @return {boolean}
*/
isDOMNode() {
return this._axNode.isDOMNode();
}
/**
* @param {string} name
*/
_appendNameElement(name) {
var nameElement = createElement('span');
nameElement.textContent = '"' + name + '"';
nameElement.classList.add('ax-readable-string');
this._nodeWrapper.appendChild(nameElement);
}
/**
* @param {?Protocol.Accessibility.AXValue} role
*/
_appendRoleElement(role) {
if (!role)
return;
var roleElement = createElementWithClass('span', 'monospace');
roleElement.classList.add(Accessibility.AXBreadcrumb.RoleStyles[role.type]);
roleElement.setTextContentTruncatedIfNeeded(role.value || '');
this._nodeWrapper.appendChild(roleElement);
}
_appendIgnoredNodeElement() {
var ignoredNodeElement = createElementWithClass('span', 'monospace');
ignoredNodeElement.textContent = Common.UIString('Ignored');
ignoredNodeElement.classList.add('ax-breadcrumbs-ignored-node');
this._nodeWrapper.appendChild(ignoredNodeElement);
}
};
/** @type {!Object<string, string>} */
Accessibility.AXBreadcrumb.RoleStyles = {
internalRole: 'ax-internal-role',
role: 'ax-role',
};