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

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: Correctly follows focus and moves to valid node when current becomes invalid 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 // Create list of ancestors of node that encountered focus change.
102 let node = event.target;
103 let ancestorList = [];
104 while (node.parent) {
105 ancestorList.push(node.parent);
106 node = node.parent;
107 }
108
109 // Rebuild scope stack.
dmazzoni 2017/05/18 16:25:52 Might be simpler to just do this in one loop rathe
elichtenberg 2017/05/18 18:31:01 Need separate loops because need the node's scope
110 this.scopeStack_ = [];
111 this.scope_ = this.desktop_;
112 while (ancestorList.length > 0) {
113 let newScope = ancestorList.pop();
114 if (newScope.role === chrome.automation.RoleType.DESKTOP)
115 continue;
116 if (AutomationPredicate.isGroup(newScope, this.scope_)) {
117 this.scopeStack_.push(this.scope_);
118 this.scope_ = newScope;
119 }
120 }
121
122 // Move to focused node.
123 this.node_ = event.target;
124 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_);
125
126 // In case the node that gained focus is not a subtreeLeaf.
127 if (AutomationPredicate.isSubtreeLeaf(this.node_, this.scope_)) {
128 this.printNode_(this.node_);
129 this.setFocusRing_();
130 } else
131 this.moveToNode(true);
132 },
133
134 /**
135 * When a node is removed from the page, move to a new valid node.
136 *
137 * @param {!chrome.automation.TreeChange} treeChange
138 * @private
139 */
140 handleNodeRemoved_: function(treeChange) {
141 if (treeChange.type === chrome.automation.TreeChangeType.NODE_REMOVED
dmazzoni 2017/05/18 16:25:52 Please split this up into a few separate checks, a
elichtenberg 2017/05/18 18:31:01 Split it up and added some TODOs. Do you think thi
142 && ((treeChange.target.role === 'rootWebArea' && !this.node_.role)
dmazzoni 2017/05/18 16:25:52 Use RoleType.ROOT_WEB_AREA here and elsewhere
elichtenberg 2017/05/18 18:31:01 Done.
143 || treeChange.target === this.node_)) {
144 console.log('Node removed');
145 chrome.accessibilityPrivate.setFocusRing([]);
146
147 // Current node not invalid until after treeChange callback, so move to
148 // valid node after callback. Delay added to prevent moving to another
149 // node about to be made invalid. If already at a valid node (e.g., user
150 // moves to it or focus changes to it), won't need to move to a new node.
151 window.setTimeout(function() {
152 if (!this.node_.role)
153 this.moveToNode(true);
154 }.bind(this), 100);
155 }
156 },
157
158 /**
83 * Set this.node_ to the next/previous interesting node, and then highlight 159 * 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 160 * 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 161 * first/last interesting node. If |doNext| is true, will search for next
86 * node. Otherwise, will search for previous node. 162 * node. Otherwise, will search for previous node.
87 * 163 *
88 * @param {boolean} doNext 164 * @param {boolean} doNext
89 */ 165 */
90 moveToNode: function(doNext) { 166 moveToNode: function(doNext) {
91 if (!this.treeWalker_) 167 // If node is invalid, set node to last valid scope.
92 return; 168 this.startAtValidNode_();
93 169
94 let node = this.treeWalker_.moveToNode(doNext); 170 let node = this.treeWalker_.moveToNode(doNext);
95 if (node) { 171 if (node) {
96 this.node_ = node; 172 this.node_ = node;
97 this.printNode_(this.node_); 173 this.printNode_(this.node_);
98 174 this.setFocusRing_();
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 } 175 }
108 }, 176 },
109 177
110 /** 178 /**
111 * Select the currently highlighted node. If the node is the current scope, 179 * 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 180 * 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, 181 * 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. 182 * 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 183 * Otherwise, meaning the node is interesting, perform the default action on
116 * it. 184 * it.
117 */ 185 */
118 selectCurrentNode: function() { 186 selectCurrentNode: function() {
119 if (!this.node_ || !this.scope_ || !this.treeWalker_) 187 if (!this.node_.role)
120 return; 188 return;
121 189
122 if (this.node_ === this.scope_) { 190 if (this.node_ === this.scope_) {
123 // Don't let user select the top-level root node (i.e., the desktop node). 191 // Don't let user select the top-level root node (i.e., the desktop node).
124 if (this.scopeStack_.length === 0) 192 if (this.scopeStack_.length === 0)
125 return; 193 return;
126 194
127 // Find a previous scope that is still valid. 195 // Find a previous scope that is still valid. The stack here always has
128 let oldScope; 196 // at least one valid scope (i.e., the desktop node).
129 do { 197 do {
130 oldScope = this.scopeStack_.pop(); 198 this.scope_ = this.scopeStack_.pop();
131 } while (oldScope && !oldScope.role); 199 } while (!this.scope_.role && this.scopeStack_.length > 0);
132 200
133 // oldScope will always be valid here, so this will always be true. 201 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_);
134 if (oldScope) { 202 chrome.accessibilityPrivate.setFocusRing(
dmazzoni 2017/05/18 16:25:52 Can you call this.setFocusRing_() here?
elichtenberg 2017/05/18 18:31:01 Yep. Just changed it.
135 this.scope_ = oldScope; 203 [this.node_.location], AutomationManager.Color.GROUP);
136 this.treeWalker_ = this.createTreeWalker_(this.scope_, this.node_); 204 console.log('Moved to previous scope');
137 chrome.accessibilityPrivate.setFocusRing( 205 this.printNode_(this.node_);
138 [this.node_.location], AutomationManager.Color.GROUP);
139 }
140 return; 206 return;
141 } 207 }
142 208
143 if (AutomationPredicate.isGroup(this.node_, this.scope_)) { 209 if (AutomationPredicate.isGroup(this.node_, this.scope_)) {
144 this.scopeStack_.push(this.scope_); 210 this.scopeStack_.push(this.scope_);
145 this.scope_ = this.node_; 211 this.scope_ = this.node_;
146 this.treeWalker_ = this.createTreeWalker_(this.scope_); 212 this.treeWalker_ = this.createTreeWalker_(this.scope_);
213 console.log('Entered scope');
147 this.moveToNode(true); 214 this.moveToNode(true);
148 return; 215 return;
149 } 216 }
150 217
151 this.node_.doDefault(); 218 this.node_.doDefault();
219 console.log('Performed default action');
220 console.log('\n');
152 }, 221 },
153 222
154 /** 223 /**
224 * Set the focus ring for the current node and determine the color for it.
225 *
226 * @private
227 */
228 setFocusRing_: function() {
dmazzoni 2017/05/18 16:25:52 How about updateFocusRing(), since it sets the foc
elichtenberg 2017/05/18 18:31:01 Done.
229 let color;
230 if (this.node_ === this.scope_)
231 color = AutomationManager.Color.SCOPE;
232 else if (AutomationPredicate.isGroup(this.node_, this.scope_))
233 color = AutomationManager.Color.GROUP;
234 else
235 color = AutomationManager.Color.LEAF;
236 chrome.accessibilityPrivate.setFocusRing([this.node_.location], color);
237 },
238
239 /**
240 * If this.node_ is invalid, set this.node_ to a valid scope. Will check the
241 * current scope and past scopes until a valid scope is found. this.node_
242 * is set to that valid scope.
243 *
244 * @private
245 */
246 startAtValidNode_: function() {
247 if (this.node_.role)
248 return;
249 console.log('Finding new valid node');
250
251 // Current node is invalid, but current scope is still valid, so set node
252 // to the current scope.
253 if (this.scope_.role)
254 this.node_ = this.scope_;
255
256 // Current node and current scope are invalid, so set both to a valid scope
257 // from the scope stack. The stack here always has at least one valid scope
258 // (i.e., the desktop node).
259 while (!this.node_.role && this.scopeStack_.length > 0) {
260 this.node_ = this.scopeStack_.pop();
261 this.scope_ = this.node_;
262 }
263 this.treeWalker_ = this.createTreeWalker_(this.scope_);
264 },
265
266 /**
155 * Create an AutomationTreeWalker for the subtree with |scope| as its root. 267 * 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 268 * If |opt_start| is defined, the tree walker will start walking the tree
157 * from |opt_start|; otherwise, it will start from |scope|. 269 * from |opt_start|; otherwise, it will start from |scope|.
158 * 270 *
159 * @param {!chrome.automation.AutomationNode} scope 271 * @param {!chrome.automation.AutomationNode} scope
160 * @param {!chrome.automation.AutomationNode=} opt_start 272 * @param {!chrome.automation.AutomationNode=} opt_start
273 * @private
161 * @return {!AutomationTreeWalker} 274 * @return {!AutomationTreeWalker}
162 */ 275 */
163 createTreeWalker_: function(scope, opt_start) { 276 createTreeWalker_: function(scope, opt_start) {
164 // If no explicit start node, start walking the tree from |scope|. 277 // If no explicit start node, start walking the tree from |scope|.
165 let start = opt_start || scope; 278 let start = opt_start || scope;
166 279
167 let leafPred = function(node) { 280 let leafPred = function(node) {
168 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope)) 281 return (node !== scope && AutomationPredicate.isSubtreeLeaf(node, scope))
169 || !AutomationPredicate.isInterestingSubtree(node); 282 || !AutomationPredicate.isInterestingSubtree(node);
170 }; 283 };
(...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after
251 */ 364 */
252 debugMoveToParent: function() { 365 debugMoveToParent: function() {
253 let parent = this.treeWalker_.debugMoveToParent(this.node_); 366 let parent = this.treeWalker_.debugMoveToParent(this.node_);
254 if (parent) { 367 if (parent) {
255 this.node_ = parent; 368 this.node_ = parent;
256 this.printNode_(this.node_); 369 this.printNode_(this.node_);
257 chrome.accessibilityPrivate.setFocusRing([this.node_.location]); 370 chrome.accessibilityPrivate.setFocusRing([this.node_.location]);
258 } 371 }
259 } 372 }
260 }; 373 };
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