blob: 51bac34874b96e6cae1c6dd0899b9f5080711ada [file] [log] [blame]
// Copyright 2018 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.
/**
* Class to handle interactions with the Switch Access menu, including moving
* through and selecting actions.
*/
class MenuManager {
/**
* @param {!NavigationManager} navigationManager
* @param {!chrome.automation.AutomationNode} desktop
*/
constructor(navigationManager, desktop) {
/**
* A list of the Menu actions that are currently enabled.
* @private {!Array<MenuManager.Action>}
*/
this.actions_ = [];
/**
* The parent automation manager.
* @private {!NavigationManager}
*/
this.navigationManager_ = navigationManager;
/**
* The root node of the screen.
* @private {!chrome.automation.AutomationNode}
*/
this.desktop_ = desktop;
/**
* The root node of the menu.
* @private {chrome.automation.AutomationNode}
*/
this.menuNode_;
/**
* The current node of the menu.
* @private {chrome.automation.AutomationNode}
*/
this.node_;
/**
* Keeps track of when we're in the Switch Access menu.
* @private {boolean}
*/
this.inMenu_ = false;
this.init_();
}
/**
* Enter the menu and highlight the first available action.
*
* @param {!chrome.automation.AutomationNode} navNode the currently selected
* node, for which the menu is to be displayed.
*/
enter(navNode) {
const actions = this.getActionsForNode_(navNode);
this.inMenu_ = true;
if (actions !== this.actions_) {
this.actions_ = actions;
MessageHandler.sendMessage(
MessageHandler.Destination.MENU_PANEL, 'setActions', actions);
}
this.node_ = this.menuNode();
if (navNode.location)
chrome.accessibilityPrivate.setSwitchAccessMenuState(
true, navNode.location);
else
console.log('Unable to show Switch Access menu.');
this.moveForward();
}
/**
* Exits the menu.
*/
exit() {
this.clearFocusRing_();
this.inMenu_ = false;
if (this.node_)
this.node_ = null;
chrome.accessibilityPrivate.setSwitchAccessMenuState(
false, MenuManager.EmptyLocation);
}
/**
* Move to the next available action in the menu. If this is no next action,
* focus the whole menu to loop again.
* @return {boolean} Whether this function had any effect.
*/
moveForward() {
if (!this.node_ || !this.inMenu_)
return false;
this.clearFocusRing_();
const treeWalker = new AutomationTreeWalker(
this.node_, constants.Dir.FORWARD,
SwitchAccessPredicate.restrictions(this.menuNode()));
const node = treeWalker.next().node;
if (!node)
this.node_ = this.menuNode();
else
this.node_ = node;
this.updateFocusRing_();
return true;
}
/**
* Move to the previous available action in the menu. If we're at the
* beginning of the list, start again at the end.
* @return {boolean} Whether this function had any effect.
*/
moveBackward() {
if (!this.node_ || !this.inMenu_)
return false;
this.clearFocusRing_();
const treeWalker = new AutomationTreeWalker(
this.node_, constants.Dir.BACKWARD,
SwitchAccessPredicate.restrictions(this.menuNode()));
let node = treeWalker.next().node;
// If node is null, find the last enabled button.
let lastChild = this.menuNode().lastChild;
while (!node && lastChild) {
if (SwitchAccessPredicate.isActionable(lastChild)) {
node = lastChild;
break;
} else {
lastChild = lastChild.previousSibling;
}
}
this.node_ = node;
this.updateFocusRing_();
return true;
}
/**
* Perform the action indicated by the current button (or no action if the
* entire menu is selected). Then exit the menu and return to traditional
* navigation.
* @return {boolean} Whether this function had any effect.
*/
selectCurrentNode() {
if (!this.node_ || !this.inMenu_)
return false;
this.clearFocusRing_();
this.node_.doDefault();
this.exit();
return true;
}
/**
* Get the menu node. If it's not defined, search for it.
* @return {!chrome.automation.AutomationNode}
*/
menuNode() {
if (this.menuNode_)
return this.menuNode_;
const treeWalker = new AutomationTreeWalker(
this.desktop_, constants.Dir.FORWARD,
SwitchAccessPredicate.switchAccessMenuDiscoveryRestrictions());
const node = treeWalker.next().node;
if (node) {
this.menuNode_ = node;
return this.menuNode_;
}
console.log('Unable to find the Switch Access menu.');
return this.desktop_;
}
/**
* Clear the focus ring.
* @private
*/
clearFocusRing_() {
this.updateFocusRing_(true);
}
/**
* Determines which menu actions are relevant, given the current node.
* @param {!chrome.automation.AutomationNode} node
* @return {!Array<MenuManager.Action>}
* @private
*/
getActionsForNode_(node) {
let actions = [MenuManager.Action.SELECT, MenuManager.Action.OPTIONS];
if (SwitchAccessPredicate.isTextInput(node))
actions.push(MenuManager.Action.DICTATION);
if (node.scrollable) {
// TODO(crbug/920659) This does not work for ARC++. Implement scroll
// directions via standardActions.
if (node.scrollX > node.scrollXMin)
actions.push(MenuManager.Action.SCROLL_LEFT);
if (node.scrollX < node.scrollXMax)
actions.push(MenuManager.Action.SCROLL_RIGHT);
if (node.scrollY > node.scrollYMin)
actions.push(MenuManager.Action.SCROLL_UP);
if (node.scrollY < node.scrollYMax)
actions.push(MenuManager.Action.SCROLL_DOWN);
}
return actions;
}
/**
* @private
*/
init_() {
// Listen for messages from the menu.
window.addEventListener('message', this.onMessageReceived_.bind(this));
}
/**
* Receive a message from the Switch Access menu, and perform the appropriate
* action.
* @private
*/
onMessageReceived_(event) {
this.exit();
if (event.data === MenuManager.Action.SELECT)
this.navigationManager_.selectCurrentNode();
else if (event.data === MenuManager.Action.DICTATION)
chrome.accessibilityPrivate.toggleDictation();
else if (event.data === MenuManager.Action.OPTIONS)
window.switchAccess.showOptionsPage();
else if (
event.data === MenuManager.Action.SCROLL_DOWN ||
event.data === MenuManager.Action.SCROLL_UP ||
event.data === MenuManager.Action.SCROLL_LEFT ||
event.data === MenuManager.Action.SCROLL_RIGHT)
this.navigationManager_.scroll(event.data);
}
/**
* Send a message to the menu to update the focus ring around the current
* node.
* TODO(anastasi): Revisit focus rings before launch
* @private
* @param {boolean=} opt_clear If true, will clear the focus ring.
*/
updateFocusRing_(opt_clear) {
if (!this.node_)
return;
const id = this.node_.htmlAttributes.id;
const onOrOff = opt_clear ? 'off' : 'on';
MessageHandler.sendMessage(
MessageHandler.Destination.MENU_PANEL, 'setFocusRing', [id, onOrOff]);
}
}
/**
* Actions available in the Switch Access Menu.
* @enum {string}
* @const
*/
MenuManager.Action = {
DICTATION: 'dictation',
// This opens the Switch Access settings in a new Chrome tab.
OPTIONS: 'options',
SCROLL_BACKWARD: 'scroll-backward',
SCROLL_DOWN: 'scroll-down',
SCROLL_FORWARD: 'scroll-forward',
SCROLL_LEFT: 'scroll-left',
SCROLL_RIGHT: 'scroll-right',
SCROLL_UP: 'scroll-up',
// This either performs the default action or enters a new scope, as
// applicable.
SELECT: 'select'
};
/**
* The ID for the div containing the Switch Access menu.
* @const
*/
MenuManager.MenuId = 'switchaccess_menu_actions';
/**
* Empty location, used for hiding the menu.
* @const
*/
MenuManager.EmptyLocation = {
left: 0,
top: 0,
width: 0,
height: 0
};