blob: dfa9b5ef88e375247d9b657f8f932dbee48372b6 [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.
Paul Lewis9950e182019-12-16 16:06:074
5import {Size} from './Geometry.js';
6import {AnchorBehavior, GlassPane, MarginBehavior, PointerEventsBehavior} from './GlassPane.js';
7import {Icon} from './Icon.js';
8import {ListControl, ListDelegate, ListMode} from './ListControl.js'; // eslint-disable-line no-unused-vars
9import {Events as ListModelEvents, ListModel} from './ListModel.js'; // eslint-disable-line no-unused-vars
10import {appendStyle} from './utils/append-style.js';
11import {createShadowRootWithCoreStyles} from './utils/create-shadow-root-with-core-styles.js';
12
Blink Reformat4c46d092018-04-07 15:32:3713/**
14 * @template T
Paul Lewis9950e182019-12-16 16:06:0715 * @implements {ListDelegate<T>}
Blink Reformat4c46d092018-04-07 15:32:3716 */
Paul Lewis9950e182019-12-16 16:06:0717export class SoftDropDown {
Blink Reformat4c46d092018-04-07 15:32:3718 /**
Paul Lewis9950e182019-12-16 16:06:0719 * @param {!ListModel<T>} model
Tim van der Lippe0830b3d2019-10-03 13:20:0720 * @param {!Delegate<T>} delegate
Blink Reformat4c46d092018-04-07 15:32:3721 */
22 constructor(model, delegate) {
23 this._delegate = delegate;
24 this._selectedItem = null;
25 this._model = model;
26
Hongchan Choi83648232019-05-04 01:35:4627 this._placeholderText = ls`(no item selected)`;
28
Blink Reformat4c46d092018-04-07 15:32:3729 this.element = createElementWithClass('button', 'soft-dropdown');
Paul Lewis9950e182019-12-16 16:06:0730 appendStyle(this.element, 'ui/softDropDownButton.css');
Joel Einbinder7fbe24c2019-01-24 05:19:0131 this._titleElement = this.element.createChild('span', 'title');
Paul Lewis9950e182019-12-16 16:06:0732 const dropdownArrowIcon = Icon.create('smallicon-triangle-down');
Joel Einbinder7fbe24c2019-01-24 05:19:0133 this.element.appendChild(dropdownArrowIcon);
John Emau8def92f2019-08-14 03:01:5334 UI.ARIAUtils.setExpanded(this.element, false);
Blink Reformat4c46d092018-04-07 15:32:3735
Paul Lewis9950e182019-12-16 16:06:0736 this._glassPane = new GlassPane();
37 this._glassPane.setMarginBehavior(MarginBehavior.NoMargin);
38 this._glassPane.setAnchorBehavior(AnchorBehavior.PreferBottom);
Blink Reformat4c46d092018-04-07 15:32:3739 this._glassPane.setOutsideClickCallback(this._hide.bind(this));
Paul Lewis9950e182019-12-16 16:06:0740 this._glassPane.setPointerEventsBehavior(PointerEventsBehavior.BlockedByGlassPane);
41 this._list = new ListControl(model, this, ListMode.EqualHeightItems);
Blink Reformat4c46d092018-04-07 15:32:3742 this._list.element.classList.add('item-list');
43 this._rowHeight = 36;
44 this._width = 315;
Paul Lewis9950e182019-12-16 16:06:0745 createShadowRootWithCoreStyles(this._glassPane.contentElement, 'ui/softDropDown.css')
John Emau2fe63512020-02-05 18:52:4746 .createChild('slot') // issue #972755
Blink Reformat4c46d092018-04-07 15:32:3747 .appendChild(this._list.element);
John Emau8def92f2019-08-14 03:01:5348 UI.ARIAUtils.markAsMenu(this._list.element);
Blink Reformat4c46d092018-04-07 15:32:3749
50 this._listWasShowing200msAgo = false;
51 this.element.addEventListener('mousedown', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:3452 if (this._listWasShowing200msAgo) {
Blink Reformat4c46d092018-04-07 15:32:3753 this._hide(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:3454 } else if (!this.element.disabled) {
Blink Reformat4c46d092018-04-07 15:32:3755 this._show(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:3456 }
Blink Reformat4c46d092018-04-07 15:32:3757 }, false);
John Emau8def92f2019-08-14 03:01:5358 this.element.addEventListener('keydown', this._onKeyDownButton.bind(this), false);
59 this._list.element.addEventListener('keydown', this._onKeyDownList.bind(this), false);
60 this._list.element.addEventListener('focusout', this._hide.bind(this), false);
Blink Reformat4c46d092018-04-07 15:32:3761 this._list.element.addEventListener('mousedown', event => event.consume(true), false);
62 this._list.element.addEventListener('mouseup', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:3463 if (event.target === this._list.element) {
Blink Reformat4c46d092018-04-07 15:32:3764 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3465 }
Blink Reformat4c46d092018-04-07 15:32:3766
Tim van der Lippe1d6e57a2019-09-30 11:55:3467 if (!this._listWasShowing200msAgo) {
Blink Reformat4c46d092018-04-07 15:32:3768 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3469 }
Blink Reformat4c46d092018-04-07 15:32:3770 this._selectHighlightedItem();
71 this._hide(event);
72 }, false);
Paul Lewis9950e182019-12-16 16:06:0773 model.addEventListener(ListModelEvents.ItemsReplaced, this._itemsReplaced, this);
Blink Reformat4c46d092018-04-07 15:32:3774 }
75
76 /**
77 * @param {!Event} event
78 */
79 _show(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3480 if (this._glassPane.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:3781 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3482 }
Blink Reformat4c46d092018-04-07 15:32:3783 this._glassPane.setContentAnchorBox(this.element.boxInWindow());
84 this._glassPane.show(/** @type {!Document} **/ (this.element.ownerDocument));
John Emau8def92f2019-08-14 03:01:5385 this._list.element.focus();
86 UI.ARIAUtils.setExpanded(this.element, true);
Blink Reformat4c46d092018-04-07 15:32:3787 this._updateGlasspaneSize();
Tim van der Lippe1d6e57a2019-09-30 11:55:3488 if (this._selectedItem) {
Blink Reformat4c46d092018-04-07 15:32:3789 this._list.selectItem(this._selectedItem);
Tim van der Lippe1d6e57a2019-09-30 11:55:3490 }
Blink Reformat4c46d092018-04-07 15:32:3791 event.consume(true);
92 setTimeout(() => this._listWasShowing200msAgo = true, 200);
93 }
94
95 _updateGlasspaneSize() {
96 const maxHeight = this._rowHeight * (Math.min(this._model.length, 9));
Paul Lewis9950e182019-12-16 16:06:0797 this._glassPane.setMaxContentSize(new Size(this._width, maxHeight));
Blink Reformat4c46d092018-04-07 15:32:3798 this._list.viewportResized();
99 }
100
101 /**
102 * @param {!Event} event
103 */
104 _hide(event) {
105 setTimeout(() => this._listWasShowing200msAgo = false, 200);
106 this._glassPane.hide();
107 this._list.selectItem(null);
John Emau8def92f2019-08-14 03:01:53108 UI.ARIAUtils.setExpanded(this.element, false);
109 this.element.focus();
Blink Reformat4c46d092018-04-07 15:32:37110 event.consume(true);
111 }
112
113 /**
114 * @param {!Event} event
115 */
John Emau8def92f2019-08-14 03:01:53116 _onKeyDownButton(event) {
117 let handled = false;
118 switch (event.key) {
119 case 'ArrowUp':
120 this._show(event);
121 this._list.selectItemNextPage();
122 handled = true;
123 break;
124 case 'ArrowDown':
125 this._show(event);
126 this._list.selectItemPreviousPage();
127 handled = true;
128 break;
129 case 'Enter':
130 case ' ':
131 this._show(event);
132 handled = true;
133 break;
134 default:
135 break;
136 }
137
Tim van der Lippe1d6e57a2019-09-30 11:55:34138 if (handled) {
John Emau8def92f2019-08-14 03:01:53139 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 }
John Emau8def92f2019-08-14 03:01:53141 }
142
143 /**
144 * @param {!Event} event
145 */
146 _onKeyDownList(event) {
Blink Reformat4c46d092018-04-07 15:32:37147 let handled = false;
148 switch (event.key) {
149 case 'ArrowLeft':
Blink Reformat4c46d092018-04-07 15:32:37150 handled = this._list.selectPreviousItem(false, false);
151 break;
152 case 'ArrowRight':
Blink Reformat4c46d092018-04-07 15:32:37153 handled = this._list.selectNextItem(false, false);
154 break;
Blink Reformat4c46d092018-04-07 15:32:37155 case 'Home':
156 for (let i = 0; i < this._model.length; 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 'End':
165 for (let i = this._model.length - 1; i >= 0; i--) {
166 if (this.isItemSelectable(this._model.at(i))) {
167 this._list.selectItem(this._model.at(i));
168 handled = true;
169 break;
170 }
171 }
172 break;
173 case 'Escape':
174 this._hide(event);
John Emau8def92f2019-08-14 03:01:53175 handled = true;
Blink Reformat4c46d092018-04-07 15:32:37176 break;
177 case 'Tab':
Blink Reformat4c46d092018-04-07 15:32:37178 case 'Enter':
John Emau8def92f2019-08-14 03:01:53179 case ' ':
Blink Reformat4c46d092018-04-07 15:32:37180 this._selectHighlightedItem();
181 this._hide(event);
John Emau8def92f2019-08-14 03:01:53182 handled = true;
Blink Reformat4c46d092018-04-07 15:32:37183 break;
184 default:
185 if (event.key.length === 1) {
186 const selectedIndex = this._list.selectedIndex();
187 const letter = event.key.toUpperCase();
188 for (let i = 0; i < this._model.length; i++) {
189 const item = this._model.at((selectedIndex + i + 1) % this._model.length);
190 if (this._delegate.titleFor(item).toUpperCase().startsWith(letter)) {
191 this._list.selectItem(item);
192 break;
193 }
194 }
195 handled = true;
196 }
197 break;
198 }
199
Tim van der Lippe1d6e57a2019-09-30 11:55:34200 if (handled) {
Blink Reformat4c46d092018-04-07 15:32:37201 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34202 }
Blink Reformat4c46d092018-04-07 15:32:37203 }
204
205 /**
206 * @param {number} width
207 */
208 setWidth(width) {
209 this._width = width;
210 this._updateGlasspaneSize();
211 }
212
213 /**
214 * @param {number} rowHeight
215 */
216 setRowHeight(rowHeight) {
217 this._rowHeight = rowHeight;
218 }
219
220 /**
Hongchan Choi83648232019-05-04 01:35:46221 * @param {string} text
222 */
223 setPlaceholderText(text) {
224 this._placeholderText = text;
Tim van der Lippe1d6e57a2019-09-30 11:55:34225 if (!this._selectedItem) {
Hongchan Choi83648232019-05-04 01:35:46226 this._titleElement.textContent = this._placeholderText;
Tim van der Lippe1d6e57a2019-09-30 11:55:34227 }
Hongchan Choi83648232019-05-04 01:35:46228 }
229
230 /**
Blink Reformat4c46d092018-04-07 15:32:37231 * @param {!Common.Event} event
232 */
233 _itemsReplaced(event) {
234 const removed = /** @type {!Array<T>} */ (event.data.removed);
235 if (removed.indexOf(this._selectedItem) !== -1) {
236 this._selectedItem = null;
237 this._selectHighlightedItem();
238 }
239 this._updateGlasspaneSize();
240 }
241
242 /**
243 * @param {?T} item
244 */
245 selectItem(item) {
246 this._selectedItem = item;
Tim van der Lippe1d6e57a2019-09-30 11:55:34247 if (this._selectedItem) {
Blink Reformat4c46d092018-04-07 15:32:37248 this._titleElement.textContent = this._delegate.titleFor(this._selectedItem);
Tim van der Lippe1d6e57a2019-09-30 11:55:34249 } else {
Hongchan Choi83648232019-05-04 01:35:46250 this._titleElement.textContent = this._placeholderText;
Tim van der Lippe1d6e57a2019-09-30 11:55:34251 }
Blink Reformat4c46d092018-04-07 15:32:37252 this._delegate.itemSelected(this._selectedItem);
253 }
254
255 /**
256 * @override
257 * @param {T} item
258 * @return {!Element}
259 */
260 createElementForItem(item) {
261 const element = createElementWithClass('div', 'item');
262 element.addEventListener('mousemove', e => {
Tim van der Lippe1d6e57a2019-09-30 11:55:34263 if ((e.movementX || e.movementY) && this._delegate.isItemSelectable(item)) {
Blink Reformat4c46d092018-04-07 15:32:37264 this._list.selectItem(item, false, /* Don't scroll */ true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34265 }
Blink Reformat4c46d092018-04-07 15:32:37266 });
267 element.classList.toggle('disabled', !this._delegate.isItemSelectable(item));
268 element.classList.toggle('highlighted', this._list.selectedItem() === item);
269
John Emau8def92f2019-08-14 03:01:53270 UI.ARIAUtils.markAsMenuItem(element);
Blink Reformat4c46d092018-04-07 15:32:37271 element.appendChild(this._delegate.createElementForItem(item));
272
273 return element;
274 }
275
276 /**
277 * @override
278 * @param {T} item
279 * @return {number}
280 */
281 heightForItem(item) {
282 return this._rowHeight;
283 }
284
285 /**
286 * @override
287 * @param {T} item
288 * @return {boolean}
289 */
290 isItemSelectable(item) {
291 return this._delegate.isItemSelectable(item);
292 }
293
294 /**
295 * @override
296 * @param {?T} from
297 * @param {?T} to
298 * @param {?Element} fromElement
299 * @param {?Element} toElement
300 */
301 selectedItemChanged(from, to, fromElement, toElement) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34302 if (fromElement) {
Blink Reformat4c46d092018-04-07 15:32:37303 fromElement.classList.remove('highlighted');
Tim van der Lippe1d6e57a2019-09-30 11:55:34304 }
305 if (toElement) {
Blink Reformat4c46d092018-04-07 15:32:37306 toElement.classList.add('highlighted');
Tim van der Lippe1d6e57a2019-09-30 11:55:34307 }
John Emau8def92f2019-08-14 03:01:53308
309 UI.ARIAUtils.setActiveDescendant(this._list.element, toElement);
Blink Reformat4c46d092018-04-07 15:32:37310 this._delegate.highlightedItemChanged(
311 from, to, fromElement && fromElement.firstElementChild, toElement && toElement.firstElementChild);
312 }
313
Jack Lynch805641c2019-12-07 00:05:39314 /**
315 * @override
316 * @param {?Element} fromElement
317 * @param {?Element} toElement
318 * @return {boolean}
319 */
320 updateSelectedItemARIA(fromElement, toElement) {
321 return false;
322 }
323
Blink Reformat4c46d092018-04-07 15:32:37324 _selectHighlightedItem() {
325 this.selectItem(this._list.selectedItem());
326 }
327
328 /**
329 * @param {T} item
330 */
331 refreshItem(item) {
332 this._list.refreshItem(item);
333 }
Tim van der Lippe0830b3d2019-10-03 13:20:07334}
Blink Reformat4c46d092018-04-07 15:32:37335
336/**
337 * @interface
338 * @template T
339 */
Tim van der Lippe0830b3d2019-10-03 13:20:07340export class Delegate {
Blink Reformat4c46d092018-04-07 15:32:37341 /**
342 * @param {T} item
343 * @return {string}
344 */
345 titleFor(item) {
346 }
347
348 /**
349 * @param {T} item
350 * @return {!Element}
351 */
352 createElementForItem(item) {
353 }
354
355 /**
356 * @param {T} item
357 * @return {boolean}
358 */
359 isItemSelectable(item) {
360 }
361
362 /**
363 * @param {?T} item
364 */
365 itemSelected(item) {
366 }
367
368 /**
369 * @param {?T} from
370 * @param {?T} to
371 * @param {?Element} fromElement
372 * @param {?Element} toElement
373 */
374 highlightedItemChanged(from, to, fromElement, toElement) {
375 }
Tim van der Lippe0830b3d2019-10-03 13:20:07376}