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

Side by Side Diff: ui/webui/resources/js/cr/ui/focus_row.js

Issue 807593005: Make downloads list keyboard shortcuts more consistent. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: fix tests Created 5 years, 11 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
OLDNEW
1 // Copyright 2014 The Chromium Authors. All rights reserved. 1 // Copyright 2014 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 cr.define('cr.ui', function() { 5 cr.define('cr.ui', function() {
6 /** 6 /**
7 * A class to manage focus between given horizontally arranged elements. 7 * A class to manage focus between given horizontally arranged elements.
8 * For example, given the page: 8 * For example, given the page:
9 * 9 *
10 * <input type="checkbox"> <label>Check me!</label> <button>X</button> 10 * <input type="checkbox"> <label>Check me!</label> <button>X</button>
11 * 11 *
12 * One could create a FocusRow by doing: 12 * One could create a FocusRow by doing:
13 * 13 *
14 * new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl]) 14 * new cr.ui.FocusRow([checkboxEl, labelEl, buttonEl])
15 * 15 *
16 * if there are references to each node or querying them from the DOM like so: 16 * if there are references to each node or querying them from the DOM like so:
17 * 17 *
18 * new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]')) 18 * new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]'))
19 * 19 *
20 * Pressing left cycles backward and pressing right cycles forward in item 20 * Pressing left cycles backward and pressing right cycles forward in item
21 * order. Pressing Home goes to the beginning of the list and End goes to the 21 * order. Pressing Home goes to the beginning of the list and End goes to the
22 * end of the list. 22 * end of the list.
23 * 23 *
24 * If an item in this row is focused, it'll stay active (accessible via tab). 24 * If an item in this row is focused, it'll stay active (accessible via tab).
25 * If no items in this row are focused, the row can stay active until focus 25 * If no items in this row are focused, the row can stay active until focus
26 * changes to a node inside |this.boundary_|. If opt_boundary isn't 26 * changes to a node inside |this.boundary_|. If opt_boundary isn't
27 * specified, any focus change deactivates the row. 27 * specified, any focus change deactivates the row.
28 * 28 *
29 * @param {!Array.<!Element>|!NodeList} items Elements to track focus of.
30 * @param {Node=} opt_boundary Focus events are ignored outside of this node. 29 * @param {Node=} opt_boundary Focus events are ignored outside of this node.
31 * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events. 30 * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events.
32 * @param {FocusRow.Observer=} opt_observer An observer that's notified if 31 * @param {FocusRow.Observer=} opt_observer An observer that's notified if
33 * this focus row is added to or removed from the focus order. 32 * this focus row is added to or removed from the focus order.
34 * @constructor 33 * @constructor
35 */ 34 */
36 function FocusRow(items, opt_boundary, opt_delegate, opt_observer) { 35 function FocusRow(opt_boundary, opt_delegate, opt_observer) {
37 /** @type {!Array.<!Element>} */
38 this.items = Array.prototype.slice.call(items);
39 assert(this.items.length > 0);
40
41 /** @type {!Node} */ 36 /** @type {!Node} */
42 this.boundary_ = opt_boundary || document; 37 this.boundary_ = opt_boundary || document;
43 38
44 /** @private {cr.ui.FocusRow.Delegate|undefined} */ 39 /** @private {cr.ui.FocusRow.Delegate|undefined} */
45 this.delegate_ = opt_delegate; 40 this.delegate_ = opt_delegate;
46 41
47 /** @private {cr.ui.FocusRow.Observer|undefined} */ 42 /** @private {cr.ui.FocusRow.Observer} */
43 assert(opt_observer);
48 this.observer_ = opt_observer; 44 this.observer_ = opt_observer;
49 45
50 /** @private {!EventTracker} */ 46 /** @private {!EventTracker} */
51 this.eventTracker_ = new EventTracker; 47 this.eventTracker_ = new EventTracker;
52 this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this)); 48 this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this));
53 this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this)); 49 this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this));
54 50
55 this.items.forEach(function(item) { 51 /** @type {Array<string>} */
56 if (item != document.activeElement) 52 this.elementIds = [];
57 item.tabIndex = -1;
58 53
59 this.eventTracker_.add(item, 'mousedown', this.onMousedown_.bind(this)); 54 /** @private {Element} */
60 }, this); 55 this.rowElement_ = null;
61
62 /**
63 * The index that should be actively participating in the page tab order.
64 * @type {number}
65 * @private
66 */
67 this.activeIndex_ = this.items.indexOf(document.activeElement);
68 } 56 }
69 57
70 /** @interface */ 58 /** @interface */
71 FocusRow.Delegate = function() {}; 59 FocusRow.Delegate = function() {};
72 60
73 FocusRow.Delegate.prototype = { 61 FocusRow.Delegate.prototype = {
74 /** 62 /**
75 * Called when a key is pressed while an item in |this.items| is focused. If 63 * Called when a key is pressed while an item in |this.getItems()| is
76 * |e|'s default is prevented, further processing is skipped. 64 * focused. If |e|'s default is prevented, further processing is skipped.
77 * @param {cr.ui.FocusRow} row The row that detected a keydown. 65 * @param {cr.ui.FocusRow} row The row that detected a keydown.
78 * @param {Event} e 66 * @param {Event} e
79 * @return {boolean} Whether the event was handled. 67 * @return {boolean} Whether the event was handled.
80 */ 68 */
81 onKeydown: assertNotReached, 69 onKeydown: assertNotReached,
82 70
83 /** 71 /**
84 * @param {cr.ui.FocusRow} row The row that detected the mouse going down. 72 * @param {cr.ui.FocusRow} row The row that detected the mouse going down.
85 * @param {Event} e 73 * @param {Event} e
86 * @return {boolean} Whether the event was handled. 74 * @return {boolean} Whether the event was handled.
87 */ 75 */
88 onMousedown: assertNotReached, 76 onMousedown: assertNotReached,
89 }; 77 };
90 78
91 /** @interface */ 79 /** @interface */
92 FocusRow.Observer = function() {}; 80 FocusRow.Observer = function() {};
93 81
94 FocusRow.Observer.prototype = { 82 FocusRow.Observer.prototype = {
95 /** 83 /**
96 * Called when the row is activated (added to the focus order). 84 * Called when the row is activated (added to the focus order).
97 * @param {cr.ui.FocusRow} row The row added to the focus order. 85 * @param {cr.ui.FocusRow} row The row added to the focus order.
98 */ 86 */
99 onActivate: assertNotReached, 87 onActivate: assertNotReached,
100 88
101 /** 89 /**
102 * Called when the row is deactivated (removed from the focus order). 90 * Called when the row is deactivated (removed from the focus order).
103 * @param {cr.ui.FocusRow} row The row removed from the focus order. 91 * @param {cr.ui.FocusRow} row The row removed from the focus order.
104 */ 92 */
105 onDeactivate: assertNotReached, 93 onDeactivate: assertNotReached,
94
95 /**
96 * Called when adding rowItems to the FocusRow to determine the element that
97 * represents the row. This should return the same element regardless of the
98 * rowItem it it called on for a specific row.
99 * @param {Element} rowItem The item to find a row element for.
100 * @return {Element} |rowItem|'s row element.
101 */
102 getRowElement: assertNotReached,
103
104 /**
105 * Called whenever there is a change in rowElement focus and the elementId
106 * is not in the FocusRow.
107 * @param {cr.ui.FocusRow} row The row that is being focused.
108 * @param {string} expectedId The id that was not found.
109 * @return {string} The id in |row| that should be focused.
110 */
111 onElementIdMiss: assertNotReached,
Evan Stade 2015/01/15 23:38:09 this name is super confusing to me. It should be s
hcarmona 2015/01/16 21:39:06 Done
106 }; 112 };
107 113
108 FocusRow.prototype = { 114 FocusRow.prototype = {
109 get activeIndex() { 115 /**
110 return this.activeIndex_; 116 * @param {Element} element The element whose id is needed.
111 }, 117 * @return {?string} |element|'s elementId. null if element is not in this
112 set activeIndex(index) { 118 * FocusRow.
113 var wasActive = this.items[this.activeIndex_]; 119 */
114 if (wasActive) 120 getElementId: function(element) {
115 wasActive.tabIndex = -1; 121 if (!this.rowElement_.contains(element))
116 122 return null;
117 this.items.forEach(function(item) { assert(item.tabIndex == -1); }); 123 return element.getAttribute("focus-row-element-id");
118 this.activeIndex_ = index; 124 },
119 125
120 if (this.items[index]) 126 /**
121 this.items[index].tabIndex = 0; 127 * @param {string} elementId
122 128 * @return {?Element} The element in this FocusRow with elementId. null if
123 if (!this.observer_) 129 * not in this FocusRow.
130 */
131 getElement: function (elementId) {
Evan Stade 2015/01/15 23:38:09 nit: extra space
hcarmona 2015/01/16 21:39:06 Done.
132 var element = this.rowElement_.querySelector('[focus-row-element-id="' +
133 elementId + '"]');
134
135 // Special case when the column is the row.
136 if (!element && elementId &&
137 this.rowElement_.getAttribute('focus-row-element-id') == elementId)
138 return this.rowElement_;
139
140 return element;
141 },
142
143 /**
144 * @return {Element} The row element that contains all focusable row items.
145 */
146 getRowElement: function() {
147 return this.rowElement_;
148 },
149
150 /**
151 * @return {[Element]} An array with all row items in this row. Empty array
152 * if nothing is focusable.
153 */
154 getItems: function() {
155 assert(this.rowElement_);
156 var items = this.rowElement_.querySelectorAll('[focus-row-element-id]');
157
158 // Special case when the column is the row.
159 if (items.length == 0 &&
160 this.rowElement_.hasAttribute('focus-row-element-id'))
161 return [ this.rowElement_ ];
162
163 return items;
164 },
165
166 /**
167 * Add an element to this FocusRow with the given elementId. No-op if either
168 * |element| or |elementId| is not provided.
169 * @param {Element} element The element that should be added.
170 * @param {string} elementId The elementId that should be used to find
171 * similar elements in the FocusRow. This MUST be unique for each row.
172 */
173 setFocusableElementId: function(element, elementId) {
174 if (!element || !elementId)
124 return; 175 return;
125 176
126 var isActive = index >= 0 && index < this.items.length; 177 assert(this.elementIds.indexOf(elementId) == -1);
127 if (isActive == !!wasActive) 178 element.setAttribute('focus-row-element-id', elementId);
179 this.elementIds.push(elementId);
180 this.eventTracker_.add(element, 'mousedown',
181 this.onMousedown_.bind(this));
182
183 if (!this.rowElement_)
184 this.rowElement_ = this.observer_.getRowElement(element);
185 else
186 assert(this.rowElement_ == this.observer_.getRowElement(element));
187 },
188
189 /**
190 * @param {bool} focused Whether the initial focus for this row is enabled
191 * or disabled.
192 */
193 setInitialFocus: function(focused) {
194 if (focused)
195 this.onFocusIdChange(this.elementIds[0]);
196 else
197 this.enableRowTab(false);
198 },
199
200 /**
201 * Called when focus changes to activate/deactivate the row. Focus is
202 * removed from the row when |elementId| is not in the FocusRow.
203 * @param {string} elementId The elementId that has focus.
204 */
205 onFocusIdChange: function(elementId) {
206 var element = this.getElement(elementId);
207 var rowClasses = this.rowElement_.classList;
208 var wasActive = rowClasses.contains('focus-row-active');
209
210 if (element) {
211 // Verify that the focus hasn't changed. This allows the FocusGrid to go
212 // back to the same focused element on a miss.
213 this.focusChanged_ = this.focusChanged_ ||
214 elementId != this.lastFocusedElementId_;
215 // Keep track of the last elementId that was focused.
216 this.lastFocusedElementId_ = elementId;
217
218 this.enableRowTab(true);
219 } else if (wasActive)
220 this.enableRowTab(false);
221
222 // Only send events if the active state is different for the row.
223 if (!!element == wasActive)
128 return; 224 return;
129 225
130 if (isActive) 226 if (element) {
227 rowClasses.add('focus-row-active');
131 this.observer_.onActivate(this); 228 this.observer_.onActivate(this);
132 else 229 } else {
230 rowClasses.remove('focus-row-active');
133 this.observer_.onDeactivate(this); 231 this.observer_.onDeactivate(this);
134 }, 232 }
135 233 },
136 /** 234
137 * Focuses the item at |index|. 235 /**
138 * @param {number} index An index to focus. Must be between 0 and 236 * Enables/disables the tabIndex of the focusable elements in the FocusRow.
139 * this.items.length - 1. 237 * tabIndex can be set properly.
140 */ 238 * @param {bool} allow True if tab is allowed for this row.
141 focusIndex: function(index) { 239 */
142 this.items[index].focus(); 240 enableRowTab: function(allow) {
dmazzoni 2015/01/15 19:38:19 I'd call this something like makeRowFocusable or a
hcarmona 2015/01/16 21:39:06 Done.
241 var items = this.getItems();
242 for (var i = 0; i < items.length; ++i)
243 items[i].tabIndex = allow ? 0 : -1;
244 },
245
246 /**
247 * Will choose an appropriate element to focus.
248 * @param {string} elementId The element id that should be focused.
249 * @return {Element} A focusable element that best matches |elementId|.
Evan Stade 2015/01/15 23:38:09 I am confused how you could "best match" an ID. ID
hcarmona 2015/01/16 21:39:06 Done.
250 */
251 getFocusableElement: function(elementId) {
252 if (!elementId)
253 return null;
254
255 /** Priority for focus is:
256 * 1. Focusable element with same elementId
257 * 2. Let the delegate decide what should be focused
258 * 3. Focus the first focusable element
259 */
260 return this.getElement(elementId) ||
261 this.getElement(this.observer_.onElementIdMiss(this, elementId)) ||
262 this.getElement(this.elementIds[0]);
dmazzoni 2015/01/15 19:38:19 Just checking, this works fine on an empty list /
hcarmona 2015/01/16 21:39:06 An empty grid would have no rows, so it should be
263 },
264
265 /**
266 * Called to set focus to a given row item based on the elementId. Will
267 * choose an appropriate element if |elementId| is not in the FocusRow.
268 * @param {string} elementId The element that should be focused.
269 */
270 setFocusId: function(elementId) {
271 var element = this.getFocusableElement(elementId);
272 if (element)
273 element.focus();
274 },
275
276 /** @private {string} */
277 lastFocusedElementId_: null,
278
279 /** @private {bool} */
280 focusChanged_: false,
281
282 /**
283 * Will reset the private focusChanged_ variable so that a change in focus
284 * can be tracked.
285 */
286 trackFocus: function() {
287 this.focusChanged_ = false;
288 },
289
290 /**
291 * @return {bool} Whether the column focus changed in this row since it was
292 * last asked to trackFocus().
293 */
294 focusChanged: function() {
295 return this.focusChanged_;
143 }, 296 },
144 297
145 /** Call this to clean up event handling before dereferencing. */ 298 /** Call this to clean up event handling before dereferencing. */
146 destroy: function() { 299 destroy: function() {
147 this.eventTracker_.removeAll(); 300 this.eventTracker_.removeAll();
148 }, 301 },
149 302
150 /** 303 /**
151 * @param {Event} e The focusin event. 304 * @param {Event} e The focusin event.
152 * @private 305 * @private
153 */ 306 */
154 onFocusin_: function(e) { 307 onFocusin_: function(e) {
155 if (this.boundary_.contains(assertInstanceof(e.target, Node))) 308 if (this.boundary_.contains(assertInstanceof(e.target, Node)))
156 this.activeIndex = this.items.indexOf(e.target); 309 this.onFocusIdChange(this.getElementId(e.target));
157 }, 310 },
158 311
159 /** 312 /**
160 * @param {Event} e A focus event. 313 * @param {Event} e A focus event.
161 * @private 314 * @private
162 */ 315 */
163 onKeydown_: function(e) { 316 onKeydown_: function(e) {
164 var item = this.items.indexOf(e.target); 317 if (!this.rowElement_.contains(e.target))
165 if (item < 0)
166 return; 318 return;
167 319
168 if (this.delegate_ && this.delegate_.onKeydown(this, e)) 320 if (this.delegate_ && this.delegate_.onKeydown(this, e))
169 return; 321 return;
170 322
323 var focusId = this.getElementId(e.target);
324 var elementIndex = this.elementIds.indexOf(focusId);
171 var index = -1; 325 var index = -1;
172 326
173 if (e.keyIdentifier == 'Left') 327 if (e.keyIdentifier == 'Left')
174 index = item + (isRTL() ? 1 : -1); 328 index = elementIndex + (isRTL() ? 1 : -1);
175 else if (e.keyIdentifier == 'Right') 329 else if (e.keyIdentifier == 'Right')
176 index = item + (isRTL() ? -1 : 1); 330 index = elementIndex + (isRTL() ? -1 : 1);
177 else if (e.keyIdentifier == 'Home') 331 else if (e.keyIdentifier == 'Home')
178 index = 0; 332 index = 0;
179 else if (e.keyIdentifier == 'End') 333 else if (e.keyIdentifier == 'End')
180 index = this.items.length - 1; 334 index = this.elementIds.length - 1;
181 335
182 if (!this.items[index]) 336 focusId = this.elementIds[index];
183 return; 337 if (focusId) {
184 338 this.setFocusId(focusId);
185 this.focusIndex(index); 339 e.preventDefault();
186 e.preventDefault(); 340 }
187 }, 341 },
188 342
189 /** 343 /**
190 * @param {Event} e A click event. 344 * @param {Event} e A click event.
191 * @private 345 * @private
192 */ 346 */
193 onMousedown_: function(e) { 347 onMousedown_: function(e) {
348 if (!this.rowElement_.contains(e.target))
349 return;
350
194 if (this.delegate_ && this.delegate_.onMousedown(this, e)) 351 if (this.delegate_ && this.delegate_.onMousedown(this, e))
195 return; 352 return;
196 353
197 if (!e.button) 354 // Only accept the left mouse click.
198 this.activeIndex = this.items.indexOf(e.currentTarget); 355 if (!e.button) {
356 // Focus this row if the target is one of the elements in this row.
357 this.onFocusIdChange(this.getElementId(e.target));
358 e.preventDefault();
359 }
199 }, 360 },
200 }; 361 };
201 362
202 return { 363 return {
203 FocusRow: FocusRow, 364 FocusRow: FocusRow,
204 }; 365 };
205 }); 366 });
OLDNEW
« chrome/browser/resources/history/history.js ('K') | « ui/webui/resources/js/cr/ui/focus_grid.js ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698