blob: 94a43eada0447b23f87ecdd560e18a8cff5b63e3 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4/**
5 * @template T
6 * @implements {UI.ListDelegate<T>}
7 */
Tim van der Lippe0830b3d2019-10-03 13:20:078export default class SoftDropDown {
Blink Reformat4c46d092018-04-07 15:32:379 /**
10 * @param {!UI.ListModel<T>} model
Tim van der Lippe0830b3d2019-10-03 13:20:0711 * @param {!Delegate<T>} delegate
Blink Reformat4c46d092018-04-07 15:32:3712 */
13 constructor(model, delegate) {
14 this._delegate = delegate;
15 this._selectedItem = null;
16 this._model = model;
17
Hongchan Choi83648232019-05-04 01:35:4618 this._placeholderText = ls`(no item selected)`;
19
Blink Reformat4c46d092018-04-07 15:32:3720 this.element = createElementWithClass('button', 'soft-dropdown');
Joel Einbinder7fbe24c2019-01-24 05:19:0121 UI.appendStyle(this.element, 'ui/softDropDownButton.css');
22 this._titleElement = this.element.createChild('span', 'title');
Blink Reformat4c46d092018-04-07 15:32:3723 const dropdownArrowIcon = UI.Icon.create('smallicon-triangle-down');
Joel Einbinder7fbe24c2019-01-24 05:19:0124 this.element.appendChild(dropdownArrowIcon);
John Emau8def92f2019-08-14 03:01:5325 UI.ARIAUtils.setExpanded(this.element, false);
Blink Reformat4c46d092018-04-07 15:32:3726
27 this._glassPane = new UI.GlassPane();
28 this._glassPane.setMarginBehavior(UI.GlassPane.MarginBehavior.NoMargin);
29 this._glassPane.setAnchorBehavior(UI.GlassPane.AnchorBehavior.PreferBottom);
30 this._glassPane.setOutsideClickCallback(this._hide.bind(this));
31 this._glassPane.setPointerEventsBehavior(UI.GlassPane.PointerEventsBehavior.BlockedByGlassPane);
32 this._list = new UI.ListControl(model, this, UI.ListMode.EqualHeightItems);
33 this._list.element.classList.add('item-list');
34 this._rowHeight = 36;
35 this._width = 315;
36 UI.createShadowRootWithCoreStyles(this._glassPane.contentElement, 'ui/softDropDown.css')
John Emau8def92f2019-08-14 03:01:5337 .createChild('div', 'list-container') // issue #972755
Blink Reformat4c46d092018-04-07 15:32:3738 .appendChild(this._list.element);
John Emau8def92f2019-08-14 03:01:5339 UI.ARIAUtils.markAsMenu(this._list.element);
Blink Reformat4c46d092018-04-07 15:32:3740
41 this._listWasShowing200msAgo = false;
42 this.element.addEventListener('mousedown', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:3443 if (this._listWasShowing200msAgo) {
Blink Reformat4c46d092018-04-07 15:32:3744 this._hide(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:3445 } else if (!this.element.disabled) {
Blink Reformat4c46d092018-04-07 15:32:3746 this._show(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:3447 }
Blink Reformat4c46d092018-04-07 15:32:3748 }, false);
John Emau8def92f2019-08-14 03:01:5349 this.element.addEventListener('keydown', this._onKeyDownButton.bind(this), false);
50 this._list.element.addEventListener('keydown', this._onKeyDownList.bind(this), false);
51 this._list.element.addEventListener('focusout', this._hide.bind(this), false);
Blink Reformat4c46d092018-04-07 15:32:3752 this._list.element.addEventListener('mousedown', event => event.consume(true), false);
53 this._list.element.addEventListener('mouseup', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:3454 if (event.target === this._list.element) {
Blink Reformat4c46d092018-04-07 15:32:3755 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3456 }
Blink Reformat4c46d092018-04-07 15:32:3757
Tim van der Lippe1d6e57a2019-09-30 11:55:3458 if (!this._listWasShowing200msAgo) {
Blink Reformat4c46d092018-04-07 15:32:3759 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3460 }
Blink Reformat4c46d092018-04-07 15:32:3761 this._selectHighlightedItem();
62 this._hide(event);
63 }, false);
64 model.addEventListener(UI.ListModel.Events.ItemsReplaced, this._itemsReplaced, this);
65 }
66
67 /**
68 * @param {!Event} event
69 */
70 _show(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3471 if (this._glassPane.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:3772 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3473 }
Blink Reformat4c46d092018-04-07 15:32:3774 this._glassPane.setContentAnchorBox(this.element.boxInWindow());
75 this._glassPane.show(/** @type {!Document} **/ (this.element.ownerDocument));
John Emau8def92f2019-08-14 03:01:5376 this._list.element.focus();
77 UI.ARIAUtils.setExpanded(this.element, true);
Blink Reformat4c46d092018-04-07 15:32:3778 this._updateGlasspaneSize();
Tim van der Lippe1d6e57a2019-09-30 11:55:3479 if (this._selectedItem) {
Blink Reformat4c46d092018-04-07 15:32:3780 this._list.selectItem(this._selectedItem);
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 }
Blink Reformat4c46d092018-04-07 15:32:3782 event.consume(true);
83 setTimeout(() => this._listWasShowing200msAgo = true, 200);
84 }
85
86 _updateGlasspaneSize() {
87 const maxHeight = this._rowHeight * (Math.min(this._model.length, 9));
88 this._glassPane.setMaxContentSize(new UI.Size(this._width, maxHeight));
89 this._list.viewportResized();
90 }
91
92 /**
93 * @param {!Event} event
94 */
95 _hide(event) {
96 setTimeout(() => this._listWasShowing200msAgo = false, 200);
97 this._glassPane.hide();
98 this._list.selectItem(null);
John Emau8def92f2019-08-14 03:01:5399 UI.ARIAUtils.setExpanded(this.element, false);
100 this.element.focus();
Blink Reformat4c46d092018-04-07 15:32:37101 event.consume(true);
102 }
103
104 /**
105 * @param {!Event} event
106 */
John Emau8def92f2019-08-14 03:01:53107 _onKeyDownButton(event) {
108 let handled = false;
109 switch (event.key) {
110 case 'ArrowUp':
111 this._show(event);
112 this._list.selectItemNextPage();
113 handled = true;
114 break;
115 case 'ArrowDown':
116 this._show(event);
117 this._list.selectItemPreviousPage();
118 handled = true;
119 break;
120 case 'Enter':
121 case ' ':
122 this._show(event);
123 handled = true;
124 break;
125 default:
126 break;
127 }
128
Tim van der Lippe1d6e57a2019-09-30 11:55:34129 if (handled) {
John Emau8def92f2019-08-14 03:01:53130 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34131 }
John Emau8def92f2019-08-14 03:01:53132 }
133
134 /**
135 * @param {!Event} event
136 */
137 _onKeyDownList(event) {
Blink Reformat4c46d092018-04-07 15:32:37138 let handled = false;
139 switch (event.key) {
140 case 'ArrowLeft':
Blink Reformat4c46d092018-04-07 15:32:37141 handled = this._list.selectPreviousItem(false, false);
142 break;
143 case 'ArrowRight':
Blink Reformat4c46d092018-04-07 15:32:37144 handled = this._list.selectNextItem(false, false);
145 break;
Blink Reformat4c46d092018-04-07 15:32:37146 case 'Home':
147 for (let i = 0; i < this._model.length; i++) {
148 if (this.isItemSelectable(this._model.at(i))) {
149 this._list.selectItem(this._model.at(i));
150 handled = true;
151 break;
152 }
153 }
154 break;
155 case 'End':
156 for (let i = this._model.length - 1; i >= 0; i--) {
157 if (this.isItemSelectable(this._model.at(i))) {
158 this._list.selectItem(this._model.at(i));
159 handled = true;
160 break;
161 }
162 }
163 break;
164 case 'Escape':
165 this._hide(event);
John Emau8def92f2019-08-14 03:01:53166 handled = true;
Blink Reformat4c46d092018-04-07 15:32:37167 break;
168 case 'Tab':
Blink Reformat4c46d092018-04-07 15:32:37169 case 'Enter':
John Emau8def92f2019-08-14 03:01:53170 case ' ':
Blink Reformat4c46d092018-04-07 15:32:37171 this._selectHighlightedItem();
172 this._hide(event);
John Emau8def92f2019-08-14 03:01:53173 handled = true;
Blink Reformat4c46d092018-04-07 15:32:37174 break;
175 default:
176 if (event.key.length === 1) {
177 const selectedIndex = this._list.selectedIndex();
178 const letter = event.key.toUpperCase();
179 for (let i = 0; i < this._model.length; i++) {
180 const item = this._model.at((selectedIndex + i + 1) % this._model.length);
181 if (this._delegate.titleFor(item).toUpperCase().startsWith(letter)) {
182 this._list.selectItem(item);
183 break;
184 }
185 }
186 handled = true;
187 }
188 break;
189 }
190
Tim van der Lippe1d6e57a2019-09-30 11:55:34191 if (handled) {
Blink Reformat4c46d092018-04-07 15:32:37192 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34193 }
Blink Reformat4c46d092018-04-07 15:32:37194 }
195
196 /**
197 * @param {number} width
198 */
199 setWidth(width) {
200 this._width = width;
201 this._updateGlasspaneSize();
202 }
203
204 /**
205 * @param {number} rowHeight
206 */
207 setRowHeight(rowHeight) {
208 this._rowHeight = rowHeight;
209 }
210
211 /**
Hongchan Choi83648232019-05-04 01:35:46212 * @param {string} text
213 */
214 setPlaceholderText(text) {
215 this._placeholderText = text;
Tim van der Lippe1d6e57a2019-09-30 11:55:34216 if (!this._selectedItem) {
Hongchan Choi83648232019-05-04 01:35:46217 this._titleElement.textContent = this._placeholderText;
Tim van der Lippe1d6e57a2019-09-30 11:55:34218 }
Hongchan Choi83648232019-05-04 01:35:46219 }
220
221 /**
Blink Reformat4c46d092018-04-07 15:32:37222 * @param {!Common.Event} event
223 */
224 _itemsReplaced(event) {
225 const removed = /** @type {!Array<T>} */ (event.data.removed);
226 if (removed.indexOf(this._selectedItem) !== -1) {
227 this._selectedItem = null;
228 this._selectHighlightedItem();
229 }
230 this._updateGlasspaneSize();
231 }
232
233 /**
234 * @param {?T} item
235 */
236 selectItem(item) {
237 this._selectedItem = item;
Tim van der Lippe1d6e57a2019-09-30 11:55:34238 if (this._selectedItem) {
Blink Reformat4c46d092018-04-07 15:32:37239 this._titleElement.textContent = this._delegate.titleFor(this._selectedItem);
Tim van der Lippe1d6e57a2019-09-30 11:55:34240 } else {
Hongchan Choi83648232019-05-04 01:35:46241 this._titleElement.textContent = this._placeholderText;
Tim van der Lippe1d6e57a2019-09-30 11:55:34242 }
Blink Reformat4c46d092018-04-07 15:32:37243 this._delegate.itemSelected(this._selectedItem);
244 }
245
246 /**
247 * @override
248 * @param {T} item
249 * @return {!Element}
250 */
251 createElementForItem(item) {
252 const element = createElementWithClass('div', 'item');
253 element.addEventListener('mousemove', e => {
Tim van der Lippe1d6e57a2019-09-30 11:55:34254 if ((e.movementX || e.movementY) && this._delegate.isItemSelectable(item)) {
Blink Reformat4c46d092018-04-07 15:32:37255 this._list.selectItem(item, false, /* Don't scroll */ true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34256 }
Blink Reformat4c46d092018-04-07 15:32:37257 });
258 element.classList.toggle('disabled', !this._delegate.isItemSelectable(item));
259 element.classList.toggle('highlighted', this._list.selectedItem() === item);
260
John Emau8def92f2019-08-14 03:01:53261 UI.ARIAUtils.markAsMenuItem(element);
Blink Reformat4c46d092018-04-07 15:32:37262 element.appendChild(this._delegate.createElementForItem(item));
263
264 return element;
265 }
266
267 /**
268 * @override
269 * @param {T} item
270 * @return {number}
271 */
272 heightForItem(item) {
273 return this._rowHeight;
274 }
275
276 /**
277 * @override
278 * @param {T} item
279 * @return {boolean}
280 */
281 isItemSelectable(item) {
282 return this._delegate.isItemSelectable(item);
283 }
284
285 /**
286 * @override
287 * @param {?T} from
288 * @param {?T} to
289 * @param {?Element} fromElement
290 * @param {?Element} toElement
291 */
292 selectedItemChanged(from, to, fromElement, toElement) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34293 if (fromElement) {
Blink Reformat4c46d092018-04-07 15:32:37294 fromElement.classList.remove('highlighted');
Tim van der Lippe1d6e57a2019-09-30 11:55:34295 }
296 if (toElement) {
Blink Reformat4c46d092018-04-07 15:32:37297 toElement.classList.add('highlighted');
Tim van der Lippe1d6e57a2019-09-30 11:55:34298 }
John Emau8def92f2019-08-14 03:01:53299
300 UI.ARIAUtils.setActiveDescendant(this._list.element, toElement);
Blink Reformat4c46d092018-04-07 15:32:37301 this._delegate.highlightedItemChanged(
302 from, to, fromElement && fromElement.firstElementChild, toElement && toElement.firstElementChild);
303 }
304
Jack Lynch805641c2019-12-07 00:05:39305 /**
306 * @override
307 * @param {?Element} fromElement
308 * @param {?Element} toElement
309 * @return {boolean}
310 */
311 updateSelectedItemARIA(fromElement, toElement) {
312 return false;
313 }
314
Blink Reformat4c46d092018-04-07 15:32:37315 _selectHighlightedItem() {
316 this.selectItem(this._list.selectedItem());
317 }
318
319 /**
320 * @param {T} item
321 */
322 refreshItem(item) {
323 this._list.refreshItem(item);
324 }
Tim van der Lippe0830b3d2019-10-03 13:20:07325}
Blink Reformat4c46d092018-04-07 15:32:37326
327/**
328 * @interface
329 * @template T
330 */
Tim van der Lippe0830b3d2019-10-03 13:20:07331export class Delegate {
Blink Reformat4c46d092018-04-07 15:32:37332 /**
333 * @param {T} item
334 * @return {string}
335 */
336 titleFor(item) {
337 }
338
339 /**
340 * @param {T} item
341 * @return {!Element}
342 */
343 createElementForItem(item) {
344 }
345
346 /**
347 * @param {T} item
348 * @return {boolean}
349 */
350 isItemSelectable(item) {
351 }
352
353 /**
354 * @param {?T} item
355 */
356 itemSelected(item) {
357 }
358
359 /**
360 * @param {?T} from
361 * @param {?T} to
362 * @param {?Element} fromElement
363 * @param {?Element} toElement
364 */
365 highlightedItemChanged(from, to, fromElement, toElement) {
366 }
Tim van der Lippe0830b3d2019-10-03 13:20:07367}
368
369/* Legacy exported object*/
370self.UI = self.UI || {};
371
372/* Legacy exported object*/
373UI = UI || {};
374
375/** @constructor */
376UI.SoftDropDown = SoftDropDown;
377
378/**
379 * @interface
380 * @template T
381 */
Tim van der Lippe20b29c22019-11-04 14:36:15382UI.SoftDropDown.Delegate = Delegate;