blob: 9075c5e80ebf1b8a4d1b7a07c496f0c0875ed1aa [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(ls`Accessibility Tree`);
this.element.classList.add('ax-subpane');
UI.ARIAUtils.markAsTree(this.element);
this.element.tabIndex = -1;
this._axSidebarView = axSidebarView;
/** @type {?Accessibility.AXBreadcrumb} */
this._preselectedBreadcrumb = null;
/** @type {?Accessibility.AXBreadcrumb} */
this._inspectedNodeBreadcrumb = null;
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');
}
/**
* @override
*/
focus() {
if (this._inspectedNodeBreadcrumb)
this._inspectedNodeBreadcrumb.nodeElement().focus();
else
this.element.focus();
}
/**
* @param {?Accessibility.AccessibilityNode} axNode
* @override
*/
setAXNode(axNode) {
const hadFocus = this.element.hasFocus();
super.setAXNode(axNode);
this._rootElement.removeChildren();
if (!axNode)
return;
const ancestorChain = [];
let ancestor = axNode;
while (ancestor) {
ancestorChain.push(ancestor);
ancestor = ancestor.parentNode();
}
ancestorChain.reverse();
let depth = 0;
let breadcrumb = null;
let parent = null;
for (ancestor of ancestorChain) {
breadcrumb = new Accessibility.AXBreadcrumb(ancestor, depth, (ancestor === axNode));
if (parent)
parent.appendChild(breadcrumb);
else
this._rootElement.appendChild(breadcrumb.element());
parent = breadcrumb;
depth++;
}
this._inspectedNodeBreadcrumb = breadcrumb;
this._inspectedNodeBreadcrumb.setPreselected(true, hadFocus);
this._setPreselectedBreadcrumb(this._inspectedNodeBreadcrumb);
/**
* @param {!Accessibility.AXBreadcrumb} parentBreadcrumb
* @param {!Accessibility.AccessibilityNode} axNode
* @param {number} localDepth
*/
function append(parentBreadcrumb, axNode, localDepth) {
const childBreadcrumb = new Accessibility.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);
}
for (const child of axNode.children())
append(this._inspectedNodeBreadcrumb, child, depth);
}
/**
* @override
*/
willHide() {
this._setPreselectedBreadcrumb(null);
}
/**
* @param {!Event} event
*/
_onKeyDown(event) {
if (!this._preselectedBreadcrumb)
return;
if (!event.composedPath().some(element => element === this._preselectedBreadcrumb.element()))
return;
if (event.shiftKey || event.metaKey || event.ctrlKey)
return;
let 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() {
const previousBreadcrumb = this._preselectedBreadcrumb.previousBreadcrumb();
if (!previousBreadcrumb)
return false;
this._setPreselectedBreadcrumb(previousBreadcrumb);
return true;
}
/**
* @return {boolean}
*/
_preselectNext() {
const nextBreadcrumb = this._preselectedBreadcrumb.nextBreadcrumb();
if (!nextBreadcrumb)
return false;
this._setPreselectedBreadcrumb(nextBreadcrumb);
return true;
}
/**
* @param {?Accessibility.AXBreadcrumb} breadcrumb
*/
_setPreselectedBreadcrumb(breadcrumb) {
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;
this._preselectedBreadcrumb.setPreselected(true, hadFocus);
if (!breadcrumb && hadFocus)
SDK.OverlayModel.hideDOMNodeHighlight();
}
/**
* @param {!Event} event
*/
_onMouseLeave(event) {
this._setHoveredBreadcrumb(null);
}
/**
* @param {!Event} event
*/
_onMouseMove(event) {
const breadcrumbElement = event.target.enclosingNodeOrSelfWithClass('ax-breadcrumb');
if (!breadcrumbElement) {
this._setHoveredBreadcrumb(null);
return;
}
const breadcrumb = breadcrumbElement.breadcrumb;
if (!breadcrumb.isDOMNode())
return;
this._setHoveredBreadcrumb(breadcrumb);
}
/**
* @param {!Event} event
*/
_onFocusOut(event) {
if (!this._preselectedBreadcrumb || event.target !== this._preselectedBreadcrumb.nodeElement())
return;
this._setPreselectedBreadcrumb(null);
}
/**
* @param {!Event} event
*/
_onClick(event) {
const breadcrumbElement = event.target.enclosingNodeOrSelfWithClass('ax-breadcrumb');
if (!breadcrumbElement) {
this._setHoveredBreadcrumb(null);
return;
}
const breadcrumb = breadcrumbElement.breadcrumb;
if (breadcrumb.inspected()) {
// If the user is clicking the inspected breadcrumb, they probably want to
// focus it.
breadcrumb.nodeElement().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;
axNode.deferredDOMNode().resolve(domNode => {
this._axSidebarView.setNode(domNode, true /* fromAXTree */);
Common.Revealer.reveal(domNode, true /* omitFocus */);
});
return true;
}
/**
* @param {!Event} event
*/
_contextMenuEventFired(event) {
const breadcrumbElement = event.target.enclosingNodeOrSelfWithClass('ax-breadcrumb');
if (!breadcrumbElement)
return;
const axNode = breadcrumbElement.breadcrumb.axNode();
if (!axNode.isDOMNode() || !axNode.deferredDOMNode())
return;
const contextMenu = new UI.ContextMenu(event);
contextMenu.viewSection().appendItem(ls`Scroll into view`, () => {
axNode.deferredDOMNode().resolvePromise().then(domNode => {
if (!domNode)
return;
domNode.scrollIntoView();
});
});
contextMenu.appendApplicableItems(axNode.deferredDOMNode());
contextMenu.show();
}
};
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-breadcrumb');
this._element.breadcrumb = this;
this._nodeElement = createElementWithClass('div', 'ax-node');
UI.ARIAUtils.markAsTreeitem(this._nodeElement);
this._nodeElement.tabIndex = -1;
this._element.appendChild(this._nodeElement);
this._nodeWrapper = createElementWithClass('div', 'wrapper');
this._nodeElement.appendChild(this._nodeWrapper);
this._selectionElement = createElementWithClass('div', 'selection fill');
this._nodeElement.appendChild(this._selectionElement);
this._childrenGroupElement = createElementWithClass('div', 'children');
UI.ARIAUtils.markAsGroup(this._childrenGroupElement);
this._element.appendChild(this._childrenGroupElement);
/** @type !Array<!Accessibility.AXBreadcrumb> */
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());
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._nodeElement.classList.add('children-unloaded');
if (!this._axNode.isDOMNode())
this._nodeElement.classList.add('no-dom-node');
}
/**
* @return {!Element}
*/
element() {
return this._element;
}
/**
* @return {!Element}
*/
nodeElement() {
return this._nodeElement;
}
/**
* @param {!Accessibility.AXBreadcrumb} breadcrumb
*/
appendChild(breadcrumb) {
this._children.push(breadcrumb);
breadcrumb.setParent(this);
this._nodeElement.classList.add('parent');
UI.ARIAUtils.setExpanded(this._nodeElement, true);
this._childrenGroupElement.appendChild(breadcrumb.element());
}
/**
* @param {!Accessibility.AXBreadcrumb} breadcrumb
*/
setParent(breadcrumb) {
this._parent = breadcrumb;
}
/**
* @return {boolean}
*/
preselected() {
return this._preselected;
}
/**
* @param {boolean} preselected
* @param {boolean} selectedByUser
*/
setPreselected(preselected, selectedByUser) {
if (this._preselected === preselected)
return;
this._preselected = preselected;
this._nodeElement.classList.toggle('preselected', preselected);
if (preselected)
this._nodeElement.setAttribute('tabIndex', 0);
else
this._nodeElement.setAttribute('tabIndex', -1);
if (this._preselected) {
if (selectedByUser)
this._nodeElement.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._nodeElement.classList.toggle('hovered', hovered);
if (this._hovered) {
this._nodeElement.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();
}
/**
* @return {?Accessibility.AXBreadcrumb}
*/
nextBreadcrumb() {
if (this._children.length)
return this._children[0];
const nextSibling = this.element().nextSibling;
if (nextSibling)
return nextSibling.breadcrumb;
return null;
}
/**
* @return {?Accessibility.AXBreadcrumb}
*/
previousBreadcrumb() {
const previousSibling = this.element().previousSibling;
if (previousSibling)
return previousSibling.breadcrumb;
return this._parent;
}
/**
* @param {string} name
*/
_appendNameElement(name) {
const 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;
const roleElement = createElementWithClass('span', 'monospace');
roleElement.classList.add(Accessibility.AXBreadcrumb.RoleStyles[role.type]);
roleElement.setTextContentTruncatedIfNeeded(role.value || '');
this._nodeWrapper.appendChild(roleElement);
}
_appendIgnoredNodeElement() {
const ignoredNodeElement = createElementWithClass('span', 'monospace');
ignoredNodeElement.textContent = ls`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',
};