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

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: Apply feedback Created 5 years, 10 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 | « ui/webui/resources/js/cr/ui/focus_grid.js ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 * var focusRow = new cr.ui.FocusRow(rowBoundary, rowEl);
15 * 15 *
16 * if there are references to each node or querying them from the DOM like so: 16 * focusRow.addFocusableElement(checkboxEl);
17 * focusRow.addFocusableElement(labelEl);
18 * focusRow.addFocusableElement(buttonEl);
17 * 19 *
18 * new cr.ui.FocusRow(dialog.querySelectorAll('list input[type=checkbox]')) 20 * focusRow.setInitialFocusability(true);
19 * 21 *
20 * Pressing left cycles backward and pressing right cycles forward in item 22 * 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 23 * order. Pressing Home goes to the beginning of the list and End goes to the
22 * end of the list. 24 * end of the list.
23 * 25 *
24 * If an item in this row is focused, it'll stay active (accessible via tab). 26 * 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 27 * 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 28 * changes to a node inside |this.boundary_|. If |boundary| isn't specified,
27 * specified, any focus change deactivates the row. 29 * any focus change deactivates the row.
28 * 30 *
29 * @param {!Array.<!Element>|!NodeList} items Elements to track focus of.
30 * @param {Node=} opt_boundary Focus events are ignored outside of this node.
31 * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events.
32 * @param {FocusRow.Observer=} opt_observer An observer that's notified if
33 * this focus row is added to or removed from the focus order.
34 * @constructor 31 * @constructor
35 */ 32 */
36 function FocusRow(items, opt_boundary, opt_delegate, opt_observer) { 33 function FocusRow() {}
37 /** @type {!Array.<!Element>} */
38 this.items = Array.prototype.slice.call(items);
39 assert(this.items.length > 0);
40
41 /** @type {!Node} */
42 this.boundary_ = opt_boundary || document;
43
44 /** @private {cr.ui.FocusRow.Delegate|undefined} */
45 this.delegate_ = opt_delegate;
46
47 /** @private {cr.ui.FocusRow.Observer|undefined} */
48 this.observer_ = opt_observer;
49
50 /** @private {!EventTracker} */
51 this.eventTracker_ = new EventTracker;
52 this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this));
53 this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this));
54
55 this.items.forEach(function(item) {
56 if (item != document.activeElement)
57 item.tabIndex = -1;
58
59 this.eventTracker_.add(item, 'mousedown', this.onMousedown_.bind(this));
60 }, this);
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 }
69 34
70 /** @interface */ 35 /** @interface */
71 FocusRow.Delegate = function() {}; 36 FocusRow.Delegate = function() {};
72 37
73 FocusRow.Delegate.prototype = { 38 FocusRow.Delegate.prototype = {
74 /** 39 /**
75 * Called when a key is pressed while an item in |this.items| is focused. If 40 * Called when a key is pressed while an item in |this.focusableElements| is
76 * |e|'s default is prevented, further processing is skipped. 41 * focused. If |e|'s default is prevented, further processing is skipped.
77 * @param {cr.ui.FocusRow} row The row that detected a keydown. 42 * @param {cr.ui.FocusRow} row The row that detected a keydown.
78 * @param {Event} e 43 * @param {Event} e
79 * @return {boolean} Whether the event was handled. 44 * @return {boolean} Whether the event was handled.
80 */ 45 */
81 onKeydown: assertNotReached, 46 onKeydown: assertNotReached,
82 47
83 /** 48 /**
84 * @param {cr.ui.FocusRow} row The row that detected the mouse going down. 49 * @param {cr.ui.FocusRow} row The row that detected the mouse going down.
85 * @param {Event} e 50 * @param {Event} e
86 * @return {boolean} Whether the event was handled. 51 * @return {boolean} Whether the event was handled.
87 */ 52 */
88 onMousedown: assertNotReached, 53 onMousedown: assertNotReached,
89 }; 54 };
90 55
91 /** @interface */ 56 FocusRow.prototype = {
92 FocusRow.Observer = function() {}; 57 __proto__: HTMLDivElement.prototype,
93
94 FocusRow.Observer.prototype = {
95 /**
96 * Called when the row is activated (added to the focus order).
97 * @param {cr.ui.FocusRow} row The row added to the focus order.
98 */
99 onActivate: assertNotReached,
100 58
101 /** 59 /**
102 * Called when the row is deactivated (removed from the focus order). 60 * Should be called in the constructor to decorate |this|.
103 * @param {cr.ui.FocusRow} row The row removed from the focus order. 61 * @param {Node} boundary Focus events are ignored outside of this node.
62 * @param {FocusRow.Delegate=} opt_delegate A delegate to handle key events.
104 */ 63 */
105 onDeactivate: assertNotReached, 64 decorate: function(boundary, opt_delegate) {
106 }; 65 /** @private {!Node} */
66 this.boundary_ = boundary || document;
107 67
108 FocusRow.prototype = { 68 /** @type {cr.ui.FocusRow.Delegate|undefined} */
109 get activeIndex() { 69 this.delegate = opt_delegate;
110 return this.activeIndex_;
111 },
112 set activeIndex(index) {
113 var wasActive = this.items[this.activeIndex_];
114 if (wasActive)
115 wasActive.tabIndex = -1;
116 70
117 this.items.forEach(function(item) { assert(item.tabIndex == -1); }); 71 /** @type {Array<Element>} */
118 this.activeIndex_ = index; 72 this.focusableElements = [];
119 73
120 if (this.items[index]) 74 /** @private {!EventTracker} */
121 this.items[index].tabIndex = 0; 75 this.eventTracker_ = new EventTracker;
122 76 this.eventTracker_.add(cr.doc, 'focusin', this.onFocusin_.bind(this));
123 if (!this.observer_) 77 this.eventTracker_.add(cr.doc, 'keydown', this.onKeydown_.bind(this));
124 return;
125
126 var isActive = index >= 0 && index < this.items.length;
127 if (isActive == !!wasActive)
128 return;
129
130 if (isActive)
131 this.observer_.onActivate(this);
132 else
133 this.observer_.onDeactivate(this);
134 }, 78 },
135 79
136 /** 80 /**
137 * Focuses the item at |index|. 81 * Called when the row's active state changes and it is added/removed from
138 * @param {number} index An index to focus. Must be between 0 and 82 * the focus order.
139 * this.items.length - 1. 83 * @param {boolean} state Whether the row has become active or inactive.
140 */ 84 */
141 focusIndex: function(index) { 85 onActiveStateChanged: function(state) {},
142 this.items[index].focus(); 86
87 /**
88 * Find the element that best matches |sampleElement|.
89 * @param {Element} sampleElement An element from a row of the same type
90 * which previously held focus.
91 * @return {!Element} The element that best matches sampleElement.
92 */
93 getEquivalentElement: assertNotReached,
94
95 /**
96 * Add an element to this FocusRow. No-op if |element| is not provided.
97 * @param {Element} element The element that should be added.
98 */
99 addFocusableElement: function(element) {
100 if (!element)
101 return;
102
103 assert(this.focusableElements.indexOf(element) == -1);
104 assert(this.contains(element));
105
106 this.focusableElements.push(element);
107 this.eventTracker_.add(element, 'mousedown',
108 this.onMousedown_.bind(this));
109 },
110
111 /**
112 * Called when focus changes to activate/deactivate the row. Focus is
113 * removed from the row when |element| is not in the FocusRow.
114 * @param {Element} element The element that has focus. null if focus should
115 * be removed.
116 * @private
117 */
118 onFocusChange_: function(element) {
119 var isActive = this.contains(element);
120 var wasActive = this.classList.contains('focus-row-active');
121
122 // Only send events if the active state is different for the row.
123 if (isActive != wasActive)
124 this.makeRowActive(isActive);
125 },
126
127 /**
128 * Enables/disables the tabIndex of the focusable elements in the FocusRow.
129 * tabIndex can be set properly.
130 * @param {boolean} active True if tab is allowed for this row.
131 */
132 makeRowActive: function(active) {
133 this.focusableElements.forEach(function(element) {
134 element.tabIndex = active ? 0 : -1;
135 });
136
137 this.classList.toggle('focus-row-active', active);
138 this.onActiveStateChanged(active);
143 }, 139 },
144 140
145 /** Call this to clean up event handling before dereferencing. */ 141 /** Call this to clean up event handling before dereferencing. */
146 destroy: function() { 142 destroy: function() {
147 this.eventTracker_.removeAll(); 143 this.eventTracker_.removeAll();
148 }, 144 },
149 145
150 /** 146 /**
151 * @param {Event} e The focusin event. 147 * @param {Event} e The focusin event.
152 * @private 148 * @private
153 */ 149 */
154 onFocusin_: function(e) { 150 onFocusin_: function(e) {
155 if (this.boundary_.contains(assertInstanceof(e.target, Node))) 151 if (this.boundary_.contains(assertInstanceof(e.target, Node)))
156 this.activeIndex = this.items.indexOf(e.target); 152 this.onFocusChange_(e.target);
157 }, 153 },
158 154
159 /** 155 /**
160 * @param {Event} e A focus event. 156 * Handles a keypress for an element in this FocusRow.
157 * @param {Event} e The keydown event.
161 * @private 158 * @private
162 */ 159 */
163 onKeydown_: function(e) { 160 onKeydown_: function(e) {
164 var item = this.items.indexOf(e.target); 161 if (!this.contains(e.target))
165 if (item < 0)
166 return; 162 return;
167 163
168 if (this.delegate_ && this.delegate_.onKeydown(this, e)) 164 if (this.delegate && this.delegate.onKeydown(this, e))
169 return; 165 return;
170 166
167 var elementIndex = this.focusableElements.indexOf(e.target);
171 var index = -1; 168 var index = -1;
172 169
173 if (e.keyIdentifier == 'Left') 170 if (e.keyIdentifier == 'Left')
174 index = item + (isRTL() ? 1 : -1); 171 index = elementIndex + (isRTL() ? 1 : -1);
175 else if (e.keyIdentifier == 'Right') 172 else if (e.keyIdentifier == 'Right')
176 index = item + (isRTL() ? -1 : 1); 173 index = elementIndex + (isRTL() ? -1 : 1);
177 else if (e.keyIdentifier == 'Home') 174 else if (e.keyIdentifier == 'Home')
178 index = 0; 175 index = 0;
179 else if (e.keyIdentifier == 'End') 176 else if (e.keyIdentifier == 'End')
180 index = this.items.length - 1; 177 index = this.focusableElements.length - 1;
181 178
182 if (!this.items[index]) 179 var elementToFocus = this.focusableElements[index];
183 return; 180 if (elementToFocus) {
184 181 this.getEquivalentElement(elementToFocus).focus();
185 this.focusIndex(index); 182 e.preventDefault();
186 e.preventDefault(); 183 }
187 }, 184 },
188 185
189 /** 186 /**
190 * @param {Event} e A click event. 187 * @param {Event} e A click event.
191 * @private 188 * @private
192 */ 189 */
193 onMousedown_: function(e) { 190 onMousedown_: function(e) {
194 if (this.delegate_ && this.delegate_.onMousedown(this, e)) 191 if (this.delegate && this.delegate.onMousedown(this, e))
195 return; 192 return;
196 193
197 if (!e.button) 194 // Only accept the left mouse click.
198 this.activeIndex = this.items.indexOf(e.currentTarget); 195 if (!e.button) {
196 // Focus this row if the target is one of the elements in this row.
197 this.onFocusChange_(e.target);
198 }
199 }, 199 },
200 }; 200 };
201 201
202 return { 202 return {
203 FocusRow: FocusRow, 203 FocusRow: FocusRow,
204 }; 204 };
205 }); 205 });
OLDNEW
« no previous file with comments | « 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