@vaadin/combo-box 23.1.0 → 23.2.0-dev.53560527d

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/combo-box",
3
- "version": "23.1.0",
3
+ "version": "23.2.0-dev.53560527d",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,10 +19,10 @@
19
19
  "main": "vaadin-combo-box.js",
20
20
  "type": "module",
21
21
  "files": [
22
+ "lit.d.ts",
23
+ "lit.js",
22
24
  "src",
23
25
  "theme",
24
- "lit.js",
25
- "lit.d.ts",
26
26
  "vaadin-*.d.ts",
27
27
  "vaadin-*.js"
28
28
  ],
@@ -36,23 +36,23 @@
36
36
  "dependencies": {
37
37
  "@open-wc/dedupe-mixin": "^1.3.0",
38
38
  "@polymer/polymer": "^3.0.0",
39
- "@vaadin/component-base": "^23.1.0",
40
- "@vaadin/field-base": "^23.1.0",
41
- "@vaadin/input-container": "^23.1.0",
42
- "@vaadin/item": "^23.1.0",
43
- "@vaadin/lit-renderer": "^23.1.0",
44
- "@vaadin/vaadin-lumo-styles": "^23.1.0",
45
- "@vaadin/vaadin-material-styles": "^23.1.0",
46
- "@vaadin/vaadin-overlay": "^23.1.0",
47
- "@vaadin/vaadin-themable-mixin": "^23.1.0"
39
+ "@vaadin/component-base": "23.2.0-dev.53560527d",
40
+ "@vaadin/field-base": "23.2.0-dev.53560527d",
41
+ "@vaadin/input-container": "23.2.0-dev.53560527d",
42
+ "@vaadin/item": "23.2.0-dev.53560527d",
43
+ "@vaadin/lit-renderer": "23.2.0-dev.53560527d",
44
+ "@vaadin/vaadin-lumo-styles": "23.2.0-dev.53560527d",
45
+ "@vaadin/vaadin-material-styles": "23.2.0-dev.53560527d",
46
+ "@vaadin/vaadin-overlay": "23.2.0-dev.53560527d",
47
+ "@vaadin/vaadin-themable-mixin": "23.2.0-dev.53560527d"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@esm-bundle/chai": "^4.3.4",
51
- "@vaadin/polymer-legacy-adapter": "^23.1.0",
51
+ "@vaadin/polymer-legacy-adapter": "23.2.0-dev.53560527d",
52
52
  "@vaadin/testing-helpers": "^0.3.2",
53
- "@vaadin/text-field": "^23.1.0",
53
+ "@vaadin/text-field": "23.2.0-dev.53560527d",
54
54
  "lit": "^2.0.0",
55
55
  "sinon": "^13.0.2"
56
56
  },
57
- "gitHead": "322bba42b83f908a78cd972b06acadc5da95a69d"
57
+ "gitHead": "6c5c18369b09e22e76365d8a8a5e4bbb220f969b"
58
58
  }
@@ -78,7 +78,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
78
78
  ready() {
79
79
  super.ready();
80
80
  this.clearCache();
81
- this.$.dropdown.addEventListener('index-requested', (e) => {
81
+ this._scroller.addEventListener('index-requested', (e) => {
82
82
  const index = e.detail.index;
83
83
  const currentScrollerPos = e.detail.currentScrollerPos;
84
84
  const allowedIndexRange = Math.floor(this.pageSize * 1.5);
@@ -177,47 +177,44 @@ export const ComboBoxDataProviderMixin = (superClass) =>
177
177
  /** @private */
178
178
  _loadPage(page) {
179
179
  // Make sure same page isn't requested multiple times.
180
- if (!this._pendingRequests[page] && this.dataProvider) {
181
- this.loading = true;
182
-
183
- const params = {
184
- page,
185
- pageSize: this.pageSize,
186
- filter: this.filter,
187
- };
188
-
189
- const callback = (items, size) => {
190
- if (this._pendingRequests[page] === callback) {
191
- if (!this.filteredItems) {
192
- const filteredItems = [];
193
- filteredItems.splice(params.page * params.pageSize, items.length, ...items);
194
- this.filteredItems = filteredItems;
195
- } else {
196
- this.splice('filteredItems', params.page * params.pageSize, items.length, ...items);
197
- }
198
- // Update selectedItem from filteredItems if value is set
199
- if (this._isValidValue(this.value) && this._getItemValue(this.selectedItem) !== this.value) {
200
- this._selectItemForValue(this.value);
201
- }
202
- if (!this.opened && !this.hasAttribute('focused')) {
203
- this._commitValue();
204
- }
205
- this.size = size;
206
-
207
- delete this._pendingRequests[page];
208
-
209
- if (Object.keys(this._pendingRequests).length === 0) {
210
- this.loading = false;
211
- }
212
- }
213
- };
180
+ if (this._pendingRequests[page] || !this.dataProvider) {
181
+ return;
182
+ }
183
+
184
+ const params = {
185
+ page,
186
+ pageSize: this.pageSize,
187
+ filter: this.filter,
188
+ };
214
189
 
215
- if (!this._pendingRequests[page]) {
216
- // Don't request page if it's already being requested
217
- this._pendingRequests[page] = callback;
218
- this.dataProvider(params, callback);
190
+ const callback = (items, size) => {
191
+ if (this._pendingRequests[page] !== callback) {
192
+ return;
219
193
  }
220
- }
194
+
195
+ const filteredItems = this.filteredItems ? [...this.filteredItems] : [];
196
+ filteredItems.splice(params.page * params.pageSize, items.length, ...items);
197
+ this.filteredItems = filteredItems;
198
+
199
+ if (!this.opened && !this.hasAttribute('focused')) {
200
+ this._commitValue();
201
+ }
202
+ this.size = size;
203
+
204
+ delete this._pendingRequests[page];
205
+
206
+ if (Object.keys(this._pendingRequests).length === 0) {
207
+ this.loading = false;
208
+ }
209
+ };
210
+
211
+ this._pendingRequests[page] = callback;
212
+ // Set the `loading` flag only after marking the request as pending
213
+ // to prevent the same page from getting requested multiple times
214
+ // as a result of `__loadingChanged` in the scroller which requests
215
+ // a virtualizer update which in turn may trigger a data provider page request.
216
+ this.loading = true;
217
+ this.dataProvider(params, callback);
221
218
  }
222
219
 
223
220
  /** @private */
@@ -287,7 +284,7 @@ export const ComboBoxDataProviderMixin = (superClass) =>
287
284
  /** @private */
288
285
  _warnDataProviderValue(dataProvider, value) {
289
286
  if (dataProvider && value !== '' && (this.selectedItem === undefined || this.selectedItem === null)) {
290
- const valueIndex = this._indexOfValue(value, this.filteredItems);
287
+ const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
291
288
  if (valueIndex < 0 || !this._getItemLabel(this.filteredItems[valueIndex])) {
292
289
  console.warn(
293
290
  'Warning: unable to determine the label for the provided `value`. ' +
@@ -3,7 +3,9 @@
3
3
  * Copyright (c) 2015 - 2022 Vaadin Ltd.
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
- import './vaadin-combo-box-dropdown.js';
6
+ import './vaadin-combo-box-item.js';
7
+ import './vaadin-combo-box-overlay.js';
8
+ import './vaadin-combo-box-scroller.js';
7
9
  import { dashToCamelCase } from '@polymer/polymer/lib/utils/case-map.js';
8
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
9
11
  import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
@@ -75,19 +77,16 @@ class ComboBoxLight extends ComboBoxDataProviderMixin(ComboBoxMixin(ThemableMixi
75
77
 
76
78
  <slot></slot>
77
79
 
78
- <vaadin-combo-box-dropdown
79
- id="dropdown"
80
- opened="[[opened]]"
80
+ <vaadin-combo-box-overlay
81
+ id="overlay"
82
+ hidden$="[[_isOverlayHidden(filteredItems, loading)]]"
83
+ opened="[[_overlayOpened]]"
84
+ loading$="[[loading]]"
85
+ theme$="[[_theme]]"
81
86
  position-target="[[inputElement]]"
82
- restore-focus-on-close="[[__restoreFocusOnClose]]"
87
+ no-vertical-overlap
83
88
  restore-focus-node="[[inputElement]]"
84
- renderer="[[renderer]]"
85
- _focused-index="[[_focusedIndex]]"
86
- _item-id-path="[[itemIdPath]]"
87
- _item-label-path="[[itemLabelPath]]"
88
- loading="[[loading]]"
89
- theme="[[_theme]]"
90
- ></vaadin-combo-box-dropdown>
89
+ ></vaadin-combo-box-overlay>
91
90
  `;
92
91
  }
93
92
 
@@ -147,6 +147,11 @@ export declare class ComboBoxMixinClass<TItem> {
147
147
 
148
148
  protected _inputElementValue: string | undefined;
149
149
 
150
+ /**
151
+ * Tag name prefix used by scroller and items.
152
+ */
153
+ protected readonly _tagNamePrefix: string;
154
+
150
155
  /**
151
156
  * Requests an update for the content of items.
152
157
  * While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
@@ -177,6 +182,4 @@ export declare class ComboBoxMixinClass<TItem> {
177
182
  checkValidity(): boolean;
178
183
 
179
184
  protected _revertInputValue(): void;
180
-
181
- protected _getItemElements(): HTMLElement[];
182
185
  }
@@ -12,6 +12,34 @@ import { InputMixin } from '@vaadin/field-base/src/input-mixin.js';
12
12
  import { VirtualKeyboardController } from '@vaadin/field-base/src/virtual-keyboard-controller.js';
13
13
  import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
14
14
 
15
+ /**
16
+ * Checks if the value is supported as an item value in this control.
17
+ *
18
+ * @param {unknown} value
19
+ * @return {boolean}
20
+ */
21
+ function isValidValue(value) {
22
+ return value !== undefined && value !== null;
23
+ }
24
+
25
+ /**
26
+ * Returns the index of the first item that satisfies the provided testing function
27
+ * ignoring placeholder items.
28
+ *
29
+ * @param {Array<ComboBoxItem | string>} items
30
+ * @param {Function} callback
31
+ * @return {number}
32
+ */
33
+ function findItemIndex(items, callback) {
34
+ return items.findIndex((item) => {
35
+ if (item instanceof ComboBoxPlaceholder) {
36
+ return false;
37
+ }
38
+
39
+ return callback(item);
40
+ });
41
+ }
42
+
15
43
  /**
16
44
  * @polymerMixin
17
45
  * @param {function(new:HTMLElement)} subclass
@@ -95,6 +123,7 @@ export const ComboBoxMixin = (subclass) =>
95
123
  */
96
124
  filteredItems: {
97
125
  type: Array,
126
+ observer: '_filteredItemsChanged',
98
127
  },
99
128
 
100
129
  /**
@@ -111,7 +140,6 @@ export const ComboBoxMixin = (subclass) =>
111
140
  type: Boolean,
112
141
  value: false,
113
142
  reflectToAttribute: true,
114
- observer: '_loadingChanged',
115
143
  },
116
144
 
117
145
  /**
@@ -196,16 +224,22 @@ export const ComboBoxMixin = (subclass) =>
196
224
  _closeOnBlurIsPrevented: Boolean,
197
225
 
198
226
  /** @private */
199
- __restoreFocusOnClose: Boolean,
227
+ _scroller: Object,
228
+
229
+ /** @private */
230
+ _overlayOpened: {
231
+ type: Boolean,
232
+ observer: '_overlayOpenedChanged',
233
+ },
200
234
  };
201
235
  }
202
236
 
203
237
  static get observers() {
204
238
  return [
205
239
  '_filterChanged(filter, itemValuePath, itemLabelPath)',
206
- '_itemsOrPathsChanged(items.*, itemValuePath, itemLabelPath)',
207
- '_filteredItemsChanged(filteredItems.*, itemValuePath, itemLabelPath)',
208
240
  '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
241
+ '_openedOrItemsChanged(opened, filteredItems, loading)',
242
+ '_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
209
243
  ];
210
244
  }
211
245
 
@@ -214,13 +248,20 @@ export const ComboBoxMixin = (subclass) =>
214
248
  this._boundOnFocusout = this._onFocusout.bind(this);
215
249
  this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
216
250
  this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
217
- this._boundClose = this.close.bind(this);
218
- this._boundOnOpened = this._onOpened.bind(this);
219
251
  this._boundOnClick = this._onClick.bind(this);
220
252
  this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
221
253
  this._boundOnTouchend = this._onTouchend.bind(this);
222
254
  }
223
255
 
256
+ /**
257
+ * Tag name prefix used by scroller and items.
258
+ * @protected
259
+ * @return {string}
260
+ */
261
+ get _tagNamePrefix() {
262
+ return 'vaadin-combo-box';
263
+ }
264
+
224
265
  /**
225
266
  * @return {string | undefined}
226
267
  * @protected
@@ -274,23 +315,19 @@ export const ComboBoxMixin = (subclass) =>
274
315
  ready() {
275
316
  super.ready();
276
317
 
318
+ this._initOverlay();
319
+ this._initScroller();
320
+
277
321
  this.addEventListener('focusout', this._boundOnFocusout);
278
322
 
279
323
  this._lastCommittedValue = this.value;
280
324
 
281
- this.$.dropdown.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
282
-
283
- this.addEventListener('vaadin-combo-box-dropdown-closed', this._boundClose);
284
- this.addEventListener('vaadin-combo-box-dropdown-opened', this._boundOnOpened);
285
325
  this.addEventListener('click', this._boundOnClick);
286
-
287
- this.$.dropdown.addEventListener('vaadin-overlay-touch-action', this._boundOnOverlayTouchAction);
288
-
289
326
  this.addEventListener('touchend', this._boundOnTouchend);
290
327
 
291
328
  const bringToFrontListener = () => {
292
329
  requestAnimationFrame(() => {
293
- this.$.dropdown.$.overlay.bringToFront();
330
+ this.$.overlay.bringToFront();
294
331
  });
295
332
  };
296
333
 
@@ -302,6 +339,14 @@ export const ComboBoxMixin = (subclass) =>
302
339
  this.addController(new VirtualKeyboardController(this));
303
340
  }
304
341
 
342
+ /** @protected */
343
+ disconnectedCallback() {
344
+ super.disconnectedCallback();
345
+
346
+ // Close the overlay on detach
347
+ this.close();
348
+ }
349
+
305
350
  /**
306
351
  * Requests an update for the content of items.
307
352
  * While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
@@ -309,11 +354,11 @@ export const ComboBoxMixin = (subclass) =>
309
354
  * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
310
355
  */
311
356
  requestContentUpdate() {
312
- if (!this.$.dropdown._scroller) {
357
+ if (!this._scroller) {
313
358
  return;
314
359
  }
315
360
 
316
- this.$.dropdown._scroller.requestContentUpdate();
361
+ this._scroller.requestContentUpdate();
317
362
 
318
363
  this._getItemElements().forEach((item) => {
319
364
  item.requestContentUpdate();
@@ -337,6 +382,104 @@ export const ComboBoxMixin = (subclass) =>
337
382
  this.opened = false;
338
383
  }
339
384
 
385
+ /** @private */
386
+ _initOverlay() {
387
+ const overlay = this.$.overlay;
388
+
389
+ // Store instance for detecting "dir" attribute on opening
390
+ overlay._comboBox = this;
391
+
392
+ overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
393
+ overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
394
+
395
+ // Prevent blurring the input when clicking inside the overlay
396
+ overlay.addEventListener('mousedown', (e) => e.preventDefault());
397
+
398
+ // Preventing the default modal behavior of the overlay on input click
399
+ overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
400
+ e.preventDefault();
401
+ });
402
+
403
+ // Manual two-way binding for the overlay "opened" property
404
+ overlay.addEventListener('opened-changed', (e) => {
405
+ this._overlayOpened = e.detail.value;
406
+ });
407
+ }
408
+
409
+ /**
410
+ * Create and initialize the scroller element.
411
+ * Override to provide custom host reference.
412
+ *
413
+ * @protected
414
+ */
415
+ _initScroller(host) {
416
+ const scrollerTag = `${this._tagNamePrefix}-scroller`;
417
+
418
+ const overlay = this.$.overlay;
419
+
420
+ overlay.renderer = (root) => {
421
+ if (!root.firstChild) {
422
+ root.appendChild(document.createElement(scrollerTag));
423
+ }
424
+ };
425
+
426
+ // Ensure the scroller is rendered
427
+ if (!this.opened) {
428
+ overlay.requestContentUpdate();
429
+ }
430
+
431
+ const scroller = overlay.querySelector(scrollerTag);
432
+
433
+ scroller.comboBox = host || this;
434
+ scroller.getItemLabel = this._getItemLabel.bind(this);
435
+ scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
436
+
437
+ // Trigger the observer to set properties
438
+ this._scroller = scroller;
439
+ }
440
+
441
+ /** @private */
442
+ // eslint-disable-next-line max-params
443
+ _updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
444
+ if (scroller) {
445
+ scroller.setProperties({
446
+ items: opened ? items : [],
447
+ opened,
448
+ loading,
449
+ selectedItem,
450
+ itemIdPath,
451
+ focusedIndex,
452
+ renderer,
453
+ theme,
454
+ });
455
+ }
456
+ }
457
+
458
+ /** @protected */
459
+ _isOverlayHidden(items, loading) {
460
+ return !loading && !(items && items.length);
461
+ }
462
+
463
+ /** @private */
464
+ _openedOrItemsChanged(opened, items, loading) {
465
+ // Close the overlay if there are no items to display.
466
+ // See https://github.com/vaadin/vaadin-combo-box/pull/964
467
+ this._overlayOpened = !!(opened && (loading || (items && items.length)));
468
+ }
469
+
470
+ /** @private */
471
+ _overlayOpenedChanged(opened, wasOpened) {
472
+ if (opened) {
473
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
474
+
475
+ this._onOpened();
476
+ } else if (wasOpened && this.filteredItems && this.filteredItems.length) {
477
+ this.close();
478
+
479
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
480
+ }
481
+ }
482
+
340
483
  /** @private */
341
484
  _focusedIndexChanged(index, oldIndex) {
342
485
  if (oldIndex === undefined) {
@@ -375,7 +518,7 @@ export const ComboBoxMixin = (subclass) =>
375
518
  this.focus();
376
519
  }
377
520
 
378
- this.__restoreFocusOnClose = true;
521
+ this.$.overlay.restoreFocusOnClose = true;
379
522
  } else {
380
523
  this._onClosed();
381
524
  if (this._openedWithFocusRing && this.hasAttribute('focused')) {
@@ -388,7 +531,7 @@ export const ComboBoxMixin = (subclass) =>
388
531
  input.setAttribute('aria-expanded', !!opened);
389
532
 
390
533
  if (opened) {
391
- input.setAttribute('aria-controls', this.$.dropdown.scrollerId);
534
+ input.setAttribute('aria-controls', this._scroller.id);
392
535
  } else {
393
536
  input.removeAttribute('aria-controls');
394
537
  }
@@ -478,7 +621,7 @@ export const ComboBoxMixin = (subclass) =>
478
621
  super._onKeyDown(e);
479
622
 
480
623
  if (e.key === 'Tab') {
481
- this.__restoreFocusOnClose = false;
624
+ this.$.overlay.restoreFocusOnClose = false;
482
625
  } else if (e.key === 'ArrowDown') {
483
626
  this._closeOnBlurIsPrevented = true;
484
627
  this._onArrowDown();
@@ -498,7 +641,11 @@ export const ComboBoxMixin = (subclass) =>
498
641
 
499
642
  /** @private */
500
643
  _getItemLabel(item) {
501
- return this.$.dropdown.getItemLabel(item);
644
+ let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
645
+ if (label === undefined || label === null) {
646
+ label = item ? item.toString() : '';
647
+ }
648
+ return label;
502
649
  }
503
650
 
504
651
  /** @private */
@@ -513,7 +660,7 @@ export const ComboBoxMixin = (subclass) =>
513
660
  /** @private */
514
661
  _onArrowDown() {
515
662
  if (this.opened) {
516
- const items = this._getOverlayItems();
663
+ const items = this.filteredItems;
517
664
  if (items) {
518
665
  this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
519
666
  this._prefillFocusedItemLabel();
@@ -529,7 +676,7 @@ export const ComboBoxMixin = (subclass) =>
529
676
  if (this._focusedIndex > -1) {
530
677
  this._focusedIndex = Math.max(0, this._focusedIndex - 1);
531
678
  } else {
532
- const items = this._getOverlayItems();
679
+ const items = this.filteredItems;
533
680
  if (items) {
534
681
  this._focusedIndex = items.length - 1;
535
682
  }
@@ -544,7 +691,8 @@ export const ComboBoxMixin = (subclass) =>
544
691
  /** @private */
545
692
  _prefillFocusedItemLabel() {
546
693
  if (this._focusedIndex > -1) {
547
- this._inputElementValue = this._getItemLabel(this.$.dropdown.focusedItem);
694
+ const focusedItem = this.filteredItems[this._focusedIndex];
695
+ this._inputElementValue = this._getItemLabel(focusedItem);
548
696
  this._markAllSelectionRange();
549
697
  }
550
698
  }
@@ -701,7 +849,11 @@ export const ComboBoxMixin = (subclass) =>
701
849
  _onOpened() {
702
850
  // Defer scroll position adjustment to improve performance.
703
851
  requestAnimationFrame(() => {
704
- this.$.dropdown.adjustScrollPosition();
852
+ // When opened is set as attribute, this logic needs to be delayed until scroller is created.
853
+ this._scroller.style.maxHeight =
854
+ getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
855
+
856
+ this._scrollIntoView(this._focusedIndex);
705
857
 
706
858
  // Set attribute after the items are rendered when overlay is opened for the first time.
707
859
  this._updateActiveDescendant(this._focusedIndex);
@@ -720,9 +872,8 @@ export const ComboBoxMixin = (subclass) =>
720
872
 
721
873
  /** @private */
722
874
  _commitValue() {
723
- const items = this._getOverlayItems();
724
- if (items && this._focusedIndex > -1) {
725
- const focusedItem = items[this._focusedIndex];
875
+ if (this._focusedIndex > -1) {
876
+ const focusedItem = this.filteredItems[this._focusedIndex];
726
877
  if (this.selectedItem !== focusedItem) {
727
878
  this.selectedItem = focusedItem;
728
879
  }
@@ -735,18 +886,14 @@ export const ComboBoxMixin = (subclass) =>
735
886
  this.value = '';
736
887
  }
737
888
  } else {
738
- const toLowerCase = (item) => item && item.toLowerCase && item.toLowerCase();
739
-
740
- // Try to find an item whose label matches the input value. A matching item is searched from
741
- // the filteredItems array (if available) and the selectedItem (if available).
742
- const itemMatchingByLabel = [...(this.filteredItems || []), this.selectedItem].find((item) => {
743
- return toLowerCase(this._getItemLabel(item)) === toLowerCase(this._inputElementValue);
744
- });
889
+ // Try to find an item which label matches the input value.
890
+ const items = [...(this.filteredItems || []), this.selectedItem];
891
+ const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
745
892
 
746
893
  if (
747
894
  this.allowCustomValue &&
748
895
  // To prevent a repetitive input value being saved after pressing ESC and Tab.
749
- !itemMatchingByLabel
896
+ !itemMatchingInputValue
750
897
  ) {
751
898
  const customValue = this._inputElementValue;
752
899
 
@@ -763,12 +910,11 @@ export const ComboBoxMixin = (subclass) =>
763
910
  });
764
911
  this.dispatchEvent(e);
765
912
  if (!e.defaultPrevented) {
766
- this._selectItemForValue(customValue);
767
913
  this.value = customValue;
768
914
  }
769
- } else if (!this.allowCustomValue && !this.opened && itemMatchingByLabel) {
915
+ } else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
770
916
  // An item matching by label was found, select it.
771
- this.value = this._getItemValue(itemMatchingByLabel);
917
+ this.value = this._getItemValue(itemMatchingInputValue);
772
918
  } else {
773
919
  // Revert the input value
774
920
  this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
@@ -834,13 +980,15 @@ export const ComboBoxMixin = (subclass) =>
834
980
  }
835
981
 
836
982
  /** @private */
837
- _filterChanged(filter, itemValuePath, itemLabelPath) {
983
+ _filterChanged(filter, _itemValuePath, _itemLabelPath) {
838
984
  if (filter === undefined) {
839
985
  return;
840
986
  }
841
987
 
842
988
  // Scroll to the top of the list whenever the filter changes.
843
- this.$.dropdown._scrollIntoView(0);
989
+ this._scrollIntoView(0);
990
+
991
+ this._focusedIndex = -1;
844
992
 
845
993
  if (this.items) {
846
994
  this.filteredItems = this._filterItems(this.items, filter);
@@ -848,14 +996,7 @@ export const ComboBoxMixin = (subclass) =>
848
996
  // With certain use cases (e. g., external filtering), `items` are
849
997
  // undefined. Filtering is unnecessary per se, but the filteredItems
850
998
  // observer should still be invoked to update focused item.
851
- this._filteredItemsChanged({ path: 'filteredItems', value: this.filteredItems }, itemValuePath, itemLabelPath);
852
- }
853
- }
854
-
855
- /** @private */
856
- _loadingChanged(loading) {
857
- if (loading) {
858
- this._focusedIndex = -1;
999
+ this._filteredItemsChanged(this.filteredItems);
859
1000
  }
860
1001
  }
861
1002
 
@@ -904,9 +1045,7 @@ export const ComboBoxMixin = (subclass) =>
904
1045
  this._inputElementValue = this._getItemLabel(selectedItem);
905
1046
  }
906
1047
 
907
- this.$.dropdown._selectedItem = selectedItem;
908
- const items = this._getOverlayItems();
909
- if (this.filteredItems && items) {
1048
+ if (this.filteredItems) {
910
1049
  this._focusedIndex = this.filteredItems.indexOf(selectedItem);
911
1050
  }
912
1051
  }
@@ -923,7 +1062,7 @@ export const ComboBoxMixin = (subclass) =>
923
1062
  return;
924
1063
  }
925
1064
 
926
- if (this._isValidValue(value)) {
1065
+ if (isValidValue(value)) {
927
1066
  let item;
928
1067
  if (this._getItemValue(this.selectedItem) !== value) {
929
1068
  this._selectItemForValue(value);
@@ -956,53 +1095,53 @@ export const ComboBoxMixin = (subclass) =>
956
1095
  this._ensureItemsOrDataProvider(() => {
957
1096
  this.items = oldItems;
958
1097
  });
1098
+
1099
+ if (items) {
1100
+ this.filteredItems = items.slice(0);
1101
+ } else if (oldItems) {
1102
+ // Only clear filteredItems if the component had items previously but got cleared
1103
+ this.filteredItems = null;
1104
+ }
959
1105
  }
960
1106
 
961
1107
  /** @private */
962
- _itemsOrPathsChanged(e) {
963
- if (e.path === 'items' || e.path === 'items.splices') {
964
- if (this.items) {
965
- this.filteredItems = this.items.slice(0);
966
- } else if (this.__previousItems) {
967
- // Only clear filteredItems if the component had items previously but got cleared
968
- this.filteredItems = null;
969
- }
970
-
971
- const valueIndex = this._indexOfValue(this.value, this.items);
972
- this._focusedIndex = valueIndex;
1108
+ _filteredItemsChanged(filteredItems, oldFilteredItems) {
1109
+ // Store the currently focused item if any. The focused index preserves
1110
+ // in the case when more filtered items are loading but it is reset
1111
+ // when the user types in a filter query.
1112
+ const focusedItem = oldFilteredItems ? oldFilteredItems[this._focusedIndex] : null;
1113
+
1114
+ // Try to sync `selectedItem` based on `value` once a new set of `filteredItems` is available
1115
+ // (as a result of external filtering or when they have been loaded by the data provider).
1116
+ // When `value` is specified but `selectedItem` is not, it means that there was no item
1117
+ // matching `value` at the moment `value` was set, so `selectedItem` has remained unsynced.
1118
+ const valueIndex = this.__getItemIndexByValue(filteredItems, this.value);
1119
+ if ((this.selectedItem === null || this.selectedItem === undefined) && valueIndex >= 0) {
1120
+ this.selectedItem = filteredItems[valueIndex];
1121
+ }
973
1122
 
974
- const item = valueIndex > -1 && this.items[valueIndex];
975
- if (item) {
976
- this.selectedItem = item;
977
- }
1123
+ // Try to first set focus on the item that had been focused before `filteredItems` were updated
1124
+ // if it is still present in the `filteredItems` array. Otherwise, set the focused index
1125
+ // depending on the selected item or the filter query.
1126
+ const focusedItemIndex = this.__getItemIndexByValue(filteredItems, this._getItemValue(focusedItem));
1127
+ if (focusedItemIndex > -1) {
1128
+ this._focusedIndex = focusedItemIndex;
1129
+ } else {
1130
+ this.__setInitialFocusedIndex();
978
1131
  }
979
- this.__previousItems = e.value;
980
1132
  }
981
1133
 
982
1134
  /** @private */
983
- _filteredItemsChanged(e) {
984
- if (e.path === 'filteredItems' || e.path === 'filteredItems.splices') {
985
- this._setOverlayItems(this.filteredItems);
986
-
987
- // When the external filtering is used and `value` was provided before `filteredItems`,
988
- // initialize the selected item with the current value here. This will also cause
989
- // the input element value to sync. In other cases, the selected item is already initialized
990
- // in other observers such as `valueChanged`, `_itemsOrPathsChanged`.
991
- const valueIndex = this._indexOfValue(this.value, this.filteredItems);
992
- if (this.selectedItem === null && valueIndex >= 0) {
993
- this._selectItemForValue(this.value);
994
- }
995
-
996
- const inputValue = this._inputElementValue;
997
- if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
998
- // When the input element value is the same as the current value or not defined,
999
- // set the focused index to the item that matches the value.
1000
- this._focusedIndex = this.$.dropdown.indexOfLabel(this._getItemLabel(this.selectedItem));
1001
- } else {
1002
- // When the user filled in something that is different from the current value = filtering is enabled,
1003
- // set the focused index to the item that matches the filter query.
1004
- this._focusedIndex = this.$.dropdown.indexOfLabel(this.filter);
1005
- }
1135
+ __setInitialFocusedIndex() {
1136
+ const inputValue = this._inputElementValue;
1137
+ if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
1138
+ // When the input element value is the same as the current value or not defined,
1139
+ // set the focused index to the item that matches the value.
1140
+ this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this._getItemLabel(this.selectedItem));
1141
+ } else {
1142
+ // When the user filled in something that is different from the current value = filtering is enabled,
1143
+ // set the focused index to the item that matches the filter query.
1144
+ this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this.filter);
1006
1145
  }
1007
1146
  }
1008
1147
 
@@ -1023,7 +1162,7 @@ export const ComboBoxMixin = (subclass) =>
1023
1162
 
1024
1163
  /** @private */
1025
1164
  _selectItemForValue(value) {
1026
- const valueIndex = this._indexOfValue(value, this.filteredItems);
1165
+ const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
1027
1166
  const previouslySelectedItem = this.selectedItem;
1028
1167
 
1029
1168
  if (valueIndex >= 0) {
@@ -1039,42 +1178,48 @@ export const ComboBoxMixin = (subclass) =>
1039
1178
  }
1040
1179
  }
1041
1180
 
1042
- /** @protected */
1043
- _getItemElements() {
1044
- return Array.from(this.$.dropdown._scroller.querySelectorAll('vaadin-combo-box-item'));
1045
- }
1046
-
1047
1181
  /** @private */
1048
- _getOverlayItems() {
1049
- return this.$.dropdown._items;
1182
+ _getItemElements() {
1183
+ return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
1050
1184
  }
1051
1185
 
1052
1186
  /** @private */
1053
- _setOverlayItems(items) {
1054
- this.$.dropdown.set('_items', items);
1187
+ _scrollIntoView(index) {
1188
+ if (!this._scroller) {
1189
+ return;
1190
+ }
1191
+ this._scroller.scrollIntoView(index);
1055
1192
  }
1056
1193
 
1057
- /** @private */
1058
- _indexOfValue(value, items) {
1059
- if (!items || !this._isValidValue(value)) {
1194
+ /**
1195
+ * Returns the first item that matches the provided value.
1196
+ *
1197
+ * @private
1198
+ */
1199
+ __getItemIndexByValue(items, value) {
1200
+ if (!items || !isValidValue(value)) {
1060
1201
  return -1;
1061
1202
  }
1062
1203
 
1063
- return items.findIndex((item) => {
1064
- if (item instanceof ComboBoxPlaceholder) {
1065
- return false;
1066
- }
1067
-
1204
+ return findItemIndex(items, (item) => {
1068
1205
  return this._getItemValue(item) === value;
1069
1206
  });
1070
1207
  }
1071
1208
 
1072
1209
  /**
1073
- * Checks if the value is supported as an item value in this control.
1210
+ * Returns the first item that matches the provided label.
1211
+ * Labels are matched against each other case insensitively.
1212
+ *
1074
1213
  * @private
1075
1214
  */
1076
- _isValidValue(value) {
1077
- return value !== undefined && value !== null;
1215
+ __getItemIndexByLabel(items, label) {
1216
+ if (!items || !label) {
1217
+ return -1;
1218
+ }
1219
+
1220
+ return findItemIndex(items, (item) => {
1221
+ return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
1222
+ });
1078
1223
  }
1079
1224
 
1080
1225
  /** @private */
@@ -1105,7 +1250,7 @@ export const ComboBoxMixin = (subclass) =>
1105
1250
  /** @private */
1106
1251
  _onFocusout(event) {
1107
1252
  // Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
1108
- if (event.relatedTarget === this.$.dropdown.$.overlay) {
1253
+ if (event.relatedTarget === this.$.overlay) {
1109
1254
  event.composedPath()[0].focus();
1110
1255
  return;
1111
1256
  }
@@ -1181,4 +1326,16 @@ export const ComboBoxMixin = (subclass) =>
1181
1326
  * To comply with https://developer.mozilla.org/en-US/docs/Web/Events/change
1182
1327
  * @event change
1183
1328
  */
1329
+
1330
+ /**
1331
+ * Fired after the `vaadin-combo-box-overlay` opens.
1332
+ *
1333
+ * @event vaadin-combo-box-dropdown-opened
1334
+ */
1335
+
1336
+ /**
1337
+ * Fired after the `vaadin-combo-box-overlay` closes.
1338
+ *
1339
+ * @event vaadin-combo-box-dropdown-closed
1340
+ */
1184
1341
  };
@@ -52,9 +52,7 @@ export class ComboBoxOverlay extends PositionMixin(OverlayElement) {
52
52
  connectedCallback() {
53
53
  super.connectedCallback();
54
54
 
55
- const dropdown = this.__dataHost;
56
- const comboBox = dropdown && dropdown.getRootNode().host;
57
- this._comboBox = comboBox;
55
+ const comboBox = this._comboBox;
58
56
 
59
57
  const hostDir = comboBox && comboBox.getAttribute('dir');
60
58
  if (hostDir) {
@@ -4,6 +4,7 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
7
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
7
8
  import { Virtualizer } from '@vaadin/component-base/src/virtualizer.js';
8
9
  import { ComboBoxPlaceholder } from './vaadin-combo-box-placeholder.js';
9
10
 
@@ -90,6 +91,7 @@ export class ComboBoxScroller extends PolymerElement {
90
91
  */
91
92
  selectedItem: {
92
93
  type: Object,
94
+ observer: '__selectedItemChanged',
93
95
  },
94
96
 
95
97
  /**
@@ -145,6 +147,9 @@ export class ComboBoxScroller extends PolymerElement {
145
147
  ready() {
146
148
  super.ready();
147
149
 
150
+ // Ensure every instance has unique ID
151
+ this.id = `${this.localName}-${generateUniqueId()}`;
152
+
148
153
  // Allow extensions to customize tag name for the items
149
154
  this.__hostTagName = this.constructor.is.replace('-scroller', '');
150
155
 
@@ -218,7 +223,7 @@ export class ComboBoxScroller extends PolymerElement {
218
223
 
219
224
  /** @private */
220
225
  __isItemFocused(focusedIndex, itemIndex) {
221
- return focusedIndex === itemIndex;
226
+ return !this.loading && focusedIndex === itemIndex;
222
227
  }
223
228
 
224
229
  /** @private */
@@ -243,12 +248,19 @@ export class ComboBoxScroller extends PolymerElement {
243
248
  }
244
249
 
245
250
  /** @private */
246
- __loadingChanged(loading) {
247
- if (this.__virtualizer && !loading) {
251
+ __loadingChanged() {
252
+ if (this.__virtualizer) {
248
253
  setTimeout(() => this.requestContentUpdate());
249
254
  }
250
255
  }
251
256
 
257
+ /** @private */
258
+ __selectedItemChanged() {
259
+ if (this.__virtualizer) {
260
+ this.requestContentUpdate();
261
+ }
262
+ }
263
+
252
264
  /** @private */
253
265
  __focusedIndexChanged(index, oldIndex) {
254
266
  if (!this.__virtualizer) {
@@ -4,7 +4,9 @@
4
4
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
5
  */
6
6
  import '@vaadin/input-container/src/vaadin-input-container.js';
7
- import './vaadin-combo-box-dropdown.js';
7
+ import './vaadin-combo-box-item.js';
8
+ import './vaadin-combo-box-overlay.js';
9
+ import './vaadin-combo-box-scroller.js';
8
10
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
9
11
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
10
12
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
@@ -196,19 +198,16 @@ class ComboBox extends ComboBoxDataProviderMixin(
196
198
  </div>
197
199
  </div>
198
200
 
199
- <vaadin-combo-box-dropdown
200
- id="dropdown"
201
- opened="[[opened]]"
202
- renderer="[[renderer]]"
201
+ <vaadin-combo-box-overlay
202
+ id="overlay"
203
+ hidden$="[[_isOverlayHidden(filteredItems, loading)]]"
204
+ opened="[[_overlayOpened]]"
205
+ loading$="[[loading]]"
206
+ theme$="[[_theme]]"
203
207
  position-target="[[_positionTarget]]"
204
- restore-focus-on-close="[[__restoreFocusOnClose]]"
208
+ no-vertical-overlap
205
209
  restore-focus-node="[[inputElement]]"
206
- _focused-index="[[_focusedIndex]]"
207
- _item-id-path="[[itemIdPath]]"
208
- _item-label-path="[[itemLabelPath]]"
209
- loading="[[loading]]"
210
- theme="[[_theme]]"
211
- ></vaadin-combo-box-dropdown>
210
+ ></vaadin-combo-box-overlay>
212
211
  `;
213
212
  }
214
213
 
@@ -273,7 +272,7 @@ class ComboBox extends ComboBoxDataProviderMixin(
273
272
  */
274
273
  _shouldRemoveFocus(event) {
275
274
  // Do not blur when focus moves to the overlay
276
- if (event.relatedTarget === this.$.dropdown.$.overlay) {
275
+ if (event.relatedTarget === this.$.overlay) {
277
276
  event.composedPath()[0].focus();
278
277
  return false;
279
278
  }
@@ -1,287 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright (c) 2015 - 2022 Vaadin Ltd.
4
- * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
- */
6
- import './vaadin-combo-box-item.js';
7
- import './vaadin-combo-box-overlay.js';
8
- import './vaadin-combo-box-scroller.js';
9
- import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
10
-
11
- /**
12
- * Element for internal use only.
13
- *
14
- * @extends HTMLElement
15
- * @private
16
- */
17
- export class ComboBoxDropdown extends PolymerElement {
18
- static get is() {
19
- return 'vaadin-combo-box-dropdown';
20
- }
21
-
22
- static get template() {
23
- return html`
24
- <vaadin-combo-box-overlay
25
- id="overlay"
26
- hidden$="[[_isOverlayHidden(_items.*, loading)]]"
27
- loading$="[[loading]]"
28
- opened="{{_overlayOpened}}"
29
- theme$="[[theme]]"
30
- position-target="[[positionTarget]]"
31
- no-vertical-overlap
32
- restore-focus-on-close="[[restoreFocusOnClose]]"
33
- restore-focus-node="[[restoreFocusNode]]"
34
- ></vaadin-combo-box-overlay>
35
- `;
36
- }
37
-
38
- static get properties() {
39
- return {
40
- /**
41
- * True if the combo-box has been activate by the user.
42
- * The actual opened state depends on whether the dropdown has items.
43
- */
44
- opened: Boolean,
45
-
46
- /**
47
- * The element to position/align the dropdown by.
48
- */
49
- positionTarget: {
50
- type: Object,
51
- },
52
-
53
- /**
54
- * Custom function for rendering the content of the `<vaadin-combo-box-item>` propagated from the combo box element.
55
- */
56
- renderer: Function,
57
-
58
- /**
59
- * `true` when new items are being loaded.
60
- */
61
- loading: {
62
- type: Boolean,
63
- value: false,
64
- reflectToAttribute: true,
65
- },
66
-
67
- /**
68
- * Used to propagate the `theme` attribute from the host element.
69
- */
70
- theme: String,
71
-
72
- _selectedItem: {
73
- type: Object,
74
- },
75
-
76
- _items: {
77
- type: Array,
78
- },
79
-
80
- _focusedIndex: {
81
- type: Number,
82
- value: -1,
83
- },
84
-
85
- focusedItem: {
86
- type: String,
87
- computed: '_getFocusedItem(_focusedIndex)',
88
- },
89
-
90
- _itemLabelPath: {
91
- type: String,
92
- value: 'label',
93
- },
94
-
95
- _itemValuePath: {
96
- type: String,
97
- value: 'value',
98
- },
99
-
100
- _scroller: Object,
101
-
102
- _itemIdPath: String,
103
-
104
- _overlayOpened: {
105
- type: Boolean,
106
- observer: '_openedChanged',
107
- },
108
- };
109
- }
110
-
111
- static get observers() {
112
- return [
113
- '_openedOrItemsChanged(opened, _items, loading)',
114
- '__updateScroller(_scroller, _items, opened, loading, _selectedItem, _itemIdPath, _focusedIndex, renderer, theme)',
115
- ];
116
- }
117
-
118
- constructor() {
119
- super();
120
-
121
- // Ensure every instance has unique ID
122
- const uniqueId = (ComboBoxDropdown._uniqueId = 1 + ComboBoxDropdown._uniqueId || 0);
123
- this.scrollerId = `${this.localName}-scroller-${uniqueId}`;
124
- }
125
-
126
- ready() {
127
- super.ready();
128
-
129
- // Allow extensions to customize tag name for the items
130
- this.__hostTagName = this.constructor.is.replace('-dropdown', '');
131
-
132
- const overlay = this.$.overlay;
133
- const scrollerTag = `${this.__hostTagName}-scroller`;
134
-
135
- overlay.renderer = (root) => {
136
- if (!root.firstChild) {
137
- const scroller = document.createElement(scrollerTag);
138
- root.appendChild(scroller);
139
- }
140
- };
141
-
142
- // Ensure the scroller is rendered
143
- overlay.requestContentUpdate();
144
-
145
- this._scroller = overlay.content.querySelector(scrollerTag);
146
- this._scroller.id = this.scrollerId;
147
-
148
- this._scroller.getItemLabel = this.getItemLabel.bind(this);
149
- this._scroller.comboBox = this.getRootNode().host;
150
-
151
- this._scroller.addEventListener('selection-changed', (e) => this._forwardScrollerEvent(e));
152
- this._scroller.addEventListener('index-requested', (e) => this._forwardScrollerEvent(e));
153
-
154
- overlay.addEventListener('touchend', (e) => this._fireTouchAction(e));
155
- overlay.addEventListener('touchmove', (e) => this._fireTouchAction(e));
156
-
157
- // Prevent blurring the input when clicking inside the overlay.
158
- overlay.addEventListener('mousedown', (e) => e.preventDefault());
159
-
160
- // Preventing the default modal behaviour of the overlay on input clicking
161
- overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
162
- e.preventDefault();
163
- });
164
- }
165
-
166
- disconnectedCallback() {
167
- super.disconnectedCallback();
168
-
169
- // Making sure the overlay is closed and removed from DOM after detaching the dropdown.
170
- this._overlayOpened = false;
171
- }
172
-
173
- _fireTouchAction(sourceEvent) {
174
- this.dispatchEvent(
175
- new CustomEvent('vaadin-overlay-touch-action', {
176
- detail: { sourceEvent },
177
- }),
178
- );
179
- }
180
-
181
- _forwardScrollerEvent(event) {
182
- this.dispatchEvent(new CustomEvent(event.type, { detail: event.detail }));
183
- }
184
-
185
- _openedChanged(opened, wasOpened) {
186
- if (opened) {
187
- this._scroller.style.maxHeight =
188
- getComputedStyle(this).getPropertyValue(`--${this.__hostTagName}-overlay-max-height`) || '65vh';
189
-
190
- this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
191
- } else if (wasOpened && !this.__emptyItems) {
192
- this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
193
- }
194
- }
195
-
196
- _openedOrItemsChanged(opened, items, loading) {
197
- // See https://github.com/vaadin/vaadin-combo-box/pull/964
198
- const hasItems = items && items.length;
199
- if (!hasItems) {
200
- this.__emptyItems = true;
201
- }
202
- this._overlayOpened = !!(opened && (loading || hasItems));
203
- this.__emptyItems = false;
204
- }
205
-
206
- _getFocusedItem(focusedIndex) {
207
- if (focusedIndex >= 0) {
208
- return this._items[focusedIndex];
209
- }
210
- }
211
-
212
- /**
213
- * Gets the index of the item with the provided label.
214
- * @return {number}
215
- */
216
- indexOfLabel(label) {
217
- if (this._items && label) {
218
- for (let i = 0; i < this._items.length; i++) {
219
- if (this.getItemLabel(this._items[i]).toString().toLowerCase() === label.toString().toLowerCase()) {
220
- return i;
221
- }
222
- }
223
- }
224
-
225
- return -1;
226
- }
227
-
228
- /**
229
- * Gets the label string for the item based on the `_itemLabelPath`.
230
- * @return {string}
231
- */
232
- getItemLabel(item, itemLabelPath) {
233
- itemLabelPath = itemLabelPath || this._itemLabelPath;
234
- let label = item && itemLabelPath ? this.get(itemLabelPath, item) : undefined;
235
- if (label === undefined || label === null) {
236
- label = item ? item.toString() : '';
237
- }
238
- return label;
239
- }
240
-
241
- _scrollIntoView(index) {
242
- if (!this._scroller) {
243
- return;
244
- }
245
- this._scroller.scrollIntoView(index);
246
- }
247
-
248
- adjustScrollPosition() {
249
- if (this.opened && this._items) {
250
- this._scrollIntoView(this._focusedIndex);
251
- }
252
- }
253
-
254
- // eslint-disable-next-line max-params
255
- __updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
256
- if (scroller) {
257
- scroller.setProperties({
258
- items: opened ? items : [],
259
- opened,
260
- loading,
261
- selectedItem,
262
- itemIdPath,
263
- focusedIndex,
264
- renderer,
265
- theme,
266
- });
267
- }
268
- }
269
-
270
- _isOverlayHidden() {
271
- return !this.loading && !(this._items && this._items.length);
272
- }
273
-
274
- /**
275
- * Fired after the `vaadin-combo-box-dropdown` opens.
276
- *
277
- * @event vaadin-combo-box-dropdown-opened
278
- */
279
-
280
- /**
281
- * Fired after the `vaadin-combo-box-dropdown` closes.
282
- *
283
- * @event vaadin-combo-box-dropdown-closed
284
- */
285
- }
286
-
287
- customElements.define(ComboBoxDropdown.is, ComboBoxDropdown);