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

Side by Side Diff: ui/webui/resources/cr_elements/cr_action_menu/cr_action_menu.js

Issue 2940933003: DO NOT SUBMIT results of new clang-format (Closed)
Patch Set: Created 3 years, 6 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 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 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 * @typedef {{ 6 * @typedef {{
7 * top: number, 7 * top: number,
8 * left: number, 8 * left: number,
9 * width: (number| undefined), 9 * width: (number| undefined),
10 * height: (number| undefined), 10 * height: (number| undefined),
(...skipping 13 matching lines...) Expand all
24 */ 24 */
25 var AnchorAlignment = { 25 var AnchorAlignment = {
26 BEFORE_START: -2, 26 BEFORE_START: -2,
27 AFTER_START: -1, 27 AFTER_START: -1,
28 CENTER: 0, 28 CENTER: 0,
29 BEFORE_END: 1, 29 BEFORE_END: 1,
30 AFTER_END: 2, 30 AFTER_END: 2,
31 }; 31 };
32 32
33 (function() { 33 (function() {
34 /** 34 /**
35 * Returns the point to start along the X or Y axis given a start and end 35 * Returns the point to start along the X or Y axis given a start and end
36 * point to anchor to, the length of the target and the direction to anchor 36 * point to anchor to, the length of the target and the direction to anchor
37 * in. If honoring the anchor would force the menu outside of min/max, this 37 * in. If honoring the anchor would force the menu outside of min/max, this
38 * will ignore the anchor position and try to keep the menu within min/max. 38 * will ignore the anchor position and try to keep the menu within min/max.
39 * @private
40 * @param {number} start
41 * @param {number} end
42 * @param {number} length
43 * @param {AnchorAlignment} anchorAlignment
44 * @param {number} min
45 * @param {number} max
46 * @return {number}
47 */
48 function getStartPointWithAnchor(
49 start, end, length, anchorAlignment, min, max) {
50 var startPoint = 0;
51 switch (anchorAlignment) {
52 case AnchorAlignment.BEFORE_START:
53 startPoint = -length;
54 break;
55 case AnchorAlignment.AFTER_START:
56 startPoint = start;
57 break;
58 case AnchorAlignment.CENTER:
59 startPoint = (start + end - length) / 2;
60 break;
61 case AnchorAlignment.BEFORE_END:
62 startPoint = end - length;
63 break;
64 case AnchorAlignment.AFTER_END:
65 startPoint = end;
66 break;
67 }
68
69 if (startPoint + length > max)
70 startPoint = end - length;
71 if (startPoint < min)
72 startPoint = start;
73 return startPoint;
74 }
75
76
77 /**
78 * @private
79 * @return {!ShowConfig}
80 */
81 function getDefaultShowConfig() {
82 return {
83 top: 0,
84 left: 0,
85 height: 0,
86 width: 0,
87 anchorAlignmentX: AnchorAlignment.AFTER_START,
88 anchorAlignmentY: AnchorAlignment.AFTER_START,
89 minX: 0,
90 minY: 0,
91 maxX: window.innerWidth,
92 maxY: window.innerHeight,
93 };
94 }
95
96 Polymer({
97 is: 'cr-action-menu',
98 extends: 'dialog',
99
100 /**
101 * List of all options in this action menu.
102 * @private {?NodeList<!Element>}
103 */
104 options_: null,
105
106 /**
107 * The element which the action menu will be anchored to. Also the element
108 * where focus will be returned after the menu is closed. Only populated if
109 * menu is opened with showAt().
110 * @private {?Element}
111 */
112 anchorElement_: null,
113
114 /**
115 * Bound reference to an event listener function such that it can be removed
116 * on detach.
117 * @private {?Function}
118 */
119 boundClose_: null,
120
121 /** @private {boolean} */
122 hasMousemoveListener_: false,
123
124 hostAttributes: {
125 tabindex: 0,
126 },
127
128 listeners: {
129 'keydown': 'onKeyDown_',
130 'mouseover': 'onMouseover_',
131 'tap': 'onTap_',
132 },
133
134 /** override */
135 attached: function() {
136 this.options_ = this.querySelectorAll('.dropdown-item');
137 },
138
139 /** override */
140 detached: function() {
141 this.removeListeners_();
142 },
143
144 /** @private */
145 removeListeners_: function() {
146 window.removeEventListener('resize', this.boundClose_);
147 window.removeEventListener('popstate', this.boundClose_);
148 },
149
150 /**
151 * @param {!Event} e
39 * @private 152 * @private
40 * @param {number} start 153 */
41 * @param {number} end 154 onTap_: function(e) {
42 * @param {number} length 155 if (e.target == this) {
43 * @param {AnchorAlignment} anchorAlignment 156 this.close();
44 * @param {number} min 157 e.stopPropagation();
45 * @param {number} max 158 }
46 * @return {number} 159 },
47 */ 160
48 function getStartPointWithAnchor( 161 /**
49 start, end, length, anchorAlignment, min, max) { 162 * @param {!KeyboardEvent} e
50 var startPoint = 0;
51 switch (anchorAlignment) {
52 case AnchorAlignment.BEFORE_START:
53 startPoint = -length;
54 break;
55 case AnchorAlignment.AFTER_START:
56 startPoint = start;
57 break;
58 case AnchorAlignment.CENTER:
59 startPoint = (start + end - length) / 2;
60 break;
61 case AnchorAlignment.BEFORE_END:
62 startPoint = end - length;
63 break;
64 case AnchorAlignment.AFTER_END:
65 startPoint = end;
66 break;
67 }
68
69 if (startPoint + length > max)
70 startPoint = end - length;
71 if (startPoint < min)
72 startPoint = start;
73 return startPoint;
74 }
75
76
77 /**
78 * @private 163 * @private
79 * @return {!ShowConfig} 164 */
80 */ 165 onKeyDown_: function(e) {
81 function getDefaultShowConfig() { 166 if (e.key == 'Tab' || e.key == 'Escape') {
82 return { 167 this.close();
83 top: 0, 168 e.preventDefault();
84 left: 0, 169 return;
85 height: 0, 170 }
86 width: 0, 171
87 anchorAlignmentX: AnchorAlignment.AFTER_START, 172 if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')
88 anchorAlignmentY: AnchorAlignment.AFTER_START, 173 return;
89 minX: 0, 174
90 minY: 0, 175 var nextOption = this.getNextOption_(e.key == 'ArrowDown' ? 1 : -1);
91 maxX: window.innerWidth, 176 if (nextOption) {
92 maxY: window.innerHeight, 177 if (!this.hasMousemoveListener_) {
93 }; 178 this.hasMousemoveListener_ = true;
94 } 179 listenOnce(this, 'mousemove', function(e) {
95 180 this.onMouseover_(e);
96 Polymer({ 181 this.hasMousemoveListener_ = false;
97 is: 'cr-action-menu', 182 }.bind(this));
98 extends: 'dialog',
99
100 /**
101 * List of all options in this action menu.
102 * @private {?NodeList<!Element>}
103 */
104 options_: null,
105
106 /**
107 * The element which the action menu will be anchored to. Also the element
108 * where focus will be returned after the menu is closed. Only populated if
109 * menu is opened with showAt().
110 * @private {?Element}
111 */
112 anchorElement_: null,
113
114 /**
115 * Bound reference to an event listener function such that it can be removed
116 * on detach.
117 * @private {?Function}
118 */
119 boundClose_: null,
120
121 /** @private {boolean} */
122 hasMousemoveListener_: false,
123
124 hostAttributes: {
125 tabindex: 0,
126 },
127
128 listeners: {
129 'keydown': 'onKeyDown_',
130 'mouseover': 'onMouseover_',
131 'tap': 'onTap_',
132 },
133
134 /** override */
135 attached: function() {
136 this.options_ = this.querySelectorAll('.dropdown-item');
137 },
138
139 /** override */
140 detached: function() {
141 this.removeListeners_();
142 },
143
144 /** @private */
145 removeListeners_: function() {
146 window.removeEventListener('resize', this.boundClose_);
147 window.removeEventListener('popstate', this.boundClose_);
148 },
149
150 /**
151 * @param {!Event} e
152 * @private
153 */
154 onTap_: function(e) {
155 if (e.target == this) {
156 this.close();
157 e.stopPropagation();
158 } 183 }
159 }, 184 nextOption.focus();
160 185 }
161 /** 186
162 * @param {!KeyboardEvent} e 187 e.preventDefault();
163 * @private 188 },
164 */ 189
165 onKeyDown_: function(e) { 190 /**
166 if (e.key == 'Tab' || e.key == 'Escape') { 191 * @param {!Event} e
167 this.close(); 192 * @private
168 e.preventDefault(); 193 */
194 onMouseover_: function(e) {
195 // TODO(scottchen): Using "focus" to determine selected item might mess
196 // with screen readers in some edge cases.
197 var i = 0;
198 do {
199 var target = e.path[i++];
200 if (target.classList && target.classList.contains('dropdown-item')) {
201 target.focus();
169 return; 202 return;
170 } 203 }
171 204 } while (this != target);
172 if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') 205
173 return; 206 // The user moved the mouse off the options. Reset focus to the dialog.
174 207 this.focus();
175 var nextOption = this.getNextOption_(e.key == 'ArrowDown' ? 1 : -1); 208 },
176 if (nextOption) { 209
177 if (!this.hasMousemoveListener_) { 210 /**
178 this.hasMousemoveListener_ = true; 211 * @param {number} step -1 for getting previous option (up), 1 for getting
179 listenOnce(this, 'mousemove', function(e) { 212 * next option (down).
180 this.onMouseover_(e); 213 * @return {?Element} The next focusable option, taking into account
181 this.hasMousemoveListener_ = false; 214 * disabled/hidden attributes, or null if no focusable option exists.
182 }.bind(this)); 215 * @private
183 } 216 */
184 nextOption.focus(); 217 getNextOption_: function(step) {
185 } 218 // Using a counter to ensure no infinite loop occurs if all elements are
186 219 // hidden/disabled.
187 e.preventDefault(); 220 var counter = 0;
188 }, 221 var nextOption = null;
189 222 var numOptions = this.options_.length;
190 /** 223 var focusedIndex =
191 * @param {!Event} e 224 Array.prototype.indexOf.call(this.options_, this.root.activeElement);
192 * @private 225
193 */ 226 // Handle case where nothing is focused and up is pressed.
194 onMouseover_: function(e) { 227 if (focusedIndex === -1 && step === -1)
195 // TODO(scottchen): Using "focus" to determine selected item might mess 228 focusedIndex = 0;
196 // with screen readers in some edge cases. 229
197 var i = 0; 230 do {
198 do { 231 focusedIndex = (numOptions + focusedIndex + step) % numOptions;
199 var target = e.path[i++]; 232 nextOption = this.options_[focusedIndex];
200 if (target.classList && target.classList.contains('dropdown-item')) { 233 if (nextOption.disabled || nextOption.hidden)
201 target.focus(); 234 nextOption = null;
202 return; 235 counter++;
203 } 236 } while (!nextOption && counter < numOptions);
204 } while (this != target); 237
205 238 return nextOption;
206 // The user moved the mouse off the options. Reset focus to the dialog. 239 },
207 this.focus(); 240
208 }, 241 /** @override */
209 242 close: function() {
210 /** 243 // Removing 'resize' and 'popstate' listeners when dialog is closed.
211 * @param {number} step -1 for getting previous option (up), 1 for getting 244 this.removeListeners_();
212 * next option (down). 245 HTMLDialogElement.prototype.close.call(this);
213 * @return {?Element} The next focusable option, taking into account 246 if (this.anchorElement_) {
214 * disabled/hidden attributes, or null if no focusable option exists. 247 cr.ui.focusWithoutInk(assert(this.anchorElement_));
215 * @private 248 this.anchorElement_ = null;
216 */ 249 }
217 getNextOption_: function(step) { 250 },
218 // Using a counter to ensure no infinite loop occurs if all elements are 251
219 // hidden/disabled. 252 /**
220 var counter = 0; 253 * Shows the menu anchored to the given element.
221 var nextOption = null; 254 * @param {!Element} anchorElement
222 var numOptions = this.options_.length; 255 */
223 var focusedIndex = 256 showAt: function(anchorElement) {
224 Array.prototype.indexOf.call(this.options_, this.root.activeElement); 257 this.anchorElement_ = anchorElement;
225 258 this.anchorElement_.scrollIntoViewIfNeeded();
226 // Handle case where nothing is focused and up is pressed. 259 var rect = this.anchorElement_.getBoundingClientRect();
227 if (focusedIndex === -1 && step === -1) 260 this.showAtPosition({
228 focusedIndex = 0; 261 top: rect.top,
229 262 left: rect.left,
230 do { 263 height: rect.height,
231 focusedIndex = (numOptions + focusedIndex + step) % numOptions; 264 width: rect.width,
232 nextOption = this.options_[focusedIndex]; 265 // Default to anchoring towards the left.
233 if (nextOption.disabled || nextOption.hidden) 266 anchorAlignmentX: AnchorAlignment.BEFORE_END,
234 nextOption = null; 267 });
235 counter++; 268 },
236 } while (!nextOption && counter < numOptions); 269
237 270 /**
238 return nextOption; 271 * Shows the menu anchored to the given box. The anchor alignment is
239 }, 272 * specified as an X and Y alignment which represents a point in the anchor
240 273 * where the menu will align to, which can have the menu either before or
241 /** @override */ 274 * after the given point in each axis. Center alignment places the center of
242 close: function() { 275 * the menu in line with the center of the anchor.
243 // Removing 'resize' and 'popstate' listeners when dialog is closed. 276 *
244 this.removeListeners_(); 277 * y-start
245 HTMLDialogElement.prototype.close.call(this); 278 * _____________
246 if (this.anchorElement_) { 279 * | |
247 cr.ui.focusWithoutInk(assert(this.anchorElement_)); 280 * | |
248 this.anchorElement_ = null; 281 * | CENTER |
249 } 282 * x-start | x | x-end
250 }, 283 * | |
251 284 * |anchor box |
252 /** 285 * |___________|
253 * Shows the menu anchored to the given element. 286 *
254 * @param {!Element} anchorElement 287 * y-end
255 */ 288 *
256 showAt: function(anchorElement) { 289 * For example, aligning the menu to the inside of the top-right edge of
257 this.anchorElement_ = anchorElement; 290 * the anchor, extending towards the bottom-left would use a alignment of
258 this.anchorElement_.scrollIntoViewIfNeeded(); 291 * (BEFORE_END, AFTER_START), whereas centering the menu below the bottom
259 var rect = this.anchorElement_.getBoundingClientRect(); 292 * edge of the anchor would use (CENTER, AFTER_END).
260 this.showAtPosition({ 293 *
261 top: rect.top, 294 * @param {!ShowConfig} config
262 left: rect.left, 295 */
263 height: rect.height, 296 showAtPosition: function(config) {
264 width: rect.width, 297 var c = Object.assign(getDefaultShowConfig(), config);
265 // Default to anchoring towards the left. 298
266 anchorAlignmentX: AnchorAlignment.BEFORE_END, 299 var top = c.top;
267 }); 300 var left = c.left;
268 }, 301 var bottom = top + c.height;
269 302 var right = left + c.width;
270 /** 303
271 * Shows the menu anchored to the given box. The anchor alignment is 304 this.boundClose_ = this.boundClose_ || function() {
272 * specified as an X and Y alignment which represents a point in the anchor 305 if (this.open)
273 * where the menu will align to, which can have the menu either before or 306 this.close();
274 * after the given point in each axis. Center alignment places the center of 307 }.bind(this);
275 * the menu in line with the center of the anchor. 308 window.addEventListener('resize', this.boundClose_);
276 * 309 window.addEventListener('popstate', this.boundClose_);
277 * y-start 310
278 * _____________ 311 // Reset position to prevent previous values from affecting layout.
279 * | | 312 this.style.left = '';
280 * | | 313 this.style.right = '';
281 * | CENTER | 314 this.style.top = '';
282 * x-start | x | x-end 315
283 * | | 316 this.showModal();
284 * |anchor box | 317
285 * |___________| 318 // Flip the X anchor in RTL.
286 * 319 var rtl = getComputedStyle(this).direction == 'rtl';
287 * y-end 320 if (rtl)
288 * 321 c.anchorAlignmentX *= -1;
289 * For example, aligning the menu to the inside of the top-right edge of 322
290 * the anchor, extending towards the bottom-left would use a alignment of 323 var menuLeft = getStartPointWithAnchor(
291 * (BEFORE_END, AFTER_START), whereas centering the menu below the bottom 324 left, right, this.offsetWidth, c.anchorAlignmentX, c.minX, c.maxX);
292 * edge of the anchor would use (CENTER, AFTER_END). 325
293 * 326 if (rtl) {
294 * @param {!ShowConfig} config 327 var menuRight = window.innerWidth - menuLeft - this.offsetWidth;
295 */ 328 this.style.right = menuRight + 'px';
296 showAtPosition: function(config) { 329 } else {
297 var c = Object.assign(getDefaultShowConfig(), config); 330 this.style.left = menuLeft + 'px';
298 331 }
299 var top = c.top; 332
300 var left = c.left; 333 var menuTop = getStartPointWithAnchor(
301 var bottom = top + c.height; 334 top, bottom, this.offsetHeight, c.anchorAlignmentY, c.minY, c.maxY);
302 var right = left + c.width; 335 this.style.top = menuTop + 'px';
303 336 },
304 this.boundClose_ = this.boundClose_ || function() { 337 });
305 if (this.open)
306 this.close();
307 }.bind(this);
308 window.addEventListener('resize', this.boundClose_);
309 window.addEventListener('popstate', this.boundClose_);
310
311 // Reset position to prevent previous values from affecting layout.
312 this.style.left = '';
313 this.style.right = '';
314 this.style.top = '';
315
316 this.showModal();
317
318 // Flip the X anchor in RTL.
319 var rtl = getComputedStyle(this).direction == 'rtl';
320 if (rtl)
321 c.anchorAlignmentX *= -1;
322
323 var menuLeft = getStartPointWithAnchor(
324 left, right, this.offsetWidth, c.anchorAlignmentX, c.minX, c.maxX);
325
326 if (rtl) {
327 var menuRight = window.innerWidth - menuLeft - this.offsetWidth;
328 this.style.right = menuRight + 'px';
329 } else {
330 this.style.left = menuLeft + 'px';
331 }
332
333 var menuTop = getStartPointWithAnchor(
334 top, bottom, this.offsetHeight, c.anchorAlignmentY, c.minY, c.maxY);
335 this.style.top = menuTop + 'px';
336 },
337 });
338 })(); 338 })();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698