blob: 9c768602eca04de23288547fb4b24621c556ae11 [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
Tim van der Lippeaa76aa22020-02-14 14:38:245import * as ARIAUtils from './ARIAUtils.js';
Paul Lewis9950e182019-12-16 16:06:076import {Size} from './Geometry.js';
7import {AnchorBehavior, GlassPane, MarginBehavior, PointerEventsBehavior} from './GlassPane.js';
8import {Icon} from './Icon.js';
9import {ListControl, ListDelegate, ListMode} from './ListControl.js'; // eslint-disable-line no-unused-vars
10import {Events as ListModelEvents, ListModel} from './ListModel.js'; // eslint-disable-line no-unused-vars
11import {appendStyle} from './utils/append-style.js';
12import {createShadowRootWithCoreStyles} from './utils/create-shadow-root-with-core-styles.js';
13
Blink Reformat4c46d092018-04-07 15:32:3714/**
15 * @template T
Paul Lewis9950e182019-12-16 16:06:0716 * @implements {ListDelegate<T>}
Blink Reformat4c46d092018-04-07 15:32:3717 */
Paul Lewis9950e182019-12-16 16:06:0718export class SoftDropDown {
Blink Reformat4c46d092018-04-07 15:32:3719 /**
Paul Lewis9950e182019-12-16 16:06:0720 * @param {!ListModel<T>} model
Tim van der Lippe0830b3d2019-10-03 13:20:0721 * @param {!Delegate<T>} delegate
Blink Reformat4c46d092018-04-07 15:32:3722 */
23 constructor(model, delegate) {
24 this._delegate = delegate;
25 this._selectedItem = null;
26 this._model = model;
27
Hongchan Choi83648232019-05-04 01:35:4628 this._placeholderText = ls`(no item selected)`;
29
Blink Reformat4c46d092018-04-07 15:32:3730 this.element = createElementWithClass('button', 'soft-dropdown');
Paul Lewis9950e182019-12-16 16:06:0731 appendStyle(this.element, 'ui/softDropDownButton.css');
Joel Einbinder7fbe24c2019-01-24 05:19:0132 this._titleElement = this.element.createChild('span', 'title');
Paul Lewis9950e182019-12-16 16:06:0733 const dropdownArrowIcon = Icon.create('smallicon-triangle-down');
Joel Einbinder7fbe24c2019-01-24 05:19:0134 this.element.appendChild(dropdownArrowIcon);
Tim van der Lippeaa76aa22020-02-14 14:38:2435 ARIAUtils.setExpanded(this.element, false);
Blink Reformat4c46d092018-04-07 15:32:3736
Paul Lewis9950e182019-12-16 16:06:0737 this._glassPane = new GlassPane();
38 this._glassPane.setMarginBehavior(MarginBehavior.NoMargin);
39 this._glassPane.setAnchorBehavior(AnchorBehavior.PreferBottom);
Blink Reformat4c46d092018-04-07 15:32:3740 this._glassPane.setOutsideClickCallback(this._hide.bind(this));
Paul Lewis9950e182019-12-16 16:06:0741 this._glassPane.setPointerEventsBehavior(PointerEventsBehavior.BlockedByGlassPane);
42 this._list = new ListControl(model, this, ListMode.EqualHeightItems);
Blink Reformat4c46d092018-04-07 15:32:3743 this._list.element.classList.add('item-list');
44 this._rowHeight = 36;
45 this._width = 315;
Paul Lewis9950e182019-12-16 16:06:0746 createShadowRootWithCoreStyles(this._glassPane.contentElement, 'ui/softDropDown.css')
John Emau2fe63512020-02-05 18:52:4747 .createChild('slot') // issue #972755
Blink Reformat4c46d092018-04-07 15:32:3748 .appendChild(this._list.element);
Tim van der Lippeaa76aa22020-02-14 14:38:2449 ARIAUtils.markAsMenu(this._list.element);
Blink Reformat4c46d092018-04-07 15:32:3750
51 this._listWasShowing200msAgo = false;
52 this.element.addEventListener('mousedown', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:3453 if (this._listWasShowing200msAgo) {
Blink Reformat4c46d092018-04-07 15:32:3754 this._hide(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:3455 } else if (!this.element.disabled) {
Blink Reformat4c46d092018-04-07 15:32:3756 this._show(event);
Tim van der Lippe1d6e57a2019-09-30 11:55:3457 }
Blink Reformat4c46d092018-04-07 15:32:3758 }, false);
John Emau8def92f2019-08-14 03:01:5359 this.element.addEventListener('keydown', this._onKeyDownButton.bind(this), false);
60 this._list.element.addEventListener('keydown', this._onKeyDownList.bind(this), false);
61 this._list.element.addEventListener('focusout', this._hide.bind(this), false);
Blink Reformat4c46d092018-04-07 15:32:3762 this._list.element.addEventListener('mousedown', event => event.consume(true), false);
63 this._list.element.addEventListener('mouseup', event => {
Tim van der Lippe1d6e57a2019-09-30 11:55:3464 if (event.target === this._list.element) {
Blink Reformat4c46d092018-04-07 15:32:3765 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3466 }
Blink Reformat4c46d092018-04-07 15:32:3767
Tim van der Lippe1d6e57a2019-09-30 11:55:3468 if (!this._listWasShowing200msAgo) {
Blink Reformat4c46d092018-04-07 15:32:3769 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3470 }
Blink Reformat4c46d092018-04-07 15:32:3771 this._selectHighlightedItem();
72 this._hide(event);
73 }, false);
Paul Lewis9950e182019-12-16 16:06:0774 model.addEventListener(ListModelEvents.ItemsReplaced, this._itemsReplaced, this);
Blink Reformat4c46d092018-04-07 15:32:3775 }
76
77 /**
78 * @param {!Event} event
79 */
80 _show(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:3481 if (this._glassPane.isShowing()) {
Blink Reformat4c46d092018-04-07 15:32:3782 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:3483 }
Blink Reformat4c46d092018-04-07 15:32:3784 this._glassPane.setContentAnchorBox(this.element.boxInWindow());
85 this._glassPane.show(/** @type {!Document} **/ (this.element.ownerDocument));
John Emau8def92f2019-08-14 03:01:5386 this._list.element.focus();
Tim van der Lippeaa76aa22020-02-14 14:38:2487 ARIAUtils.setExpanded(this.element, true);
Blink Reformat4c46d092018-04-07 15:32:3788 this._updateGlasspaneSize();
Tim van der Lippe1d6e57a2019-09-30 11:55:3489 if (this._selectedItem) {
Blink Reformat4c46d092018-04-07 15:32:3790 this._list.selectItem(this._selectedItem);
Tim van der Lippe1d6e57a2019-09-30 11:55:3491 }
Blink Reformat4c46d092018-04-07 15:32:3792 event.consume(true);
93 setTimeout(() => this._listWasShowing200msAgo = true, 200);
94 }
95
96 _updateGlasspaneSize() {
97 const maxHeight = this._rowHeight * (Math.min(this._model.length, 9));
Paul Lewis9950e182019-12-16 16:06:0798 this._glassPane.setMaxContentSize(new Size(this._width, maxHeight));
Blink Reformat4c46d092018-04-07 15:32:3799 this._list.viewportResized();
100 }
101
102 /**
103 * @param {!Event} event
104 */
105 _hide(event) {
106 setTimeout(() => this._listWasShowing200msAgo = false, 200);
107 this._glassPane.hide();
108 this._list.selectItem(null);
Tim van der Lippeaa76aa22020-02-14 14:38:24109 ARIAUtils.setExpanded(this.element, false);
John Emau8def92f2019-08-14 03:01:53110 this.element.focus();
Blink Reformat4c46d092018-04-07 15:32:37111 event.consume(true);
112 }
113
114 /**
115 * @param {!Event} event
116 */
John Emau8def92f2019-08-14 03:01:53117 _onKeyDownButton(event) {
118 let handled = false;
119 switch (event.key) {
120 case 'ArrowUp':
121 this._show(event);
122 this._list.selectItemNextPage();
123 handled = true;
124 break;
125 case 'ArrowDown':
126 this._show(event);
127 this._list.selectItemPreviousPage();
128 handled = true;
129 break;
130 case 'Enter':
131 case ' ':
132 this._show(event);
133 handled = true;
134 break;
135 default:
136 break;
137 }
138
Tim van der Lippe1d6e57a2019-09-30 11:55:34139 if (handled) {
John Emau8def92f2019-08-14 03:01:53140 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34141 }
John Emau8def92f2019-08-14 03:01:53142 }
143
144 /**
145 * @param {!Event} event
146 */
147 _onKeyDownList(event) {
Blink Reformat4c46d092018-04-07 15:32:37148 let handled = false;
149 switch (event.key) {
150 case 'ArrowLeft':
Blink Reformat4c46d092018-04-07 15:32:37151 handled = this._list.selectPreviousItem(false, false);
152 break;
153 case 'ArrowRight':
Blink Reformat4c46d092018-04-07 15:32:37154 handled = this._list.selectNextItem(false, false);
155 break;
Blink Reformat4c46d092018-04-07 15:32:37156 case 'Home':
157 for (let i = 0; i < this._model.length; i++) {
158 if (this.isItemSelectable(this._model.at(i))) {
159 this._list.selectItem(this._model.at(i));
160 handled = true;
161 break;
162 }
163 }
164 break;
165 case 'End':
166 for (let i = this._model.length - 1; i >= 0; i--) {
167 if (this.isItemSelectable(this._model.at(i))) {
168 this._list.selectItem(this._model.at(i));
169 handled = true;
170 break;
171 }
172 }
173 break;
174 case 'Escape':
175 this._hide(event);
John Emau8def92f2019-08-14 03:01:53176 handled = true;
Blink Reformat4c46d092018-04-07 15:32:37177 break;
178 case 'Tab':
Blink Reformat4c46d092018-04-07 15:32:37179 case 'Enter':
John Emau8def92f2019-08-14 03:01:53180 case ' ':
Blink Reformat4c46d092018-04-07 15:32:37181 this._selectHighlightedItem();
182 this._hide(event);
John Emau8def92f2019-08-14 03:01:53183 handled = true;
Blink Reformat4c46d092018-04-07 15:32:37184 break;
185 default:
186 if (event.key.length === 1) {
187 const selectedIndex = this._list.selectedIndex();
188 const letter = event.key.toUpperCase();
189 for (let i = 0; i < this._model.length; i++) {
190 const item = this._model.at((selectedIndex + i + 1) % this._model.length);
191 if (this._delegate.titleFor(item).toUpperCase().startsWith(letter)) {
192 this._list.selectItem(item);
193 break;
194 }
195 }
196 handled = true;
197 }
198 break;
199 }
200
Tim van der Lippe1d6e57a2019-09-30 11:55:34201 if (handled) {
Blink Reformat4c46d092018-04-07 15:32:37202 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34203 }
Blink Reformat4c46d092018-04-07 15:32:37204 }
205
206 /**
207 * @param {number} width
208 */
209 setWidth(width) {
210 this._width = width;
211 this._updateGlasspaneSize();
212 }
213
214 /**
215 * @param {number} rowHeight
216 */
217 setRowHeight(rowHeight) {
218 this._rowHeight = rowHeight;
219 }
220
221 /**
Hongchan Choi83648232019-05-04 01:35:46222 * @param {string} text
223 */
224 setPlaceholderText(text) {
225 this._placeholderText = text;
Tim van der Lippe1d6e57a2019-09-30 11:55:34226 if (!this._selectedItem) {
Hongchan Choi83648232019-05-04 01:35:46227 this._titleElement.textContent = this._placeholderText;
Tim van der Lippe1d6e57a2019-09-30 11:55:34228 }
Hongchan Choi83648232019-05-04 01:35:46229 }
230
231 /**
Blink Reformat4c46d092018-04-07 15:32:37232 * @param {!Common.Event} event
233 */
234 _itemsReplaced(event) {
235 const removed = /** @type {!Array<T>} */ (event.data.removed);
236 if (removed.indexOf(this._selectedItem) !== -1) {
237 this._selectedItem = null;
238 this._selectHighlightedItem();
239 }
240 this._updateGlasspaneSize();
241 }
242
243 /**
244 * @param {?T} item
245 */
246 selectItem(item) {
247 this._selectedItem = item;
Tim van der Lippe1d6e57a2019-09-30 11:55:34248 if (this._selectedItem) {
Blink Reformat4c46d092018-04-07 15:32:37249 this._titleElement.textContent = this._delegate.titleFor(this._selectedItem);
Tim van der Lippe1d6e57a2019-09-30 11:55:34250 } else {
Hongchan Choi83648232019-05-04 01:35:46251 this._titleElement.textContent = this._placeholderText;
Tim van der Lippe1d6e57a2019-09-30 11:55:34252 }
Blink Reformat4c46d092018-04-07 15:32:37253 this._delegate.itemSelected(this._selectedItem);
254 }
255
256 /**
257 * @override
258 * @param {T} item
259 * @return {!Element}
260 */
261 createElementForItem(item) {
262 const element = createElementWithClass('div', 'item');
263 element.addEventListener('mousemove', e => {
Tim van der Lippe1d6e57a2019-09-30 11:55:34264 if ((e.movementX || e.movementY) && this._delegate.isItemSelectable(item)) {
Blink Reformat4c46d092018-04-07 15:32:37265 this._list.selectItem(item, false, /* Don't scroll */ true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34266 }
Blink Reformat4c46d092018-04-07 15:32:37267 });
268 element.classList.toggle('disabled', !this._delegate.isItemSelectable(item));
269 element.classList.toggle('highlighted', this._list.selectedItem() === item);
270
Tim van der Lippeaa76aa22020-02-14 14:38:24271 ARIAUtils.markAsMenuItem(element);
Blink Reformat4c46d092018-04-07 15:32:37272 element.appendChild(this._delegate.createElementForItem(item));
273
274 return element;
275 }
276
277 /**
278 * @override
279 * @param {T} item
280 * @return {number}
281 */
282 heightForItem(item) {
283 return this._rowHeight;
284 }
285
286 /**
287 * @override
288 * @param {T} item
289 * @return {boolean}
290 */
291 isItemSelectable(item) {
292 return this._delegate.isItemSelectable(item);
293 }
294
295 /**
296 * @override
297 * @param {?T} from
298 * @param {?T} to
299 * @param {?Element} fromElement
300 * @param {?Element} toElement
301 */
302 selectedItemChanged(from, to, fromElement, toElement) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34303 if (fromElement) {
Blink Reformat4c46d092018-04-07 15:32:37304 fromElement.classList.remove('highlighted');
Tim van der Lippe1d6e57a2019-09-30 11:55:34305 }
306 if (toElement) {
Blink Reformat4c46d092018-04-07 15:32:37307 toElement.classList.add('highlighted');
Tim van der Lippe1d6e57a2019-09-30 11:55:34308 }
John Emau8def92f2019-08-14 03:01:53309
Tim van der Lippeaa76aa22020-02-14 14:38:24310 ARIAUtils.setActiveDescendant(this._list.element, toElement);
Blink Reformat4c46d092018-04-07 15:32:37311 this._delegate.highlightedItemChanged(
312 from, to, fromElement && fromElement.firstElementChild, toElement && toElement.firstElementChild);
313 }
314
Jack Lynch805641c2019-12-07 00:05:39315 /**
316 * @override
317 * @param {?Element} fromElement
318 * @param {?Element} toElement
319 * @return {boolean}
320 */
321 updateSelectedItemARIA(fromElement, toElement) {
322 return false;
323 }
324
Blink Reformat4c46d092018-04-07 15:32:37325 _selectHighlightedItem() {
326 this.selectItem(this._list.selectedItem());
327 }
328
329 /**
330 * @param {T} item
331 */
332 refreshItem(item) {
333 this._list.refreshItem(item);
334 }
Tim van der Lippe0830b3d2019-10-03 13:20:07335}
Blink Reformat4c46d092018-04-07 15:32:37336
337/**
338 * @interface
339 * @template T
340 */
Tim van der Lippe0830b3d2019-10-03 13:20:07341export class Delegate {
Blink Reformat4c46d092018-04-07 15:32:37342 /**
343 * @param {T} item
344 * @return {string}
345 */
346 titleFor(item) {
347 }
348
349 /**
350 * @param {T} item
351 * @return {!Element}
352 */
353 createElementForItem(item) {
354 }
355
356 /**
357 * @param {T} item
358 * @return {boolean}
359 */
360 isItemSelectable(item) {
361 }
362
363 /**
364 * @param {?T} item
365 */
366 itemSelected(item) {
367 }
368
369 /**
370 * @param {?T} from
371 * @param {?T} to
372 * @param {?Element} fromElement
373 * @param {?Element} toElement
374 */
375 highlightedItemChanged(from, to, fromElement, toElement) {
376 }
Tim van der Lippe0830b3d2019-10-03 13:20:07377}