blob: 07cebfb5f7e537bfff222e69648b5122da9a1fbe [file] [log] [blame]
// Copyright 2019 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.
const AutomationNode = chrome.automation.AutomationNode;
/**
* This class handles interactions with an onscreen element based on a single
* AutomationNode.
*/
class NodeWrapper extends SAChildNode {
/**
* @param {!AutomationNode} baseNode
* @param {?SARootNode} parent
*/
constructor(baseNode, parent) {
super(SwitchAccessPredicate.isGroup(baseNode, parent));
/** @private {!AutomationNode} */
this.baseNode_ = baseNode;
}
/** @override */
equals(other) {
if (!other || !(other instanceof NodeWrapper)) return false;
other = /** @type {!NodeWrapper} */ (other);
return other.baseNode_ === this.baseNode_;
}
/** @override */
get role() {
return this.baseNode_.role;
}
/** @override */
get location() {
return this.baseNode_.location;
}
/** @override */
get automationNode() {
return this.baseNode_;
}
/** @override */
get actions() {
let actions = [];
if (SwitchAccessPredicate.isTextInput(this.baseNode_)) {
actions.push(SAConstants.MenuAction.OPEN_KEYBOARD);
actions.push(SAConstants.MenuAction.DICTATION);
} else {
actions.push(SAConstants.MenuAction.SELECT);
}
const ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) {
if (ancestor.scrollX > ancestor.scrollXMin) {
actions.push(SAConstants.MenuAction.SCROLL_LEFT);
}
if (ancestor.scrollX < ancestor.scrollXMax) {
actions.push(SAConstants.MenuAction.SCROLL_RIGHT);
}
if (ancestor.scrollY > ancestor.scrollYMin) {
actions.push(SAConstants.MenuAction.SCROLL_UP);
}
if (ancestor.scrollY < ancestor.scrollYMax) {
actions.push(SAConstants.MenuAction.SCROLL_DOWN);
}
}
const standardActions = /** @type {!Array<!SAConstants.MenuAction>} */ (
this.baseNode_.standardActions.filter(
action => Object.values(SAConstants.MenuAction).includes(action)));
return actions.concat(standardActions);
}
/** @override */
performAction(action) {
let ancestor;
switch (action) {
case SAConstants.MenuAction.OPEN_KEYBOARD:
this.baseNode_.focus();
return true;
case SAConstants.MenuAction.SELECT:
this.baseNode_.doDefault();
return true;
case SAConstants.MenuAction.DICTATION:
chrome.accessibilityPrivate.toggleDictation();
return true;
case SAConstants.MenuAction.SCROLL_DOWN:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) ancestor.scrollDown(() => {});
return true;
case SAConstants.MenuAction.SCROLL_UP:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) ancestor.scrollUp(() => {});
return true;
case SAConstants.MenuAction.SCROLL_RIGHT:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) ancestor.scrollRight(() => {});
return true;
case SAConstants.MenuAction.SCROLL_LEFT:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) ancestor.scrollLeft(() => {});
return true;
default:
if (Object.values(chrome.automation.ActionType).includes(action)) {
this.baseNode_.performStandardAction(
/** @type {chrome.automation.ActionType} */ (action));
}
return true;
}
}
/**
* @return {AutomationNode}
* @protected
*/
getScrollableAncestor_() {
let ancestor = this.baseNode_;
while (!ancestor.scrollable && ancestor.parent)
ancestor = ancestor.parent;
return ancestor;
}
/** @override */
isEquivalentTo(node) {
return this.baseNode_ === node;
}
/** @override */
asRootNode() {
if (!this.isGroup()) return null;
return RootNodeWrapper.buildTree(this.baseNode_);
}
}
/**
* This class handles constructing and traversing a group of onscreen elements
* based on all the interesting descendants of a single AutomationNode.
*/
class RootNodeWrapper extends SARootNode {
/**
* @param {!AutomationNode} baseNode
*/
constructor(baseNode) {
super();
/** @private {!AutomationNode} */
this.baseNode_ = baseNode;
}
/** @override */
equals(other) {
if (!(other instanceof RootNodeWrapper)) return false;
other = /** @type {!RootNodeWrapper} */ (other);
return super.equals(other) && this.baseNode_ === other.baseNode_;
}
/** @override */
get location() {
return this.baseNode_.location || super.location;
}
/** @override */
isValid() {
return !!this.baseNode_.role;
}
/** @override */
isEquivalentTo(automationNode) {
return this.baseNode_ === automationNode;
}
/** @override */
get automationNode() {
return this.baseNode_;
}
/**
* @param {!AutomationNode} rootNode
* @return {!RootNodeWrapper}
*/
static buildTree(rootNode) {
const root = new RootNodeWrapper(rootNode);
const childConstructor = (node) => new NodeWrapper(node, root);
RootNodeWrapper.buildHelper(root, childConstructor);
return root;
}
/**
* Helper function to connect tree elements, given constructors for the root
* and child types.
* @param {!RootNodeWrapper} root
* @param {function(!AutomationNode): !SAChildNode} childConstructor
* Constructs a child node from an automation node.
*/
static buildHelper(root, childConstructor) {
const interestingChildren = RootNodeWrapper.getInterestingChildren(root);
if (interestingChildren.length < 1) {
throw new Error('Root node must have at least 1 interesting child.');
}
const backButton = new BackButtonNode(root);
let children = interestingChildren.map(childConstructor);
children.push(backButton);
SARootNode.connectChildren(children);
root.setChildren_(children);
return;
}
/**
* @param {!AutomationNode} desktop
* @return {!RootNodeWrapper}
*/
static buildDesktopTree(desktop) {
const root = new RootNodeWrapper(desktop);
const interestingChildren = RootNodeWrapper.getInterestingChildren(root);
if (interestingChildren.length < 1) {
throw new Error('Desktop node must have at least 1 interesting child.');
}
const childConstructor = (autoNode) => new NodeWrapper(autoNode, root);
let children = interestingChildren.map(childConstructor);
SARootNode.connectChildren(children);
root.setChildren_(children);
return root;
}
/**
* @param {!RootNodeWrapper} root
* @return {!Array<!AutomationNode>}
*/
static getInterestingChildren(root) {
let interestingChildren = [];
let treeWalker = new AutomationTreeWalker(
root.baseNode_, constants.Dir.FORWARD,
SwitchAccessPredicate.restrictions(root));
let node = treeWalker.next().node;
while (node) {
interestingChildren.push(node);
node = treeWalker.next().node;
}
return interestingChildren;
}
}