@vaadin/vaadin-combo-box 21.0.1 → 22.0.0-alpha10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,498 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2021 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
7
- import '@polymer/iron-list/iron-list.js';
8
- import './vaadin-combo-box-item.js';
9
- import './vaadin-combo-box-dropdown.js';
10
- import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
11
-
12
- const TOUCH_DEVICE = (() => {
13
- try {
14
- document.createEvent('TouchEvent');
15
- return true;
16
- } catch (e) {
17
- return false;
18
- }
19
- })();
20
-
21
- /**
22
- * Element for internal use only.
23
- *
24
- * @extends HTMLElement
25
- * @private
26
- */
27
- class ComboBoxDropdownWrapperElement extends PolymerElement {
28
- static get template() {
29
- return html`
30
- <vaadin-combo-box-dropdown
31
- id="dropdown"
32
- hidden="[[_hidden(_items.*, loading)]]"
33
- position-target="[[positionTarget]]"
34
- on-position-changed="_setOverlayHeight"
35
- disable-upgrade=""
36
- theme="[[theme]]"
37
- >
38
- <template>
39
- <style>
40
- #scroller {
41
- overflow: auto;
42
-
43
- /* Fixes item background from getting on top of scrollbars on Safari */
44
- transform: translate3d(0, 0, 0);
45
-
46
- /* Enable momentum scrolling on iOS (iron-list v1.2+ no longer does it for us) */
47
- -webkit-overflow-scrolling: touch;
48
-
49
- /* Fixes scrollbar disappearing when 'Show scroll bars: Always' enabled in Safari */
50
- box-shadow: 0 0 0 white;
51
- }
52
- </style>
53
- <div id="scroller" on-click="_stopPropagation">
54
- <iron-list id="selector" role="listbox" items="[[_getItems(opened, _items)]]" scroll-target="[[_scroller]]">
55
- <template>
56
- <vaadin-combo-box-item
57
- on-click="_onItemClick"
58
- index="[[__requestItemByIndex(item, index, _resetScrolling)]]"
59
- item="[[item]]"
60
- label="[[getItemLabel(item, _itemLabelPath)]]"
61
- selected="[[_isItemSelected(item, _selectedItem, _itemIdPath)]]"
62
- renderer="[[renderer]]"
63
- role$="[[_getAriaRole(index)]]"
64
- aria-selected$="[[_getAriaSelected(_focusedIndex,index)]]"
65
- focused="[[_isItemFocused(_focusedIndex,index)]]"
66
- tabindex="-1"
67
- theme$="[[theme]]"
68
- ></vaadin-combo-box-item>
69
- </template>
70
- </iron-list>
71
- </div>
72
- </template>
73
- </vaadin-combo-box-dropdown>
74
- `;
75
- }
76
-
77
- static get is() {
78
- return 'vaadin-combo-box-dropdown-wrapper';
79
- }
80
-
81
- static get properties() {
82
- return {
83
- /**
84
- * True if the device supports touch events.
85
- */
86
- touchDevice: {
87
- type: Boolean,
88
- value: TOUCH_DEVICE
89
- },
90
-
91
- opened: Boolean,
92
-
93
- /**
94
- * The element to position/align the dropdown by.
95
- */
96
- positionTarget: {
97
- type: Object
98
- },
99
-
100
- /**
101
- * Custom function for rendering the content of the `<vaadin-combo-box-item>` propagated from the combo box element.
102
- */
103
- renderer: Function,
104
-
105
- /**
106
- * `true` when new items are being loaded.
107
- */
108
- loading: {
109
- type: Boolean,
110
- value: false,
111
- reflectToAttribute: true,
112
- observer: '_setOverlayHeight'
113
- },
114
-
115
- /**
116
- * Used to propagate the `theme` attribute from the host element.
117
- */
118
- theme: String,
119
-
120
- /**
121
- * Used to recognize if the filter changed, so to skip the
122
- * scrolling restore. If true, then scroll to 0 position. Restore
123
- * the previous position otherwise.
124
- */
125
- filterChanged: {
126
- type: Boolean,
127
- value: false
128
- },
129
-
130
- /**
131
- * Used to recognize scroller reset after new items have been set
132
- * to iron-list and to ignore unwanted pages load. If 'true', then
133
- * skip loading of the pages until it becomes 'false'.
134
- */
135
- _resetScrolling: {
136
- type: Boolean,
137
- value: false
138
- },
139
-
140
- _selectedItem: {
141
- type: Object
142
- },
143
-
144
- _items: {
145
- type: Object
146
- },
147
-
148
- _focusedIndex: {
149
- type: Number,
150
- value: -1,
151
- observer: '_focusedIndexChanged'
152
- },
153
-
154
- _focusedItem: {
155
- type: String,
156
- computed: '_getFocusedItem(_focusedIndex)'
157
- },
158
-
159
- _itemLabelPath: {
160
- type: String,
161
- value: 'label'
162
- },
163
-
164
- _itemValuePath: {
165
- type: String,
166
- value: 'value'
167
- },
168
-
169
- _selector: Object,
170
-
171
- _itemIdPath: String,
172
-
173
- /**
174
- * Stores the scroller position before updating the 'items', in
175
- * order to restore it immediately after 'items' have been updated
176
- */
177
- _oldScrollerPosition: {
178
- type: Number,
179
- value: 0
180
- }
181
- };
182
- }
183
-
184
- static get observers() {
185
- return ['_loadingChanged(loading)', '_openedChanged(opened, _items, loading)', '_restoreScrollerPosition(_items)'];
186
- }
187
-
188
- _fireTouchAction(sourceEvent) {
189
- this.dispatchEvent(
190
- new CustomEvent('vaadin-overlay-touch-action', {
191
- detail: { sourceEvent: sourceEvent }
192
- })
193
- );
194
- }
195
-
196
- _getItems(opened, items) {
197
- if (opened) {
198
- if (this._isNotEmpty(items) && this._selector && !this.filterChanged) {
199
- // iron-list triggers the scroller's reset after items update, and
200
- // this is not appropriate for undefined size lazy loading.
201
- // see https://github.com/vaadin/vaadin-combo-box-flow/issues/386
202
- // We store iron-list scrolling position in order to restore
203
- // it later on after the items have been updated.
204
- const currentScrollerPosition = this._selector.firstVisibleIndex;
205
- if (currentScrollerPosition !== 0) {
206
- this._oldScrollerPosition = currentScrollerPosition;
207
- this._resetScrolling = true;
208
- }
209
- }
210
- // Let the position to be restored in the future calls unless it's not
211
- // caused by filtering
212
- this.filterChanged = false;
213
- return items;
214
- }
215
- return [];
216
- }
217
-
218
- _restoreScrollerPosition(items) {
219
- if (this._isNotEmpty(items) && this._selector && this._oldScrollerPosition !== 0) {
220
- // new items size might be less than old scrolling position
221
- this._scrollIntoView(Math.min(items.length - 1, this._oldScrollerPosition));
222
- this._resetScrolling = false;
223
- // reset position to 0 again in order to properly handle the filter
224
- // cases (scroll to 0 after typing the filter)
225
- this._oldScrollerPosition = 0;
226
- }
227
- }
228
-
229
- _isNotEmpty(items) {
230
- return !this._isEmpty(items);
231
- }
232
-
233
- _isEmpty(items) {
234
- return !items || !items.length;
235
- }
236
-
237
- _openedChanged(opened, items, loading) {
238
- if (this.$.dropdown.hasAttribute('disable-upgrade')) {
239
- if (!opened) {
240
- return;
241
- } else {
242
- this._initDropdown();
243
- }
244
- }
245
-
246
- if (this._isEmpty(items)) {
247
- this.$.dropdown.__emptyItems = true;
248
- }
249
- this.$.dropdown.opened = !!(opened && (loading || this._isNotEmpty(items)));
250
- this.$.dropdown.__emptyItems = false;
251
- }
252
-
253
- _initDropdown() {
254
- this.$.dropdown.removeAttribute('disable-upgrade');
255
-
256
- this._selector = this.$.dropdown.$.overlay.content.querySelector('#selector');
257
- this._scroller = this.$.dropdown.$.overlay.content.querySelector('#scroller');
258
-
259
- this._patchWheelOverScrolling();
260
-
261
- this._loadingChanged(this.loading);
262
-
263
- this.$.dropdown.$.overlay.addEventListener('touchend', (e) => this._fireTouchAction(e));
264
- this.$.dropdown.$.overlay.addEventListener('touchmove', (e) => this._fireTouchAction(e));
265
-
266
- // Prevent blurring the input when clicking inside the overlay.
267
- this.$.dropdown.$.overlay.addEventListener('mousedown', (e) => e.preventDefault());
268
- }
269
-
270
- _loadingChanged(loading) {
271
- if (this.$.dropdown.hasAttribute('disable-upgrade')) {
272
- return;
273
- }
274
-
275
- if (loading) {
276
- this.$.dropdown.$.overlay.setAttribute('loading', '');
277
- } else {
278
- this.$.dropdown.$.overlay.removeAttribute('loading');
279
- }
280
- }
281
-
282
- _setOverlayHeight() {
283
- if (!this.opened || !this.positionTarget) {
284
- return;
285
- }
286
-
287
- const targetRect = this.positionTarget.getBoundingClientRect();
288
-
289
- this._scroller.style.maxHeight =
290
- getComputedStyle(this).getPropertyValue('--vaadin-combo-box-overlay-max-height') || '65vh';
291
-
292
- const maxHeight = this._maxOverlayHeight(targetRect);
293
-
294
- // overlay max height is restrained by the #scroller max height which is set to 65vh in CSS.
295
- this.$.dropdown.$.overlay.style.maxHeight = maxHeight;
296
-
297
- // we need to set height for iron-list to make its `firstVisibleIndex` work correctly.
298
- this._selector.style.maxHeight = maxHeight;
299
-
300
- this.updateViewportBoundaries();
301
- }
302
-
303
- _maxOverlayHeight(targetRect) {
304
- const margin = 8;
305
- const minHeight = 116; // Height of two items in combo-box
306
- if (this.$.dropdown.alignedAbove) {
307
- return Math.max(targetRect.top - margin + Math.min(document.body.scrollTop, 0), minHeight) + 'px';
308
- } else {
309
- return Math.max(document.documentElement.clientHeight - targetRect.bottom - margin, minHeight) + 'px';
310
- }
311
- }
312
-
313
- _getFocusedItem(focusedIndex) {
314
- if (focusedIndex >= 0) {
315
- return this._items[focusedIndex];
316
- }
317
- }
318
-
319
- _isItemSelected(item, selectedItem, itemIdPath) {
320
- if (item instanceof ComboBoxPlaceholder) {
321
- return false;
322
- } else if (itemIdPath && item !== undefined && selectedItem !== undefined) {
323
- return this.get(itemIdPath, item) === this.get(itemIdPath, selectedItem);
324
- } else {
325
- return item === selectedItem;
326
- }
327
- }
328
-
329
- _onItemClick(e) {
330
- this.dispatchEvent(new CustomEvent('selection-changed', { detail: { item: e.model.item } }));
331
- }
332
-
333
- /**
334
- * Gets the index of the item with the provided label.
335
- * @return {number}
336
- */
337
- indexOfLabel(label) {
338
- if (this._items && label) {
339
- for (let i = 0; i < this._items.length; i++) {
340
- if (this.getItemLabel(this._items[i]).toString().toLowerCase() === label.toString().toLowerCase()) {
341
- return i;
342
- }
343
- }
344
- }
345
-
346
- return -1;
347
- }
348
-
349
- /**
350
- * If dataProvider is used, dispatch a request for the item’s index if
351
- * the item is a placeholder object.
352
- *
353
- * @return {number}
354
- */
355
- __requestItemByIndex(item, index, resetScrolling) {
356
- if (item instanceof ComboBoxPlaceholder && index !== undefined && !resetScrolling) {
357
- this.dispatchEvent(
358
- new CustomEvent('index-requested', { detail: { index, currentScrollerPos: this._oldScrollerPosition } })
359
- );
360
- }
361
-
362
- return index;
363
- }
364
-
365
- /**
366
- * Gets the label string for the item based on the `_itemLabelPath`.
367
- * @return {string}
368
- */
369
- getItemLabel(item, itemLabelPath) {
370
- itemLabelPath = itemLabelPath || this._itemLabelPath;
371
- let label = item && itemLabelPath ? this.get(itemLabelPath, item) : undefined;
372
- if (label === undefined || label === null) {
373
- label = item ? item.toString() : '';
374
- }
375
- return label;
376
- }
377
-
378
- _isItemFocused(focusedIndex, itemIndex) {
379
- return focusedIndex == itemIndex;
380
- }
381
-
382
- _getAriaSelected(focusedIndex, itemIndex) {
383
- return this._isItemFocused(focusedIndex, itemIndex).toString();
384
- }
385
-
386
- _getAriaRole(itemIndex) {
387
- return itemIndex !== undefined ? 'option' : false;
388
- }
389
-
390
- _focusedIndexChanged(index) {
391
- if (index >= 0) {
392
- this._scrollIntoView(index);
393
- }
394
- }
395
-
396
- _scrollIntoView(index) {
397
- if (!(this.opened && index >= 0)) {
398
- return;
399
- }
400
- const visibleItemsCount = this._visibleItemsCount();
401
-
402
- let targetIndex = index;
403
-
404
- if (index > this._selector.lastVisibleIndex - 1) {
405
- // Index is below the bottom, scrolling down. Make the item appear at the bottom.
406
- // First scroll to target (will be at the top of the scroller) to make sure it's rendered.
407
- this._selector.scrollToIndex(index);
408
- // Then calculate the index for the following scroll (to get the target to bottom of the scroller).
409
- targetIndex = index - visibleItemsCount + 1;
410
- } else if (index > this._selector.firstVisibleIndex) {
411
- // The item is already visible, scrolling is unnecessary per se. But we need to trigger iron-list to set
412
- // the correct scrollTop on the scrollTarget. Scrolling to firstVisibleIndex.
413
- targetIndex = this._selector.firstVisibleIndex;
414
- }
415
- this._selector.scrollToIndex(Math.max(0, targetIndex));
416
-
417
- // Sometimes the item is partly below the bottom edge, detect and adjust.
418
- const pidx = this._selector._getPhysicalIndex(index),
419
- physicalItem = this._selector._physicalItems[pidx];
420
- if (!physicalItem) {
421
- return;
422
- }
423
- const physicalItemRect = physicalItem.getBoundingClientRect(),
424
- scrollerRect = this._scroller.getBoundingClientRect(),
425
- scrollTopAdjust = physicalItemRect.bottom - scrollerRect.bottom + this._viewportTotalPaddingBottom;
426
- if (scrollTopAdjust > 0) {
427
- this._scroller.scrollTop += scrollTopAdjust;
428
- }
429
- }
430
-
431
- ensureItemsRendered() {
432
- this._selector._render();
433
- }
434
-
435
- adjustScrollPosition() {
436
- if (this.opened && this._items) {
437
- this._scrollIntoView(this._focusedIndex);
438
- }
439
- }
440
-
441
- /**
442
- * We want to prevent the kinetic scrolling energy from being transferred from the overlay contents over to the parent.
443
- * Further improvement ideas: after the contents have been scrolled to the top or bottom and scrolling has stopped, it could allow
444
- * scrolling the parent similarly to touch scrolling.
445
- */
446
- _patchWheelOverScrolling() {
447
- const selector = this._selector;
448
- selector.addEventListener('wheel', (e) => {
449
- const scroller = selector._scroller || selector.scrollTarget;
450
- const scrolledToTop = scroller.scrollTop === 0;
451
- const scrolledToBottom = scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight <= 1;
452
-
453
- if (scrolledToTop && e.deltaY < 0) {
454
- e.preventDefault();
455
- } else if (scrolledToBottom && e.deltaY > 0) {
456
- e.preventDefault();
457
- }
458
- });
459
- }
460
-
461
- updateViewportBoundaries() {
462
- this._cachedViewportTotalPaddingBottom = undefined;
463
- this._selector.updateViewportBoundaries();
464
- }
465
-
466
- get _viewportTotalPaddingBottom() {
467
- if (this._cachedViewportTotalPaddingBottom === undefined) {
468
- const itemsStyle = window.getComputedStyle(this._selector.$.items);
469
- this._cachedViewportTotalPaddingBottom = [itemsStyle.paddingBottom, itemsStyle.borderBottomWidth]
470
- .map((v) => {
471
- return parseInt(v, 10);
472
- })
473
- .reduce((sum, v) => {
474
- return sum + v;
475
- });
476
- }
477
-
478
- return this._cachedViewportTotalPaddingBottom;
479
- }
480
-
481
- _visibleItemsCount() {
482
- // Ensure items are positioned
483
- this._selector.scrollToIndex(this._selector.firstVisibleIndex);
484
- // Ensure viewport boundaries are up-to-date
485
- this.updateViewportBoundaries();
486
- return this._selector.lastVisibleIndex - this._selector.firstVisibleIndex + 1;
487
- }
488
-
489
- _stopPropagation(e) {
490
- e.stopPropagation();
491
- }
492
-
493
- _hidden() {
494
- return !this.loading && this._isEmpty(this._items);
495
- }
496
- }
497
-
498
- customElements.define(ComboBoxDropdownWrapperElement.is, ComboBoxDropdownWrapperElement);