Commit dea14d21 authored by Katie D's avatar Katie D Committed by Commit Bot

[Switch access] Don't navigate to occluded windows.

Check if the top center of a window is visible before moving
SA focus to that window.

It's still possible to end up at an occluded window when restoring
SA focus from history, or by going to firstValidChild. We could
add an eventhandler to the desktop or a repeated interval to
each window node child to check for occlusion. This is left as
a TODO.

AX-Relnotes: N/A
Bug: 1106080
Change-Id: Ib88d53a3b18b8c9f5da2403b61d1624e1258014c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2318245
Commit-Queue: Katie Dektar <katie@chromium.org>
Reviewed-by: default avatarAnastasia Helfinstein <anastasi@google.com>
Cr-Commit-Position: refs/heads/master@{#792318}
parent 83cd0256
......@@ -324,6 +324,7 @@ js_library("navigation_manager") {
":switch_access_constants",
":switch_access_node",
":switch_access_predicate",
"../common:automation_util",
"../common:event_handler",
"../common:repeated_event_handler",
]
......
......@@ -41,6 +41,10 @@ class FocusHistory {
* @return {boolean} Whether the history was rebuilt from the given node.
*/
buildFromAutomationNode(node) {
if (!node.parent) {
// No ancestors, cannot create stack.
return false;
}
// Create a list of ancestors.
const ancestorStack = [node];
while (node.parent) {
......
......@@ -16,6 +16,12 @@ class NavigationManager {
this.group_ = DesktopNode.build(this.desktop_);
/** @private {!SAChildNode} */
// TODO(crbug.com/1106080): It is possible for the firstChild to be a
// window which is occluded, for example if Switch Access is turned on
// when the user has several browser windows opened. We should either
// dynamically pick this.node_'s initial value based on an occlusion check,
// or ensure that we move away from occluded children as quickly as soon
// as they are detected using an interval set in DesktopNode.
this.node_ = this.group_.firstChild;
/** @private {!FocusHistory} */
......@@ -144,7 +150,8 @@ class NavigationManager {
static moveBackward() {
const navigator = NavigationManager.instance;
if (navigator.node_.isValidAndVisible()) {
navigator.setNode_(navigator.node_.previous);
NavigationManager.tryMoving(
navigator.node_.previous, (node) => node.previous, navigator.node_);
} else {
NavigationManager.moveToValidNode();
}
......@@ -156,12 +163,67 @@ class NavigationManager {
static moveForward() {
const navigator = NavigationManager.instance;
if (navigator.node_.isValidAndVisible()) {
navigator.setNode_(navigator.node_.next);
NavigationManager.tryMoving(
navigator.node_.next, (node) => node.next, navigator.node_);
} else {
NavigationManager.moveToValidNode();
}
}
/**
* Tries to move to another node, |node|, but if |node| is a window that's not
* in the foreground it will use |getNext| to find the next node to try.
* Checks against |startingNode| to ensure we don't get stuck in an infinite
* loop.
* @param {!SAChildNode} node The node to try to move into.
* @param {function(!SAChildNode): !SAChildNode} getNext gets the next node to
* try if we cannot move to |next|. Takes |next| as a parameter.
* @param {!SAChildNode} startingNode The first node in the sequence. If we
* loop back to this node, stop trying to move, as there are no other
* nodes we can move to.
*/
static tryMoving(node, getNext, startingNode) {
if (node == startingNode) {
// This should only happen if the desktop contains exactly one interesting
// child and all other children are windows which are occluded.
// Unlikely to happen since we can always access the shelf.
return;
}
const navigator = NavigationManager.instance;
const baseNode = node.automationNode;
if (!(node instanceof NodeWrapper) || !baseNode) {
navigator.setNode_(node);
return;
}
if (!SwitchAccessPredicate.isWindow(baseNode)) {
navigator.setNode_(node);
return;
}
const location = node.location;
if (!location) {
// Closure compiler doesn't realize we already checked isValidAndVisible
// before calling tryMoving, so we need to explicitly check location here
// so that RectHelper.center does not cause a closure error.
NavigationManager.moveToValidNode();
return;
}
const center = RectHelper.center(location);
// Check if the top center is visible as a proxy for occlusion. It's
// possible that other parts of the window are occluded, but in Chrome we
// can't drag windows off the top of the screen.
navigator.desktop_.hitTestWithReply(center.x, location.top, (hitNode) => {
if (AutomationUtil.isDescendantOf(
hitNode,
/** @type {!AutomationNode} */ (baseNode))) {
navigator.setNode_(node);
} else if (node.isValidAndVisible()) {
NavigationManager.tryMoving(getNext(node), getNext, startingNode);
} else {
NavigationManager.moveToValidNode();
}
});
}
/**
* Moves to the Switch Access focus up the group stack closest to the ancestor
* that hasn't been invalidated.
......
......@@ -83,6 +83,10 @@ class DesktopNode extends RootNodeWrapper {
false /* shouldRecover */);
}
// TODO(crbug.com/1106080): Add hittest intervals to new children which are
// SwitchAccessPredicate.isWindow to check whether those children are
// occluded or visible. Remove any intervals on the previous window
// children before reassigning root.children.
root.children = interestingChildren.map(childConstructor);
}
}
......@@ -122,9 +122,13 @@ class NodeWrapper extends SAChildNode {
onFocus() {
super.onFocus();
this.locationChangedHandler_ = new RepeatedEventHandler(
this.baseNode_, chrome.automation.EventType.LOCATION_CHANGED,
() => FocusRingManager.setFocusedNode(this),
{exactMatch: true, allAncestors: true});
this.baseNode_, chrome.automation.EventType.LOCATION_CHANGED, () => {
if (this.isValidAndVisible()) {
FocusRingManager.setFocusedNode(this);
} else {
NavigationManager.moveToValidNode();
}
}, {exactMatch: true, allAncestors: true});
}
/** @override */
......@@ -354,9 +358,7 @@ class RootNodeWrapper extends SARootNode {
* @return {!RootNodeWrapper}
*/
static buildTree(rootNode) {
if (rootNode.role === chrome.automation.RoleType.WINDOW ||
(rootNode.role === chrome.automation.RoleType.CLIENT &&
rootNode.parent.role === chrome.automation.RoleType.WINDOW)) {
if (SwitchAccessPredicate.isWindow(rootNode)) {
return WindowRootNode.buildTree(rootNode);
}
if (rootNode.role === chrome.automation.RoleType.KEYBOARD) {
......
......@@ -182,6 +182,16 @@ const SwitchAccessPredicate = {
*/
isTextInput: (node) => !!node && !!node.state[StateType.EDITABLE],
/**
* Returns true if |node| should be considered a window.
* @param {AutomationNode} node
* @return {boolean}
*/
isWindow: (node) => !!node &&
(node.role === chrome.automation.RoleType.WINDOW ||
(node.role === chrome.automation.RoleType.CLIENT && !!node.parent &&
node.parent.role === chrome.automation.RoleType.WINDOW)),
/**
* Returns a Restrictions object ready to be passed to AutomationTreeWalker.
*
......
......@@ -15,6 +15,7 @@
"common/constants.js",
"common/array_util.js",
"common/automation_predicate.js",
"common/automation_util.js",
"common/event_handler.js",
"common/repeated_event_handler.js",
"common/repeated_tree_change_handler.js",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment