blob: 4d8f1a2b9045fd20c10febe395acdbee6b0b04ca [file] [log] [blame]
// Copyright 2017 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 StateType = chrome.automation.StateType;
const RoleType = chrome.automation.RoleType;
const DefaultActionVerb = chrome.automation.DefaultActionVerb;
const GROUP_INTERESTING_CHILD_THRESHOLD = 2;
/**
* Contains predicates for the chrome automation API. The following basic
* predicates are available:
* - isActionable
* - isGroup
* - isInteresting
* - isInterestingSubtree
* - isTextInput
* - isNotContainer
* - isSwitchAccessMenuPanel
*
* In addition to these basic predicates, there are also methods to get the
* restrictions required by TreeWalker for specific traversal situations.
*/
const SwitchAccessPredicate = {
/**
* Returns true if |node| is actionable, meaning that a user can interact with
* it in some way.
*
* @param {!chrome.automation.AutomationNode} node
* @return {boolean}
*/
isActionable: (node) => {
const defaultActionVerb = node.defaultActionVerb;
const loc = node.location;
const parent = node.parent;
const root = node.root;
const role = node.role;
const state = node.state;
// Skip things that are offscreen or invisible.
if (state[StateType.OFFSCREEN] || loc.top < 0 || loc.left < 0 ||
state[StateType.INVISIBLE]) {
return false;
}
// Skip things that are disabled.
if (node.restriction === chrome.automation.Restriction.DISABLED) {
return false;
}
// These web containers are not directly actionable.
if (role === RoleType.WEB_VIEW || role === RoleType.ROOT_WEB_AREA) {
return false;
}
if (parent) {
// crbug.com/710559
// Work around for browser tabs.
if (role === RoleType.TAB && parent.role === RoleType.TAB_LIST &&
root.role === RoleType.DESKTOP) {
return true;
}
}
// Check various indicators that the node is actionable.
if (role === RoleType.BUTTON || role === RoleType.SLIDER) return true;
if (SwitchAccessPredicate.isTextInput(node)) return true;
if (defaultActionVerb &&
(defaultActionVerb === DefaultActionVerb.ACTIVATE ||
defaultActionVerb === DefaultActionVerb.CHECK ||
defaultActionVerb === DefaultActionVerb.OPEN ||
defaultActionVerb === DefaultActionVerb.PRESS ||
defaultActionVerb === DefaultActionVerb.SELECT ||
defaultActionVerb === DefaultActionVerb.UNCHECK)) {
return true;
}
if (role === RoleType.LIST_ITEM &&
defaultActionVerb === DefaultActionVerb.CLICK) {
return true;
}
// Focusable items should be surfaced as either groups or actionable. So
// should menu items.
// Current heuristic is to show as actionble any focusable item where no
// child is an interesting subtree.
if (state[StateType.FOCUSABLE] || role === RoleType.MENU_ITEM) {
return !node.children.some(SwitchAccessPredicate.isInterestingSubtree);
}
return false;
},
/**
* Returns true if |node| is a group, meaning that the node has more than one
* interesting descendant, and that its interesting descendants exist in more
* than one subtree of its immediate children.
*
* Additionally, for |node| to be a group, it cannot have the same bounding
* box as its scope.
*
* @param {!chrome.automation.AutomationNode} node
* @param {SARootNode} scope
* @return {boolean}
*/
isGroup: (node, scope) => {
if (scope && !scope.isEquivalentTo(node) &&
RectHelper.areEqual(node.location, scope.location)) {
return false;
}
if (node.state[StateType.INVISIBLE]) return false;
let interestingBranchesCount =
SwitchAccessPredicate.isActionable(node) ? 1 : 0;
let child = node.firstChild;
while (child) {
if (SwitchAccessPredicate.isInterestingSubtree(child)) {
interestingBranchesCount += 1;
}
if (interestingBranchesCount >= GROUP_INTERESTING_CHILD_THRESHOLD) {
return true;
}
child = child.nextSibling;
}
return false;
},
/**
* Returns true if |node| is interesting for the user, meaning that |node|
* is either actionable or a group.
*
* @param {!chrome.automation.AutomationNode} node
* @param {!SARootNode} scope
* @return {boolean}
*/
isInteresting: (node, scope) => SwitchAccessPredicate.isActionable(node) ||
SwitchAccessPredicate.isGroup(node, scope),
/**
* Returns true if there is an interesting node in the subtree containing
* |node| as its root (including |node| itself).
*
* This function does not call isInteresting directly, because that would
* cause a loop (isInteresting calls isGroup, and isGroup calls
* isInterestingSubtree).
*
* @param {!chrome.automation.AutomationNode} node
* @return {boolean}
*/
isInterestingSubtree: (node) => SwitchAccessPredicate.isActionable(node) ||
node.children.some(SwitchAccessPredicate.isInterestingSubtree),
/**
* Returns true if |node| is an element that contains editable text.
* @param {chrome.automation.AutomationNode} node
* @return {boolean}
*/
isTextInput: (node) => !!node && !!node.state[StateType.EDITABLE],
/**
* Returns true if |node| does not have a role of desktop, window, web view,
* or root web area.
* @param {!chrome.automation.AutomationNode} node
* @return {boolean}
*/
isNotContainer: (node) => node.role !== RoleType.ROOT_WEB_AREA &&
node.role !== RoleType.WINDOW && node.role !== RoleType.DESKTOP &&
node.role !== RoleType.WEB_VIEW,
/**
* Returns true if |node| is the Switch Access menu.
* @param {!chrome.automation.AutomationNode} node
* @return {boolean}
*/
isSwitchAccessMenuPanel: (node) =>
node.htmlAttributes.id === SAConstants.MENU_PANEL_ID,
/**
* Returns a Restrictions object ready to be passed to AutomationTreeWalker.
*
* @param {!SARootNode} scope
* @return {!AutomationTreeWalkerRestriction}
*/
restrictions: (scope) => {
return {
leaf: SwitchAccessPredicate.leaf(scope),
root: SwitchAccessPredicate.root(scope),
visit: SwitchAccessPredicate.visit(scope)
};
},
/**
* Creates a function that confirms if |node| is a terminal leaf node of a
* SwitchAccess scope tree when |scope| is the root.
*
* @param {!SARootNode} scope
* @return {function(!chrome.automation.AutomationNode): boolean}
*/
leaf: function(scope) {
return (node) => node.state[StateType.INVISIBLE] ||
(!scope.isEquivalentTo(node) &&
SwitchAccessPredicate.isInteresting(node, scope)) ||
!SwitchAccessPredicate.isInterestingSubtree(node);
},
/**
* Creates a function that confirms if |node| is the root of a SwitchAccess
* scope tree when |scope| is the root.
*
* @param {!SARootNode} scope
* @return {function(!chrome.automation.AutomationNode): boolean}
*/
root: function(scope) {
return (node) => scope.isEquivalentTo(node);
},
/**
* Creates a function that determines whether |node| is to be visited in the
* SwitchAccess scope tree with |scope| as the root.
*
* @param {!SARootNode} scope
* @return {function(!chrome.automation.AutomationNode): boolean}
*/
visit: function(scope) {
return (node) => node.role !== RoleType.DESKTOP &&
SwitchAccessPredicate.isInteresting(node, scope);
},
/**
* Returns a Restrictions object for finding the Switch Access Menu root.
* @return {!AutomationTreeWalkerRestriction}
*/
switchAccessMenuPanelDiscoveryRestrictions: () => {
return {
leaf: SwitchAccessPredicate.isNotContainer,
visit: SwitchAccessPredicate.isSwitchAccessMenuPanel
};
}
};