@vaadin/combo-box 23.1.2 → 23.2.0-dev.48e5e3967

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.
@@ -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,15 +224,21 @@ 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
- '_filterChanged(filter, itemValuePath, itemLabelPath)',
206
- '_filteredItemsChanged(filteredItems)',
207
239
  '_selectedItemChanged(selectedItem, itemValuePath, itemLabelPath)',
240
+ '_openedOrItemsChanged(opened, filteredItems, loading)',
241
+ '_updateScroller(_scroller, filteredItems, opened, loading, selectedItem, itemIdPath, _focusedIndex, renderer, theme)',
208
242
  ];
209
243
  }
210
244
 
@@ -213,13 +247,20 @@ export const ComboBoxMixin = (subclass) =>
213
247
  this._boundOnFocusout = this._onFocusout.bind(this);
214
248
  this._boundOverlaySelectedItemChanged = this._overlaySelectedItemChanged.bind(this);
215
249
  this._boundOnClearButtonMouseDown = this.__onClearButtonMouseDown.bind(this);
216
- this._boundClose = this.close.bind(this);
217
- this._boundOnOpened = this._onOpened.bind(this);
218
250
  this._boundOnClick = this._onClick.bind(this);
219
251
  this._boundOnOverlayTouchAction = this._onOverlayTouchAction.bind(this);
220
252
  this._boundOnTouchend = this._onTouchend.bind(this);
221
253
  }
222
254
 
255
+ /**
256
+ * Tag name prefix used by scroller and items.
257
+ * @protected
258
+ * @return {string}
259
+ */
260
+ get _tagNamePrefix() {
261
+ return 'vaadin-combo-box';
262
+ }
263
+
223
264
  /**
224
265
  * @return {string | undefined}
225
266
  * @protected
@@ -273,23 +314,19 @@ export const ComboBoxMixin = (subclass) =>
273
314
  ready() {
274
315
  super.ready();
275
316
 
317
+ this._initOverlay();
318
+ this._initScroller();
319
+
276
320
  this.addEventListener('focusout', this._boundOnFocusout);
277
321
 
278
322
  this._lastCommittedValue = this.value;
279
323
 
280
- this.$.dropdown.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
281
-
282
- this.addEventListener('vaadin-combo-box-dropdown-closed', this._boundClose);
283
- this.addEventListener('vaadin-combo-box-dropdown-opened', this._boundOnOpened);
284
324
  this.addEventListener('click', this._boundOnClick);
285
-
286
- this.$.dropdown.addEventListener('vaadin-overlay-touch-action', this._boundOnOverlayTouchAction);
287
-
288
325
  this.addEventListener('touchend', this._boundOnTouchend);
289
326
 
290
327
  const bringToFrontListener = () => {
291
328
  requestAnimationFrame(() => {
292
- this.$.dropdown.$.overlay.bringToFront();
329
+ this.$.overlay.bringToFront();
293
330
  });
294
331
  };
295
332
 
@@ -301,6 +338,14 @@ export const ComboBoxMixin = (subclass) =>
301
338
  this.addController(new VirtualKeyboardController(this));
302
339
  }
303
340
 
341
+ /** @protected */
342
+ disconnectedCallback() {
343
+ super.disconnectedCallback();
344
+
345
+ // Close the overlay on detach
346
+ this.close();
347
+ }
348
+
304
349
  /**
305
350
  * Requests an update for the content of items.
306
351
  * While performing the update, it invokes the renderer (passed in the `renderer` property) once an item.
@@ -308,11 +353,11 @@ export const ComboBoxMixin = (subclass) =>
308
353
  * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
309
354
  */
310
355
  requestContentUpdate() {
311
- if (!this.$.dropdown._scroller) {
356
+ if (!this._scroller) {
312
357
  return;
313
358
  }
314
359
 
315
- this.$.dropdown._scroller.requestContentUpdate();
360
+ this._scroller.requestContentUpdate();
316
361
 
317
362
  this._getItemElements().forEach((item) => {
318
363
  item.requestContentUpdate();
@@ -336,6 +381,128 @@ export const ComboBoxMixin = (subclass) =>
336
381
  this.opened = false;
337
382
  }
338
383
 
384
+ /**
385
+ * Override Polymer lifecycle callback to handle `filter` property change after
386
+ * the observer for `opened` property is triggered. This is needed when opening
387
+ * combo-box on user input to ensure the focused index is set correctly.
388
+ *
389
+ * @param {!Object} currentProps Current accessor values
390
+ * @param {?Object} changedProps Properties changed since the last call
391
+ * @param {?Object} oldProps Previous values for each changed property
392
+ * @protected
393
+ * @override
394
+ */
395
+ _propertiesChanged(currentProps, changedProps, oldProps) {
396
+ super._propertiesChanged(currentProps, changedProps, oldProps);
397
+
398
+ if (changedProps.filter !== undefined) {
399
+ this._filterChanged(changedProps.filter);
400
+ }
401
+ }
402
+
403
+ /** @private */
404
+ _initOverlay() {
405
+ const overlay = this.$.overlay;
406
+
407
+ // Store instance for detecting "dir" attribute on opening
408
+ overlay._comboBox = this;
409
+
410
+ overlay.addEventListener('touchend', this._boundOnOverlayTouchAction);
411
+ overlay.addEventListener('touchmove', this._boundOnOverlayTouchAction);
412
+
413
+ // Prevent blurring the input when clicking inside the overlay
414
+ overlay.addEventListener('mousedown', (e) => e.preventDefault());
415
+
416
+ // Preventing the default modal behavior of the overlay on input click
417
+ overlay.addEventListener('vaadin-overlay-outside-click', (e) => {
418
+ e.preventDefault();
419
+ });
420
+
421
+ // Manual two-way binding for the overlay "opened" property
422
+ overlay.addEventListener('opened-changed', (e) => {
423
+ this._overlayOpened = e.detail.value;
424
+ });
425
+ }
426
+
427
+ /**
428
+ * Create and initialize the scroller element.
429
+ * Override to provide custom host reference.
430
+ *
431
+ * @protected
432
+ */
433
+ _initScroller(host) {
434
+ const scrollerTag = `${this._tagNamePrefix}-scroller`;
435
+
436
+ const overlay = this.$.overlay;
437
+
438
+ overlay.renderer = (root) => {
439
+ if (!root.firstChild) {
440
+ root.appendChild(document.createElement(scrollerTag));
441
+ }
442
+ };
443
+
444
+ // Ensure the scroller is rendered
445
+ if (!this.opened) {
446
+ overlay.requestContentUpdate();
447
+ }
448
+
449
+ const scroller = overlay.querySelector(scrollerTag);
450
+
451
+ scroller.comboBox = host || this;
452
+ scroller.getItemLabel = this._getItemLabel.bind(this);
453
+ scroller.addEventListener('selection-changed', this._boundOverlaySelectedItemChanged);
454
+
455
+ // Trigger the observer to set properties
456
+ this._scroller = scroller;
457
+ }
458
+
459
+ /** @private */
460
+ // eslint-disable-next-line max-params
461
+ _updateScroller(scroller, items, opened, loading, selectedItem, itemIdPath, focusedIndex, renderer, theme) {
462
+ if (scroller) {
463
+ if (opened) {
464
+ scroller.style.maxHeight =
465
+ getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
466
+ }
467
+
468
+ scroller.setProperties({
469
+ items: opened ? items : [],
470
+ opened,
471
+ loading,
472
+ selectedItem,
473
+ itemIdPath,
474
+ focusedIndex,
475
+ renderer,
476
+ theme,
477
+ });
478
+ }
479
+ }
480
+
481
+ /** @protected */
482
+ _isOverlayHidden(items, loading) {
483
+ return !loading && !(items && items.length);
484
+ }
485
+
486
+ /** @private */
487
+ _openedOrItemsChanged(opened, items, loading) {
488
+ // Close the overlay if there are no items to display.
489
+ // See https://github.com/vaadin/vaadin-combo-box/pull/964
490
+ this._overlayOpened = !!(opened && (loading || (items && items.length)));
491
+ }
492
+
493
+ /** @private */
494
+ _overlayOpenedChanged(opened, wasOpened) {
495
+ if (opened) {
496
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-opened', { bubbles: true, composed: true }));
497
+
498
+ this._onOpened();
499
+ } else if (wasOpened && this.filteredItems && this.filteredItems.length) {
500
+ this.close();
501
+
502
+ this.dispatchEvent(new CustomEvent('vaadin-combo-box-dropdown-closed', { bubbles: true, composed: true }));
503
+ }
504
+ }
505
+
339
506
  /** @private */
340
507
  _focusedIndexChanged(index, oldIndex) {
341
508
  if (oldIndex === undefined) {
@@ -374,7 +541,7 @@ export const ComboBoxMixin = (subclass) =>
374
541
  this.focus();
375
542
  }
376
543
 
377
- this.__restoreFocusOnClose = true;
544
+ this.$.overlay.restoreFocusOnClose = true;
378
545
  } else {
379
546
  this._onClosed();
380
547
  if (this._openedWithFocusRing && this.hasAttribute('focused')) {
@@ -387,7 +554,7 @@ export const ComboBoxMixin = (subclass) =>
387
554
  input.setAttribute('aria-expanded', !!opened);
388
555
 
389
556
  if (opened) {
390
- input.setAttribute('aria-controls', this.$.dropdown.scrollerId);
557
+ input.setAttribute('aria-controls', this._scroller.id);
391
558
  } else {
392
559
  input.removeAttribute('aria-controls');
393
560
  }
@@ -477,7 +644,7 @@ export const ComboBoxMixin = (subclass) =>
477
644
  super._onKeyDown(e);
478
645
 
479
646
  if (e.key === 'Tab') {
480
- this.__restoreFocusOnClose = false;
647
+ this.$.overlay.restoreFocusOnClose = false;
481
648
  } else if (e.key === 'ArrowDown') {
482
649
  this._closeOnBlurIsPrevented = true;
483
650
  this._onArrowDown();
@@ -497,7 +664,11 @@ export const ComboBoxMixin = (subclass) =>
497
664
 
498
665
  /** @private */
499
666
  _getItemLabel(item) {
500
- return this.$.dropdown.getItemLabel(item);
667
+ let label = item && this.itemLabelPath ? this.get(this.itemLabelPath, item) : undefined;
668
+ if (label === undefined || label === null) {
669
+ label = item ? item.toString() : '';
670
+ }
671
+ return label;
501
672
  }
502
673
 
503
674
  /** @private */
@@ -512,7 +683,7 @@ export const ComboBoxMixin = (subclass) =>
512
683
  /** @private */
513
684
  _onArrowDown() {
514
685
  if (this.opened) {
515
- const items = this._getOverlayItems();
686
+ const items = this.filteredItems;
516
687
  if (items) {
517
688
  this._focusedIndex = Math.min(items.length - 1, this._focusedIndex + 1);
518
689
  this._prefillFocusedItemLabel();
@@ -528,7 +699,7 @@ export const ComboBoxMixin = (subclass) =>
528
699
  if (this._focusedIndex > -1) {
529
700
  this._focusedIndex = Math.max(0, this._focusedIndex - 1);
530
701
  } else {
531
- const items = this._getOverlayItems();
702
+ const items = this.filteredItems;
532
703
  if (items) {
533
704
  this._focusedIndex = items.length - 1;
534
705
  }
@@ -543,7 +714,8 @@ export const ComboBoxMixin = (subclass) =>
543
714
  /** @private */
544
715
  _prefillFocusedItemLabel() {
545
716
  if (this._focusedIndex > -1) {
546
- this._inputElementValue = this._getItemLabel(this.$.dropdown.focusedItem);
717
+ const focusedItem = this.filteredItems[this._focusedIndex];
718
+ this._inputElementValue = this._getItemLabel(focusedItem);
547
719
  this._markAllSelectionRange();
548
720
  }
549
721
  }
@@ -700,7 +872,7 @@ export const ComboBoxMixin = (subclass) =>
700
872
  _onOpened() {
701
873
  // Defer scroll position adjustment to improve performance.
702
874
  requestAnimationFrame(() => {
703
- this.$.dropdown.adjustScrollPosition();
875
+ this._scrollIntoView(this._focusedIndex);
704
876
 
705
877
  // Set attribute after the items are rendered when overlay is opened for the first time.
706
878
  this._updateActiveDescendant(this._focusedIndex);
@@ -719,9 +891,8 @@ export const ComboBoxMixin = (subclass) =>
719
891
 
720
892
  /** @private */
721
893
  _commitValue() {
722
- const items = this._getOverlayItems();
723
- if (items && this._focusedIndex > -1) {
724
- const focusedItem = items[this._focusedIndex];
894
+ if (this._focusedIndex > -1) {
895
+ const focusedItem = this.filteredItems[this._focusedIndex];
725
896
  if (this.selectedItem !== focusedItem) {
726
897
  this.selectedItem = focusedItem;
727
898
  }
@@ -734,18 +905,14 @@ export const ComboBoxMixin = (subclass) =>
734
905
  this.value = '';
735
906
  }
736
907
  } else {
737
- const toLowerCase = (item) => item && item.toLowerCase && item.toLowerCase();
738
-
739
- // Try to find an item whose label matches the input value. A matching item is searched from
740
- // the filteredItems array (if available) and the selectedItem (if available).
741
- const itemMatchingByLabel = [...(this.filteredItems || []), this.selectedItem].find((item) => {
742
- return toLowerCase(this._getItemLabel(item)) === toLowerCase(this._inputElementValue);
743
- });
908
+ // Try to find an item which label matches the input value.
909
+ const items = [...(this.filteredItems || []), this.selectedItem];
910
+ const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
744
911
 
745
912
  if (
746
913
  this.allowCustomValue &&
747
914
  // To prevent a repetitive input value being saved after pressing ESC and Tab.
748
- !itemMatchingByLabel
915
+ !itemMatchingInputValue
749
916
  ) {
750
917
  const customValue = this._inputElementValue;
751
918
 
@@ -762,12 +929,11 @@ export const ComboBoxMixin = (subclass) =>
762
929
  });
763
930
  this.dispatchEvent(e);
764
931
  if (!e.defaultPrevented) {
765
- this._selectItemForValue(customValue);
766
932
  this.value = customValue;
767
933
  }
768
- } else if (!this.allowCustomValue && !this.opened && itemMatchingByLabel) {
934
+ } else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
769
935
  // An item matching by label was found, select it.
770
- this.value = this._getItemValue(itemMatchingByLabel);
936
+ this.value = this._getItemValue(itemMatchingInputValue);
771
937
  } else {
772
938
  // Revert the input value
773
939
  this._inputElementValue = this.selectedItem ? this._getItemLabel(this.selectedItem) : this.value || '';
@@ -796,19 +962,27 @@ export const ComboBoxMixin = (subclass) =>
796
962
  * @override
797
963
  */
798
964
  _onInput(event) {
799
- if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
800
- this.open();
801
- }
965
+ const filter = this._inputElementValue;
802
966
 
803
- const value = this._inputElementValue;
804
- if (this.filter === value) {
967
+ // When opening dropdown on user input, both `opened` and `filter` properties are set.
968
+ // Perform a batched property update instead of relying on sync property observers.
969
+ // This is necessary to avoid an extra data-provider request for loading first page.
970
+ const props = {};
971
+
972
+ if (this.filter === filter) {
805
973
  // Filter and input value might get out of sync, while keyboard navigating for example.
806
974
  // Afterwards, input value might be changed to the same value as used in filtering.
807
975
  // In situation like these, we need to make sure all the filter changes handlers are run.
808
- this._filterChanged(this.filter, this.itemValuePath, this.itemLabelPath);
976
+ this._filterChanged(this.filter);
809
977
  } else {
810
- this.filter = value;
978
+ props.filter = filter;
979
+ }
980
+
981
+ if (!this.opened && !this._isClearButton(event) && !this.autoOpenDisabled) {
982
+ props.opened = true;
811
983
  }
984
+
985
+ this.setProperties(props);
812
986
  }
813
987
 
814
988
  /**
@@ -831,13 +1005,11 @@ export const ComboBoxMixin = (subclass) =>
831
1005
  }
832
1006
 
833
1007
  /** @private */
834
- _filterChanged(filter, _itemValuePath, _itemLabelPath) {
835
- if (filter === undefined) {
836
- return;
837
- }
838
-
1008
+ _filterChanged(filter) {
839
1009
  // Scroll to the top of the list whenever the filter changes.
840
- this.$.dropdown._scrollIntoView(0);
1010
+ this._scrollIntoView(0);
1011
+
1012
+ this._focusedIndex = -1;
841
1013
 
842
1014
  if (this.items) {
843
1015
  this.filteredItems = this._filterItems(this.items, filter);
@@ -849,13 +1021,6 @@ export const ComboBoxMixin = (subclass) =>
849
1021
  }
850
1022
  }
851
1023
 
852
- /** @private */
853
- _loadingChanged(loading) {
854
- if (loading) {
855
- this._focusedIndex = -1;
856
- }
857
- }
858
-
859
1024
  /** @protected */
860
1025
  _revertInputValue() {
861
1026
  if (this.filter !== '') {
@@ -901,9 +1066,7 @@ export const ComboBoxMixin = (subclass) =>
901
1066
  this._inputElementValue = this._getItemLabel(selectedItem);
902
1067
  }
903
1068
 
904
- this.$.dropdown._selectedItem = selectedItem;
905
- const items = this._getOverlayItems();
906
- if (this.filteredItems && items) {
1069
+ if (this.filteredItems) {
907
1070
  this._focusedIndex = this.filteredItems.indexOf(selectedItem);
908
1071
  }
909
1072
  }
@@ -920,7 +1083,7 @@ export const ComboBoxMixin = (subclass) =>
920
1083
  return;
921
1084
  }
922
1085
 
923
- if (this._isValidValue(value)) {
1086
+ if (isValidValue(value)) {
924
1087
  if (this._getItemValue(this.selectedItem) !== value) {
925
1088
  this._selectItemForValue(value);
926
1089
  }
@@ -956,43 +1119,50 @@ export const ComboBoxMixin = (subclass) =>
956
1119
 
957
1120
  if (items) {
958
1121
  this.filteredItems = items.slice(0);
959
- } else if (this.__previousItems) {
1122
+ } else if (oldItems) {
960
1123
  // Only clear filteredItems if the component had items previously but got cleared
961
1124
  this.filteredItems = null;
962
1125
  }
1126
+ }
963
1127
 
964
- const valueIndex = this._indexOfValue(this.value, items);
965
- this._focusedIndex = valueIndex;
1128
+ /** @private */
1129
+ _filteredItemsChanged(filteredItems, oldFilteredItems) {
1130
+ // Store the currently focused item if any. The focused index preserves
1131
+ // in the case when more filtered items are loading but it is reset
1132
+ // when the user types in a filter query.
1133
+ const focusedItem = oldFilteredItems ? oldFilteredItems[this._focusedIndex] : null;
1134
+
1135
+ // Try to sync `selectedItem` based on `value` once a new set of `filteredItems` is available
1136
+ // (as a result of external filtering or when they have been loaded by the data provider).
1137
+ // When `value` is specified but `selectedItem` is not, it means that there was no item
1138
+ // matching `value` at the moment `value` was set, so `selectedItem` has remained unsynced.
1139
+ const valueIndex = this.__getItemIndexByValue(filteredItems, this.value);
1140
+ if ((this.selectedItem === null || this.selectedItem === undefined) && valueIndex >= 0) {
1141
+ this.selectedItem = filteredItems[valueIndex];
1142
+ }
966
1143
 
967
- const item = valueIndex > -1 && items[valueIndex];
968
- if (item) {
969
- this.selectedItem = item;
1144
+ // Try to first set focus on the item that had been focused before `filteredItems` were updated
1145
+ // if it is still present in the `filteredItems` array. Otherwise, set the focused index
1146
+ // depending on the selected item or the filter query.
1147
+ const focusedItemIndex = this.__getItemIndexByValue(filteredItems, this._getItemValue(focusedItem));
1148
+ if (focusedItemIndex > -1) {
1149
+ this._focusedIndex = focusedItemIndex;
1150
+ } else {
1151
+ this.__setInitialFocusedIndex();
970
1152
  }
971
- this.__previousItems = items;
972
1153
  }
973
1154
 
974
1155
  /** @private */
975
- _filteredItemsChanged(filteredItems, _itemValuePath, _itemLabelPath) {
976
- this._setOverlayItems(filteredItems);
977
-
978
- // When the external filtering is used and `value` was provided before `filteredItems`,
979
- // initialize the selected item with the current value here. This will also cause
980
- // the input element value to sync. In other cases, the selected item is already initialized
981
- // in other observers such as `valueChanged`, `_itemsChanged`.
982
- const valueIndex = this._indexOfValue(this.value, filteredItems);
983
- if (this.selectedItem === null && valueIndex >= 0) {
984
- this._selectItemForValue(this.value);
985
- }
986
-
1156
+ __setInitialFocusedIndex() {
987
1157
  const inputValue = this._inputElementValue;
988
1158
  if (inputValue === undefined || inputValue === this._getItemLabel(this.selectedItem)) {
989
1159
  // When the input element value is the same as the current value or not defined,
990
1160
  // set the focused index to the item that matches the value.
991
- this._focusedIndex = this.$.dropdown.indexOfLabel(this._getItemLabel(this.selectedItem));
1161
+ this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this._getItemLabel(this.selectedItem));
992
1162
  } else {
993
1163
  // When the user filled in something that is different from the current value = filtering is enabled,
994
1164
  // set the focused index to the item that matches the filter query.
995
- this._focusedIndex = this.$.dropdown.indexOfLabel(this.filter);
1165
+ this._focusedIndex = this.__getItemIndexByLabel(this.filteredItems, this.filter);
996
1166
  }
997
1167
  }
998
1168
 
@@ -1013,7 +1183,7 @@ export const ComboBoxMixin = (subclass) =>
1013
1183
 
1014
1184
  /** @private */
1015
1185
  _selectItemForValue(value) {
1016
- const valueIndex = this._indexOfValue(value, this.filteredItems);
1186
+ const valueIndex = this.__getItemIndexByValue(this.filteredItems, value);
1017
1187
  const previouslySelectedItem = this.selectedItem;
1018
1188
 
1019
1189
  if (valueIndex >= 0) {
@@ -1029,42 +1199,48 @@ export const ComboBoxMixin = (subclass) =>
1029
1199
  }
1030
1200
  }
1031
1201
 
1032
- /** @protected */
1033
- _getItemElements() {
1034
- return Array.from(this.$.dropdown._scroller.querySelectorAll('vaadin-combo-box-item'));
1035
- }
1036
-
1037
1202
  /** @private */
1038
- _getOverlayItems() {
1039
- return this.$.dropdown._items;
1203
+ _getItemElements() {
1204
+ return Array.from(this._scroller.querySelectorAll(`${this._tagNamePrefix}-item`));
1040
1205
  }
1041
1206
 
1042
1207
  /** @private */
1043
- _setOverlayItems(items) {
1044
- this.$.dropdown.set('_items', items);
1208
+ _scrollIntoView(index) {
1209
+ if (!this._scroller) {
1210
+ return;
1211
+ }
1212
+ this._scroller.scrollIntoView(index);
1045
1213
  }
1046
1214
 
1047
- /** @private */
1048
- _indexOfValue(value, items) {
1049
- if (!items || !this._isValidValue(value)) {
1215
+ /**
1216
+ * Returns the first item that matches the provided value.
1217
+ *
1218
+ * @private
1219
+ */
1220
+ __getItemIndexByValue(items, value) {
1221
+ if (!items || !isValidValue(value)) {
1050
1222
  return -1;
1051
1223
  }
1052
1224
 
1053
- return items.findIndex((item) => {
1054
- if (item instanceof ComboBoxPlaceholder) {
1055
- return false;
1056
- }
1057
-
1225
+ return findItemIndex(items, (item) => {
1058
1226
  return this._getItemValue(item) === value;
1059
1227
  });
1060
1228
  }
1061
1229
 
1062
1230
  /**
1063
- * Checks if the value is supported as an item value in this control.
1231
+ * Returns the first item that matches the provided label.
1232
+ * Labels are matched against each other case insensitively.
1233
+ *
1064
1234
  * @private
1065
1235
  */
1066
- _isValidValue(value) {
1067
- return value !== undefined && value !== null;
1236
+ __getItemIndexByLabel(items, label) {
1237
+ if (!items || !label) {
1238
+ return -1;
1239
+ }
1240
+
1241
+ return findItemIndex(items, (item) => {
1242
+ return this._getItemLabel(item).toString().toLowerCase() === label.toString().toLowerCase();
1243
+ });
1068
1244
  }
1069
1245
 
1070
1246
  /** @private */
@@ -1095,7 +1271,7 @@ export const ComboBoxMixin = (subclass) =>
1095
1271
  /** @private */
1096
1272
  _onFocusout(event) {
1097
1273
  // Fixes the problem with `focusout` happening when clicking on the scroll bar on Edge
1098
- if (event.relatedTarget === this.$.dropdown.$.overlay) {
1274
+ if (event.relatedTarget === this.$.overlay) {
1099
1275
  event.composedPath()[0].focus();
1100
1276
  return;
1101
1277
  }
@@ -1121,29 +1297,6 @@ export const ComboBoxMixin = (subclass) =>
1121
1297
  this._clear();
1122
1298
  }
1123
1299
 
1124
- /**
1125
- * Returns true if `value` is valid, and sets the `invalid` flag appropriately.
1126
- *
1127
- * @return {boolean} True if the value is valid and sets the `invalid` flag appropriately
1128
- */
1129
- validate() {
1130
- return !(this.invalid = !this.checkValidity());
1131
- }
1132
-
1133
- /**
1134
- * Returns true if the current input value satisfies all constraints (if any).
1135
- * You can override this method for custom validations.
1136
- *
1137
- * @return {boolean}
1138
- */
1139
- checkValidity() {
1140
- if (super.checkValidity) {
1141
- return super.checkValidity();
1142
- }
1143
-
1144
- return !this.required || !!this.value;
1145
- }
1146
-
1147
1300
  /**
1148
1301
  * Fired when the value changes.
1149
1302
  *
@@ -1171,4 +1324,16 @@ export const ComboBoxMixin = (subclass) =>
1171
1324
  * To comply with https://developer.mozilla.org/en-US/docs/Web/Events/change
1172
1325
  * @event change
1173
1326
  */
1327
+
1328
+ /**
1329
+ * Fired after the `vaadin-combo-box-overlay` opens.
1330
+ *
1331
+ * @event vaadin-combo-box-dropdown-opened
1332
+ */
1333
+
1334
+ /**
1335
+ * Fired after the `vaadin-combo-box-overlay` closes.
1336
+ *
1337
+ * @event vaadin-combo-box-dropdown-closed
1338
+ */
1174
1339
  };