Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(602)

Side by Side Diff: chrome/browser/resources/chromeos/switch_access/automation_manager.js

Issue 2872023005: Correctly follows focus and moves to valid node when current becomes invalid (Closed)
Patch Set: Responded to comments Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | chrome/browser/resources/chromeos/switch_access/compiled_resources2.gyp » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2017 The Chromium Authors. All rights reserved. 1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * Class to manage interactions with the accessibility tree, including moving 6 * Class to manage interactions with the accessibility tree, including moving
7 * to and selecting nodes. 7 * to and selecting nodes.
8 * 8 *
9 * @constructor 9 * @constructor
10 * @param {!chrome.automation.AutomationNode} desktop
10 */ 11 */
11 function AutomationManager() { 12 function AutomationManager(desktop) {
12 /** 13 /**
13 * Currently highlighted node. 14 * Currently highlighted node.
14 * 15 *
15 * @private {chrome.automation.AutomationNode} 16 * @private {!chrome.automation.AutomationNode}
16 */ 17 */
17 this.node_ = null; 18 this.node_ = desktop;
18 19
19 /** 20 /**
20 * The root of the subtree that the user is navigating through. 21 * The root of the subtree that the user is navigating through.
21 * 22 *
22 * @private {chrome.automation.AutomationNode} 23 * @private {!chrome.automation.AutomationNode}
23 */ 24 */
24 this.scope_ = null; 25 this.scope_ = desktop;
25 26
26 /** 27 /**
27 * The desktop node. 28 * The desktop node.
28 * 29 *
29 * @private {chrome.automation.AutomationNode} 30 * @private {!chrome.automation.AutomationNode}
30 */ 31 */
31 this.desktop_ = null; 32 this.desktop_ = desktop;
32 33
33 /** 34 /**
34 * A stack of past scopes. Allows user to traverse back to previous groups 35 * A stack of past scopes. Allows user to traverse back to previous groups
35 * after selecting one or more groups. The most recent group is at the end 36 * after selecting one or more groups. The most recent group is at the end
36 * of the array. 37 * of the array.
37 * 38 *
38 * @private {Array<chrome.automation.AutomationNode>} 39 * @private {Array<!chrome.automation.AutomationNode>}
39 */ 40 */
40 this.scopeStack_ = []; 41 this.scopeStack_ = [];
41 42
42 /** 43 /**
43 * Moves to the appropriate node in the accessibility tree. 44 * Moves to the appropriate node in the accessibility tree.
44 * 45 *
45 * @private {AutomationTreeWalker} 46 * @private {!AutomationTreeWalker}
46 */ 47 */
47 this.treeWalker_ = null; 48 this.treeWalker_ = this.createTreeWalker_(desktop);
48 49
49 this.init_(); 50 this.init_();
50 }; 51 };
51 52
52 /** 53 /**
53 * Highlight colors for the focus ring to distinguish between different types 54 * Highlight colors for the focus ring to distinguish between different types
54 * of nodes. 55 * of nodes.
55 * 56 *
56 * @const 57 * @const
57 */ 58 */
58 AutomationManager.Color = { 59 AutomationManager.Color = {
59 SCOPE: '#de742f', // dark orange 60 SCOPE: '#de742f', // dark orange
60 GROUP: '#ffbb33', // light orange 61 GROUP: '#ffbb33', // light orange
61 LEAF: '#78e428' //light green 62 LEAF: '#78e428' //light green
62 }; 63 };
63 64
64 AutomationManager.prototype = { 65 AutomationManager.prototype = {
65 /** 66 /**
66 * Set this.node_, this.root_, and this.desktop_ to the desktop node, and 67 * Set this.node_, this.root_, and this.desktop_ to the desktop node, and
67 * creates an initial tree walker. 68 * creates an initial tree walker.
68 * 69 *
69 * @private 70 * @private
70 */ 71 */
71 init_: function() { 72 init_: function() {
72 chrome.automation.getDesktop(function(desktop) { 73 console.log('AutomationNode for desktop is loaded');
73 this.node_ = desktop; 74 this.printNode_(this.node_);
74 this.scope_ = desktop; 75
75 this.desktop_ = desktop; 76 this.desktop_.addEventListener(
76 this.treeWalker_ = this.createTreeWalker_(desktop); 77 chrome.automation.EventType.FOCUS,
77 console.log('AutomationNode for desktop is loaded'); 78 this.handleFocusChange_.bind(this),
78 this.printNode_(this.node_); 79 false);
79 }.bind(this)); 80
81 // TODO(elichtenberg): Eventually use a more specific filter than
82 // ALL_TREE_CHANGES.
83 chrome.automation.addTreeChangeObserver(
84 chrome.automation.TreeChangeObserverFilter.ALL_TREE_CHANGES,
85 this.handleNodeRemoved_.bind(this));
80 }, 86 },
81 87
82 /** 88 /**
89 * When an interesting element gains focus on the page, move to it. If an
90 * element gains focus but is not interesting, move to the next interesting
91 * node after it.
92 *
93 * @param {!chrome.automation.AutomationEvent} event
94 * @private
95 */
96 handleFocusChange_: function(event) {
97 if (this.node_ === event.target)
98 return;
99 console.log('Focus changed');
100
101 // Rebuild scope stack and set scope for focused node.
102 this.buildScopeStack_(event.target);
103
104 // Move to focused node.
105 this.node_ = event.target;
106 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_);
107
108 // In case the node that gained focus is not a subtreeLeaf.
109 if (AutomationPredicate.isSubtreeLeaf(this.node_, this.scope_)) {
110 this.printNode_(this.node_);
111 this.updateFocusRing_();
112 } else
113 this.moveToNode(true);
114 },
115
116 /**
117 * Create a new scope stack and set the current scope for |node|.
118 *
119 * @param {!chrome.automation.AutomationNode} node
120 * @private
121 */
122 buildScopeStack_: function(node) {
123 // Create list of |node|'s ancestors, with highest level ancestor at the
124 // end.
125 let ancestorList = [];
126 while (node.parent) {
127 ancestorList.push(node.parent);
128 node = node.parent;
129 }
130
131 // Starting with desktop as the scope, if an ancestor is a group, set it to
132 // the new scope and push the old scope onto the scope stack.
133 this.scopeStack_ = [];
134 this.scope_ = this.desktop_;
135 while (ancestorList.length > 0) {
136 let ancestor = ancestorList.pop();
137 if (ancestor.role === chrome.automation.RoleType.DESKTOP)
138 continue;
139 if (AutomationPredicate.isGroup(ancestor, this.scope_)) {
140 this.scopeStack_.push(this.scope_);
141 this.scope_ = ancestor;
142 }
143 }
144 },
145
146 /**
147 * When a node is removed from the page, move to a new valid node.
148 *
149 * @param {!chrome.automation.TreeChange} treeChange
150 * @private
151 */
152 handleNodeRemoved_: function(treeChange) {
153 // TODO(elichtenberg): Only listen to NODE_REMOVED callbacks. Don't need
154 // any others.
155 if (treeChange.type !== chrome.automation.TreeChangeType.NODE_REMOVED)
156 return;
157
158 // TODO(elichtenberg): Currently not getting NODE_REMOVED event when whole
159 // tree is deleted. Once fixed, can delete this. Should only need to check
160 // if target is current node.
161 let removedByRWA =
162 treeChange.target.role === chrome.automation.RoleType.ROOT_WEB_AREA
163 && !this.node_.role;
164
165 if (!removedByRWA && treeChange.target !== this.node_)
166 return;
167
168 console.log('Node removed');
169 chrome.accessibilityPrivate.setFocusRing([]);
170
171 // Current node not invalid until after treeChange callback, so move to
172 // valid node after callback. Delay added to prevent moving to another
173 // node about to be made invalid. If already at a valid node (e.g., user
174 // moves to it or focus changes to it), won't need to move to a new node.
175 window.setTimeout(function() {
176 if (!this.node_.role)
177 this.moveToNode(true);
178 }.bind(this), 100);
179 },
180
181 /**
83 * Set this.node_ to the next/previous interesting node, and then highlight 182 * Set this.node_ to the next/previous interesting node, and then highlight
84 * it on the screen. If no interesting node is found, set this.node_ to the 183 * it on the screen. If no interesting node is found, set this.node_ to the
85 * first/last interesting node. If |doNext| is true, will search for next 184 * first/last interesting node. If |doNext| is true, will search for next
86 * node. Otherwise, will search for previous node. 185 * node. Otherwise, will search for previous node.
87 * 186 *
88 * @param {boolean} doNext 187 * @param {boolean} doNext
89 */ 188 */
90 moveToNode: function(doNext) { 189 moveToNode: function(doNext) {
91 if (!this.treeWalker_) 190 // If node is invalid, set node to last valid scope.
92 return; 191 this.startAtValidNode_();
93 192
94 let node = this.treeWalker_.moveToNode(doNext); 193 let node = this.treeWalker_.moveToNode(doNext);
95 if (node) { 194 if (node) {
96 this.node_ = node; 195 this.node_ = node;
97 this.printNode_(this.node_); 196 this.printNode_(this.node_);
98 197 this.updateFocusRing_();
99 let color;
100 if (this.node_ === this.scope_)
101 color = AutomationManager.Color.SCOPE;
102 else if (AutomationPredicate.isInteresting(this.node_))
103 color = AutomationManager.Color.LEAF;
104 else
105 color = AutomationManager.Color.GROUP;
106 chrome.accessibilityPrivate.setFocusRing([this.node_.location], color);
107 } 198 }
108 }, 199 },
109 200
110 /** 201 /**
111 * Select the currently highlighted node. If the node is the current scope, 202 * Select the currently highlighted node. If the node is the current scope,
112 * go back to the previous scope (i.e., create a new tree walker rooted at 203 * go back to the previous scope (i.e., create a new tree walker rooted at
113 * the previous scope). If the node is a group other than the current scope, 204 * the previous scope). If the node is a group other than the current scope,
114 * create a new tree walker for the new subtree the user is scanning through. 205 * create a new tree walker for the new subtree the user is scanning through.
115 * Otherwise, meaning the node is interesting, perform the default action on 206 * Otherwise, meaning the node is interesting, perform the default action on
116 * it. 207 * it.
117 */ 208 */
118 selectCurrentNode: function() { 209 selectCurrentNode: function() {
119 if (!this.node_ || !this.scope_ || !this.treeWalker_) 210 if (!this.node_.role)
120 return; 211 return;
121 212
122 if (this.node_ === this.scope_) { 213 if (this.node_ === this.scope_) {
123 // Don't let user select the top-level root node (i.e., the desktop node). 214 // Don't let user select the top-level root node (i.e., the desktop node).
124 if (this.scopeStack_.length === 0) 215 if (this.scopeStack_.length === 0)
125 return; 216 return;
126 217
127 // Find a previous scope that is still valid. 218 // Find a previous scope that is still valid. The stack here always has
128 let oldScope; 219 // at least one valid scope (i.e., the desktop node).
129 do { 220 do {
130 oldScope = this.scopeStack_.pop(); 221 this.scope_ = this.scopeStack_.pop();
131 } while (oldScope && !oldScope.role); 222 } while (!this.scope_.role && this.scopeStack_.length > 0);
132 223
133 // oldScope will always be valid here, so this will always be true. 224 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_);
134 if (oldScope) { 225 this.updateFocusRing_();
135 this.scope_ = oldScope; 226 console.log('Moved to previous scope');
136 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); 227 this.printNode_(this.node_);
137 chrome.accessibilityPrivate.setFocusRing(
138 [this.node_.location], AutomationManager.Color.GROUP);
139 }
140 return; 228 return;
141 } 229 }
142 230
143 if (AutomationPredicate.isGroup(this.node_, this.scope_)) { 231 if (AutomationPredicate.isGroup(this.node_, this.scope_)) {
144 this.scopeStack_.push(this.scope_); 232 this.scopeStack_.push(this.scope_);
145 this.scope_ = this.node_; 233 this.scope_ = this.node_;
146 this.treeWalker_ = this.createTreeWalker_(this.scope_); 234 this.treeWalker_ = this.createTreeWalker_(this.scope_);
235 console.log('Entered scope');
147 this.moveToNode(true); 236 this.moveToNode(true);
148 return; 237 return;
149 } 238 }
150 239
151 this.node_.doDefault(); 240 this.node_.doDefault();
241 console.log('Performed default action');
242 console.log('\n');
152 }, 243 },
153 244
154 /** 245 /**
246 * Set the focus ring for the current node and determine the color for it.
247 *
248 * @private
249 */
250 updateFocusRing_: function() {
251 let color;
252 if (this.node_ === this.scope_)
253 color = AutomationManager.Color.SCOPE;
254 else if (AutomationPredicate.isGroup(this.node_, this.scope_))
255 color = AutomationManager.Color.GROUP;
256 else
257 color = AutomationManager.Color.LEAF;
258 chrome.accessibilityPrivate.setFocusRing([this.node_.location], color);
259 },
260
261 /**
262 * If this.node_ is invalid, set this.node_ to a valid scope. Will check the
263 * current scope and past scopes until a valid scope is found. this.node_
264 * is set to that valid scope.
265 *
266 * @private
267 */
268 startAtValidNode_: function() {
269 if (this.node_.role)
270 return;
271 console.log('Finding new valid node');
272
273 // Current node is invalid, but current scope is still valid, so set node
274 // to the current scope.
275 if (this.scope_.role)
276 this.node_ = this.scope_;
277
278 // Current node and current scope are invalid, so set both to a valid scope
279 // from the scope stack. The stack here always has at least one valid scope
280 // (i.e., the desktop node).
281 while (!this.node_.role && this.scopeStack_.length > 0) {
282 this.node_ = this.scopeStack_.pop();
283 this.scope_ = this.node_;
284 }
285 this.treeWalker_ = this.createTreeWalker_(this.scope_);
286 },
287
288 /**
155 * Create an AutomationTreeWalker for the subtree with |scope| as its root. 289 * Create an AutomationTreeWalker for the subtree with |scope| as its root.
156 * If |opt_start| is defined, the tree walker will start walking the tree 290 * If |opt_start| is defined, the tree walker will start walking the tree
157 * from |opt_start|; otherwise, it will start from |scope|. 291 * from |opt_start|; otherwise, it will start from |scope|.
158 * 292 *
159 * @param {!chrome.automation.AutomationNode} scope 293 * @param {!chrome.automation.AutomationNode} scope
160 * @param {!chrome.automation.AutomationNode=} opt_start 294 * @param {!chrome.automation.AutomationNode=} opt_start
295 * @private
161 * @return {!AutomationTreeWalker} 296 * @return {!AutomationTreeWalker}
162 */ 297 */
163 createTreeWalker_: function(scope, opt_start) { 298 createTreeWalker_: function(scope, opt_start) {
164 // If no explicit start node, start walking the tree from |scope|. 299 // If no explicit start node, start walking the tree from |scope|.
165 let start = opt_start || scope; 300 let start = opt_start || scope;
166 301
167 let leafPred = function(node) { 302 let leafPred = function(node) {
168 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope)) 303 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope))
169 || !AutomationPredicate.isInterestingSubtree(node); 304 || !AutomationPredicate.isInterestingSubtree(node);
170 }; 305 };
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
251 */ 386 */
252 debugMoveToParent: function() { 387 debugMoveToParent: function() {
253 let parent = this.treeWalker_.debugMoveToParent(this.node_); 388 let parent = this.treeWalker_.debugMoveToParent(this.node_);
254 if (parent) { 389 if (parent) {
255 this.node_ = parent; 390 this.node_ = parent;
256 this.printNode_(this.node_); 391 this.printNode_(this.node_);
257 chrome.accessibilityPrivate.setFocusRing([this.node_.location]); 392 chrome.accessibilityPrivate.setFocusRing([this.node_.location]);
258 } 393 }
259 } 394 }
260 }; 395 };
OLDNEW
« no previous file with comments | « no previous file | chrome/browser/resources/chromeos/switch_access/compiled_resources2.gyp » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698