@vaadin/multi-select-combo-box 25.0.0-alpha1 → 25.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.
Files changed (25) hide show
  1. package/package.json +17 -15
  2. package/src/{vaadin-multi-select-combo-box-styles.d.ts → styles/vaadin-multi-select-combo-box-base-styles.d.ts} +1 -3
  3. package/src/styles/vaadin-multi-select-combo-box-base-styles.js +58 -0
  4. package/src/styles/vaadin-multi-select-combo-box-chip-base-styles.js +112 -0
  5. package/src/styles/vaadin-multi-select-combo-box-chip-core-styles.js +33 -0
  6. package/src/styles/vaadin-multi-select-combo-box-core-styles.d.ts +8 -0
  7. package/src/{vaadin-multi-select-combo-box-styles.js → styles/vaadin-multi-select-combo-box-core-styles.js} +6 -29
  8. package/src/styles/vaadin-multi-select-combo-box-overlay-base-styles.js +19 -0
  9. package/src/styles/vaadin-multi-select-combo-box-overlay-core-styles.js +21 -0
  10. package/src/styles/vaadin-multi-select-combo-box-scroller-base-styles.js +8 -0
  11. package/src/styles/vaadin-multi-select-combo-box-scroller-core-styles.js +27 -0
  12. package/src/vaadin-multi-select-combo-box-chip.js +4 -3
  13. package/src/vaadin-multi-select-combo-box-container.js +1 -0
  14. package/src/vaadin-multi-select-combo-box-item.js +7 -11
  15. package/src/vaadin-multi-select-combo-box-mixin.d.ts +9 -80
  16. package/src/vaadin-multi-select-combo-box-mixin.js +396 -267
  17. package/src/vaadin-multi-select-combo-box-overlay.js +5 -19
  18. package/src/vaadin-multi-select-combo-box-scroller.js +3 -24
  19. package/src/vaadin-multi-select-combo-box.d.ts +13 -7
  20. package/src/vaadin-multi-select-combo-box.js +40 -90
  21. package/theme/lumo/vaadin-multi-select-combo-box-styles.js +4 -1
  22. package/web-types.json +207 -230
  23. package/web-types.lit.json +78 -78
  24. package/src/vaadin-multi-select-combo-box-internal-mixin.js +0 -449
  25. package/src/vaadin-multi-select-combo-box-internal.js +0 -56
@@ -4,6 +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 { announce } from '@vaadin/a11y-base/src/announce.js';
7
+ import { ComboBoxDataProviderMixin } from '@vaadin/combo-box/src/vaadin-combo-box-data-provider-mixin.js';
8
+ import { ComboBoxItemsMixin } from '@vaadin/combo-box/src/vaadin-combo-box-items-mixin.js';
9
+ import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
7
10
  import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
8
11
  import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
9
12
  import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
@@ -13,11 +16,15 @@ import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-c
13
16
 
14
17
  /**
15
18
  * @polymerMixin
19
+ * @mixes ComboBoxDataProviderMixin
20
+ * @mixes ComboBoxItemsMixin
16
21
  * @mixes InputControlMixin
17
22
  * @mixes ResizeMixin
18
23
  */
19
24
  export const MultiSelectComboBoxMixin = (superClass) =>
20
- class MultiSelectComboBoxMixinClass extends InputControlMixin(ResizeMixin(superClass)) {
25
+ class MultiSelectComboBoxMixinClass extends ComboBoxDataProviderMixin(
26
+ ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass))),
27
+ ) {
21
28
  static get properties() {
22
29
  return {
23
30
  /**
@@ -29,7 +36,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
29
36
  type: Boolean,
30
37
  value: false,
31
38
  reflectToAttribute: true,
32
- observer: '_autoExpandHorizontallyChanged',
33
39
  sync: true,
34
40
  },
35
41
 
@@ -43,37 +49,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
43
49
  type: Boolean,
44
50
  value: false,
45
51
  reflectToAttribute: true,
46
- observer: '_autoExpandVerticallyChanged',
47
- sync: true,
48
- },
49
-
50
- /**
51
- * Set true to prevent the overlay from opening automatically.
52
- * @attr {boolean} auto-open-disabled
53
- */
54
- autoOpenDisabled: {
55
- type: Boolean,
56
- sync: true,
57
- },
58
-
59
- /**
60
- * Set to true to display the clear icon which clears the input.
61
- * @attr {boolean} clear-button-visible
62
- */
63
- clearButtonVisible: {
64
- type: Boolean,
65
- reflectToAttribute: true,
66
- observer: '_clearButtonVisibleChanged',
67
- value: false,
68
- sync: true,
69
- },
70
-
71
- /**
72
- * A full set of items to filter the visible options from.
73
- * The items can be of either `String` or `Object` type.
74
- */
75
- items: {
76
- type: Array,
77
52
  sync: true,
78
53
  },
79
54
 
@@ -85,28 +60,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
85
60
  */
86
61
  itemClassNameGenerator: {
87
62
  type: Object,
88
- observer: '__itemClassNameGeneratorChanged',
89
- sync: true,
90
- },
91
-
92
- /**
93
- * The item property used for a visual representation of the item.
94
- * @attr {string} item-label-path
95
- */
96
- itemLabelPath: {
97
- type: String,
98
- value: 'label',
99
- sync: true,
100
- },
101
-
102
- /**
103
- * Path for the value of the item. If `items` is an array of objects,
104
- * this property is used as a string value for the selected item.
105
- * @attr {string} item-value-path
106
- */
107
- itemValuePath: {
108
- type: String,
109
- value: 'value',
110
63
  sync: true,
111
64
  },
112
65
 
@@ -174,23 +127,12 @@ export const MultiSelectComboBoxMixin = (superClass) =>
174
127
  sync: true,
175
128
  },
176
129
 
177
- /**
178
- * A space-delimited list of CSS class names to set on the overlay element.
179
- *
180
- * @attr {string} overlay-class
181
- */
182
- overlayClass: {
183
- type: String,
184
- sync: true,
185
- },
186
-
187
130
  /**
188
131
  * When present, it specifies that the field is read-only.
189
132
  */
190
133
  readonly: {
191
134
  type: Boolean,
192
135
  value: false,
193
- observer: '_readonlyChanged',
194
136
  reflectToAttribute: true,
195
137
  sync: true,
196
138
  },
@@ -206,53 +148,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
206
148
  sync: true,
207
149
  },
208
150
 
209
- /**
210
- * True if the dropdown is open, false otherwise.
211
- */
212
- opened: {
213
- type: Boolean,
214
- notify: true,
215
- value: false,
216
- reflectToAttribute: true,
217
- sync: true,
218
- },
219
-
220
- /**
221
- * Total number of items.
222
- */
223
- size: {
224
- type: Number,
225
- sync: true,
226
- },
227
-
228
- /**
229
- * Number of items fetched at a time from the data provider.
230
- * @attr {number} page-size
231
- */
232
- pageSize: {
233
- type: Number,
234
- value: 50,
235
- observer: '_pageSizeChanged',
236
- sync: true,
237
- },
238
-
239
- /**
240
- * Function that provides items lazily. Receives two arguments:
241
- *
242
- * - `params` - Object with the following properties:
243
- * - `params.page` Requested page index
244
- * - `params.pageSize` Current page size
245
- * - `params.filter` Currently applied filter
246
- *
247
- * - `callback(items, size)` - Callback function with arguments:
248
- * - `items` Current page of items
249
- * - `size` Total number of items.
250
- */
251
- dataProvider: {
252
- type: Object,
253
- sync: true,
254
- },
255
-
256
151
  /**
257
152
  * When true, the user can input a value that is not present in the items list.
258
153
  * @attr {boolean} allow-custom-value
@@ -290,26 +185,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
290
185
  sync: true,
291
186
  },
292
187
 
293
- /**
294
- * Filtering string the user has typed into the input field.
295
- */
296
- filter: {
297
- type: String,
298
- value: '',
299
- notify: true,
300
- sync: true,
301
- },
302
-
303
- /**
304
- * A subset of items, filtered based on the user input. Filtered items
305
- * can be assigned directly to omit the internal filtering functionality.
306
- * The items can be of either `String` or `Object` type.
307
- */
308
- filteredItems: {
309
- type: Array,
310
- sync: true,
311
- },
312
-
313
188
  /**
314
189
  * Set to true to group selected items at the top of the overlay.
315
190
  * @attr {boolean} selected-items-on-top
@@ -348,6 +223,13 @@ export const MultiSelectComboBoxMixin = (superClass) =>
348
223
  /** @private */
349
224
  _topGroup: {
350
225
  type: Array,
226
+ observer: '_topGroupChanged',
227
+ sync: true,
228
+ },
229
+
230
+ /** @private */
231
+ _inputField: {
232
+ type: Object,
351
233
  },
352
234
  };
353
235
  }
@@ -355,7 +237,9 @@ export const MultiSelectComboBoxMixin = (superClass) =>
355
237
  static get observers() {
356
238
  return [
357
239
  '_selectedItemsChanged(selectedItems)',
240
+ '__openedOrItemsChanged(opened, _dropdownItems, loading, __keepOverlayOpened)',
358
241
  '__updateOverflowChip(_overflow, _overflowItems, disabled, readonly)',
242
+ '__updateScroller(opened, _dropdownItems, _focusedIndex, _theme)',
359
243
  '__updateTopGroup(selectedItemsOnTop, selectedItems, opened)',
360
244
  ];
361
245
  }
@@ -399,6 +283,15 @@ export const MultiSelectComboBoxMixin = (superClass) =>
399
283
  return this.selectedItems && this.selectedItems.length > 0;
400
284
  }
401
285
 
286
+ /**
287
+ * Tag name prefix used by scroller and items.
288
+ * @protected
289
+ * @return {string}
290
+ */
291
+ get _tagNamePrefix() {
292
+ return 'vaadin-multi-select-combo-box';
293
+ }
294
+
402
295
  /** @protected */
403
296
  ready() {
404
297
  super.ready();
@@ -419,6 +312,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
419
312
  this._tooltipController.setAriaTarget(this.inputElement);
420
313
  this._tooltipController.setShouldShow((target) => !target.opened);
421
314
 
315
+ this._toggleElement = this.$.toggleButton;
422
316
  this._inputField = this.shadowRoot.querySelector('[part="input-field"]');
423
317
 
424
318
  this._overflowController = new SlotController(this, 'overflow', 'vaadin-multi-select-combo-box-chip', {
@@ -428,8 +322,41 @@ export const MultiSelectComboBoxMixin = (superClass) =>
428
322
  },
429
323
  });
430
324
  this.addController(this._overflowController);
325
+ }
431
326
 
432
- this.__updateChips();
327
+ /** @protected */
328
+ updated(props) {
329
+ super.updated(props);
330
+
331
+ ['loading', 'itemIdPath', 'itemClassNameGenerator', 'renderer'].forEach((prop) => {
332
+ if (props.has(prop)) {
333
+ this._scroller[prop] = this[prop];
334
+ }
335
+ });
336
+
337
+ if (props.has('selectedItems') && this.opened) {
338
+ this.$.overlay._updateOverlayWidth();
339
+ }
340
+
341
+ const chipProps = [
342
+ 'autoExpandHorizontally',
343
+ 'autoExpandVertically',
344
+ 'disabled',
345
+ 'readonly',
346
+ 'clearButtonVisible',
347
+ 'itemClassNameGenerator',
348
+ ];
349
+ if (chipProps.some((prop) => props.has(prop))) {
350
+ this.__updateChips();
351
+ }
352
+
353
+ if (props.has('readonly')) {
354
+ this._setDropdownItems(this.filteredItems);
355
+
356
+ if (this.dataProvider) {
357
+ this.clearCache();
358
+ }
359
+ }
433
360
  }
434
361
 
435
362
  /**
@@ -440,6 +367,17 @@ export const MultiSelectComboBoxMixin = (superClass) =>
440
367
  return this.required && !this.readonly ? this._hasValue : true;
441
368
  }
442
369
 
370
+ /**
371
+ * Opens the dropdown list.
372
+ * @override
373
+ */
374
+ open() {
375
+ // Allow opening dropdown when readonly.
376
+ if (!this.disabled && !(this.readonly && this.selectedItems.length === 0)) {
377
+ this.opened = true;
378
+ }
379
+ }
380
+
443
381
  /**
444
382
  * Clears the selected items.
445
383
  */
@@ -451,11 +389,15 @@ export const MultiSelectComboBoxMixin = (superClass) =>
451
389
 
452
390
  /**
453
391
  * Clears the cached pages and reloads data from data provider when needed.
392
+ * @override
454
393
  */
455
394
  clearCache() {
456
- if (this.$ && this.$.comboBox) {
457
- this.$.comboBox.clearCache();
395
+ // Do not clear the data provider cache when read-only.
396
+ if (this.readonly) {
397
+ return;
458
398
  }
399
+
400
+ super.clearCache();
459
401
  }
460
402
 
461
403
  /**
@@ -465,34 +407,130 @@ export const MultiSelectComboBoxMixin = (superClass) =>
465
407
  * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
466
408
  */
467
409
  requestContentUpdate() {
468
- if (this.$ && this.$.comboBox) {
469
- this.$.comboBox.requestContentUpdate();
410
+ if (!this._scroller) {
411
+ return;
470
412
  }
413
+
414
+ this._scroller.requestContentUpdate();
415
+
416
+ this._getItemElements().forEach((item) => {
417
+ item.requestContentUpdate();
418
+ });
471
419
  }
472
420
 
473
421
  /**
474
- * Override method inherited from `DisabledMixin` to forward disabled to chips.
422
+ * Override method from `ComboBoxBaseMixin` to implement clearing logic.
475
423
  * @protected
476
424
  * @override
477
425
  */
478
- _disabledChanged(disabled, oldDisabled) {
479
- super._disabledChanged(disabled, oldDisabled);
426
+ _onClearAction() {
427
+ this.clear();
428
+ }
480
429
 
481
- if (disabled || oldDisabled) {
482
- this.__updateChips();
430
+ /**
431
+ * Override method from `ComboBoxBaseMixin`
432
+ * to commit value on overlay closing.
433
+ * @protected
434
+ * @override
435
+ */
436
+ _onClosed() {
437
+ // Do not commit selected item again on outside click
438
+ this._ignoreCommitValue = true;
439
+
440
+ if (!this.loading || this.allowCustomValue) {
441
+ this._commitValue();
442
+ }
443
+ }
444
+
445
+ /** @private */
446
+ __updateScroller(opened, items, focusedIndex, theme) {
447
+ if (opened) {
448
+ this._scroller.style.maxHeight =
449
+ getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
450
+ }
451
+
452
+ this._scroller.setProperties({
453
+ items: opened ? items : [],
454
+ opened,
455
+ focusedIndex,
456
+ theme,
457
+ });
458
+ }
459
+
460
+ /** @private */
461
+ __openedOrItemsChanged(opened, items, loading, keepOverlayOpened) {
462
+ // Close the overlay if there are no items to display.
463
+ // See https://github.com/vaadin/vaadin-combo-box/pull/964
464
+ this._overlayOpened = opened && (keepOverlayOpened || loading || !!(items && items.length));
465
+ }
466
+
467
+ /**
468
+ * @protected
469
+ */
470
+ _closeOrCommit() {
471
+ if (!this.opened) {
472
+ this._commitValue();
473
+ } else {
474
+ this.close();
483
475
  }
484
476
  }
485
477
 
486
478
  /**
487
- * Override method inherited from `InputMixin` to forward the input to combo-box.
488
479
  * @protected
489
480
  * @override
490
481
  */
491
- _inputElementChanged(input) {
492
- super._inputElementChanged(input);
482
+ _commitValue() {
483
+ // Store filter value for checking if user input is matching
484
+ // an item which is already selected, to not un-select it.
485
+ this._lastFilter = this.filter;
486
+
487
+ // Do not commit focused item on not blur / outside click
488
+ if (this._ignoreCommitValue) {
489
+ this._inputElementValue = '';
490
+ this._focusedIndex = -1;
491
+ this._ignoreCommitValue = false;
492
+ } else {
493
+ this.__commitUserInput();
494
+ }
493
495
 
494
- if (input) {
495
- this.$.comboBox._setInputElement(input);
496
+ // Clear filter unless keepFilter is set
497
+ if (!this.keepFilter || !this.opened) {
498
+ this.filter = '';
499
+ }
500
+ }
501
+
502
+ /** @private */
503
+ __commitUserInput() {
504
+ if (this._focusedIndex > -1) {
505
+ const focusedItem = this._dropdownItems[this._focusedIndex];
506
+ this.__selectItem(focusedItem);
507
+ } else if (this._inputElementValue) {
508
+ // Detect if input value doesn't match an existing item
509
+ const items = [...this._dropdownItems];
510
+ const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
511
+
512
+ if (this.allowCustomValue && !itemMatchingInputValue) {
513
+ const customValue = this._inputElementValue;
514
+
515
+ // Store reference to the last custom value for checking it on focusout.
516
+ this._lastCustomValue = customValue;
517
+
518
+ this.__clearInternalValue(true);
519
+
520
+ this.dispatchEvent(
521
+ new CustomEvent('custom-value-set', {
522
+ detail: customValue,
523
+ composed: true,
524
+ bubbles: true,
525
+ }),
526
+ );
527
+ } else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
528
+ // An item matching by label was found, select it.
529
+ this.__selectItem(itemMatchingInputValue);
530
+ } else {
531
+ // Clear input value on Escape press while closed.
532
+ this._inputElementValue = '';
533
+ }
496
534
  }
497
535
  }
498
536
 
@@ -502,6 +540,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
502
540
  * @protected
503
541
  */
504
542
  _setFocused(focused) {
543
+ if (!focused) {
544
+ this._ignoreCommitValue = true;
545
+ }
546
+
505
547
  super._setFocused(focused);
506
548
 
507
549
  // Do not validate when focusout is caused by document
@@ -510,6 +552,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
510
552
  this._focusedChipIndex = -1;
511
553
  this._requestValidation();
512
554
  }
555
+
556
+ if (!focused && this.readonly && !this._closeOnBlurIsPrevented) {
557
+ this.close();
558
+ }
513
559
  }
514
560
 
515
561
  /**
@@ -541,73 +587,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
541
587
  super._delegateAttribute(name, value);
542
588
  }
543
589
 
544
- /** @private */
545
- _autoExpandHorizontallyChanged(autoExpand, oldAutoExpand) {
546
- if (autoExpand || oldAutoExpand) {
547
- this.__updateChips();
548
- }
549
- }
550
-
551
- /** @private */
552
- _autoExpandVerticallyChanged(autoExpand, oldAutoExpand) {
553
- if (autoExpand || oldAutoExpand) {
554
- this.__updateChips();
555
- }
556
- }
557
-
558
- /**
559
- * Setting clear button visible reduces total space available
560
- * for rendering chips, and making it hidden increases it.
561
- * @private
562
- */
563
- _clearButtonVisibleChanged(visible, oldVisible) {
564
- if (visible || oldVisible) {
565
- this.__updateChips();
566
- }
567
- }
568
-
569
- /**
570
- * Implement two-way binding for the `filteredItems` property
571
- * that can be set on the internal combo-box element.
572
- *
573
- * @param {CustomEvent} event
574
- * @private
575
- */
576
- _onFilteredItemsChanged(event) {
577
- const { value } = event.detail;
578
- if (Array.isArray(value) || value == null) {
579
- this.filteredItems = value;
580
- }
581
- }
582
-
583
- /** @private */
584
- _readonlyChanged(readonly, oldReadonly) {
585
- if (readonly || oldReadonly) {
586
- this.__updateChips();
587
- }
588
-
589
- if (this.dataProvider) {
590
- this.clearCache();
591
- }
592
- }
593
-
594
- /** @private */
595
- __itemClassNameGeneratorChanged(generator, oldGenerator) {
596
- if (generator || oldGenerator) {
597
- this.__updateChips();
598
- }
599
- }
600
-
601
- /** @private */
602
- _pageSizeChanged(pageSize, oldPageSize) {
603
- if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
604
- this.pageSize = oldPageSize;
605
- console.error('"pageSize" value must be an integer > 0');
606
- }
607
-
608
- this.$.comboBox.pageSize = this.pageSize;
609
- }
610
-
611
590
  /** @private */
612
591
  _placeholderChanged(placeholder) {
613
592
  const tmpPlaceholder = this.__tmpA11yPlaceholder;
@@ -643,15 +622,90 @@ export const MultiSelectComboBoxMixin = (superClass) =>
643
622
 
644
623
  // Update selected for dropdown items
645
624
  this.requestContentUpdate();
625
+ }
646
626
 
647
- if (this.opened) {
648
- this.$.comboBox._updateOverlayWidth();
627
+ /** @private */
628
+ _topGroupChanged(topGroup) {
629
+ if (topGroup) {
630
+ this._setDropdownItems(this.filteredItems);
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Override method from `ComboBoxBaseMixin` to handle valid value.
636
+ * @protected
637
+ * @override
638
+ */
639
+ _hasValidInputValue() {
640
+ const hasInvalidOption = this._focusedIndex < 0 && this._inputElementValue !== '';
641
+ return this.allowCustomValue || !hasInvalidOption;
642
+ }
643
+
644
+ /**
645
+ * Override method inherited from the combo-box
646
+ * to not request data provider when read-only.
647
+ *
648
+ * @protected
649
+ * @override
650
+ */
651
+ _shouldFetchData() {
652
+ if (this.readonly) {
653
+ return false;
654
+ }
655
+
656
+ return super._shouldFetchData();
657
+ }
658
+
659
+ /**
660
+ * Override combo-box method to group selected
661
+ * items at the top of the overlay.
662
+ *
663
+ * @protected
664
+ * @override
665
+ */
666
+ _setDropdownItems(items) {
667
+ if (this.readonly) {
668
+ this.__setDropdownItems(this.selectedItems);
669
+ return;
670
+ }
671
+
672
+ if (this.filter || !this.selectedItemsOnTop) {
673
+ this.__setDropdownItems(items);
674
+ return;
675
+ }
676
+
677
+ if (items && items.length && this._topGroup && this._topGroup.length) {
678
+ // Filter out items included to the top group.
679
+ const filteredItems = items.filter((item) => this._findIndex(item, this._topGroup, this.itemIdPath) === -1);
680
+
681
+ this.__setDropdownItems(this._topGroup.concat(filteredItems));
682
+ return;
649
683
  }
684
+
685
+ this.__setDropdownItems(items);
650
686
  }
651
687
 
652
688
  /** @private */
653
- _getItemLabel(item) {
654
- return this.$.comboBox._getItemLabel(item);
689
+ __setDropdownItems(newItems) {
690
+ const oldItems = this._dropdownItems;
691
+ this._dropdownItems = newItems;
692
+
693
+ // Store the currently focused item if any. The focused index preserves
694
+ // in the case when more filtered items are loading but it is reset
695
+ // when the user types in a filter query.
696
+ const focusedItem = oldItems ? oldItems[this._focusedIndex] : null;
697
+
698
+ // Try to first set focus on the item that had been focused before `newItems` were updated
699
+ // if it is still present in the `newItems` array. Otherwise, set the focused index
700
+ // depending on the selected item or the filter query.
701
+ const focusedItemIndex = this.__getItemIndexByValue(newItems, this._getItemValue(focusedItem));
702
+ if (focusedItemIndex > -1) {
703
+ this._focusedIndex = focusedItemIndex;
704
+ } else {
705
+ // When the user filled in something that is different from the current value = filtering is enabled,
706
+ // set the focused index to the item that matches the filter query.
707
+ this._focusedIndex = this.__getItemIndexByLabel(newItems, this.filter);
708
+ }
655
709
  }
656
710
 
657
711
  /** @private */
@@ -682,13 +736,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
682
736
  */
683
737
  __clearInternalValue(force = false) {
684
738
  if (!this.keepFilter || force) {
685
- // Clear both combo box value and filter.
739
+ // Clear both input value and filter.
686
740
  this.filter = '';
687
- this.$.comboBox.clear();
741
+ this._inputElementValue = '';
688
742
  } else {
689
- // Only clear combo box value. This effectively resets _lastCommittedValue
690
- // which allows toggling the same item multiple times via keyboard.
691
- this.$.comboBox.clear();
692
743
  // Restore input to the filter value. Needed when items are
693
744
  // navigated with keyboard, which overrides the input value
694
745
  // with the item label.
@@ -756,11 +807,26 @@ export const MultiSelectComboBoxMixin = (superClass) =>
756
807
  __updateTopGroup(selectedItemsOnTop, selectedItems, opened) {
757
808
  if (!selectedItemsOnTop) {
758
809
  this._topGroup = [];
759
- } else if (!opened) {
810
+ } else if (!opened || this.__needToSyncTopGroup()) {
760
811
  this._topGroup = [...selectedItems];
761
812
  }
762
813
  }
763
814
 
815
+ /** @private */
816
+ __needToSyncTopGroup() {
817
+ // Only sync for object items
818
+ if (!this.itemIdPath) {
819
+ return false;
820
+ }
821
+ return (
822
+ this._topGroup &&
823
+ this._topGroup.some((item) => {
824
+ const selectedItem = this.selectedItems[this._findIndex(item, this.selectedItems, this.itemIdPath)];
825
+ return selectedItem && item !== selectedItem;
826
+ })
827
+ );
828
+ }
829
+
764
830
  /** @private */
765
831
  __createChip(item) {
766
832
  const chip = document.createElement('vaadin-multi-select-combo-box-chip');
@@ -910,26 +976,20 @@ export const MultiSelectComboBoxMixin = (superClass) =>
910
976
  }
911
977
  }
912
978
 
913
- /** @private */
914
- _onClearButtonTouchend(event) {
915
- // Cancel the following click and focus events
916
- event.preventDefault();
917
- // Prevent default combo box behavior which can otherwise unnecessarily
918
- // clear the input and filter
919
- event.stopPropagation();
920
-
921
- this.clear();
922
- }
923
-
924
979
  /**
925
- * Override method inherited from `InputControlMixin` and clear items.
980
+ * Override method from `ComboBoxBaseMixin` to deselect
981
+ * dropdown item by requesting content update on clear.
982
+ * @param {Event} event
926
983
  * @protected
927
- * @override
928
984
  */
929
985
  _onClearButtonClick(event) {
930
986
  event.stopPropagation();
931
987
 
932
- this.clear();
988
+ super._onClearButtonClick(event);
989
+
990
+ if (this.opened) {
991
+ this.requestContentUpdate();
992
+ }
933
993
  }
934
994
 
935
995
  /**
@@ -954,10 +1014,86 @@ export const MultiSelectComboBoxMixin = (superClass) =>
954
1014
  * @override
955
1015
  */
956
1016
  _onEscape(event) {
957
- if (this.clearButtonVisible && this.selectedItems && this.selectedItems.length) {
1017
+ if (this.readonly) {
1018
+ event.stopPropagation();
1019
+ if (this.opened) {
1020
+ this.close();
1021
+ }
1022
+ return;
1023
+ }
1024
+
1025
+ if (this.clearButtonVisible && !this.opened && this.selectedItems && this.selectedItems.length) {
958
1026
  event.stopPropagation();
959
1027
  this.selectedItems = [];
960
1028
  }
1029
+
1030
+ super._onEscape(event);
1031
+ }
1032
+
1033
+ /**
1034
+ * Override method from `ComboBoxBaseMixin` to handle Escape pres.
1035
+ * @protected
1036
+ * @override
1037
+ */
1038
+ _onEscapeCancel() {
1039
+ this._closeOrCommit();
1040
+ }
1041
+
1042
+ /**
1043
+ * Override an event listener from `KeyboardMixin` to keep
1044
+ * overlay open when item is selected or unselected.
1045
+ * @param {!Event} event
1046
+ * @protected
1047
+ * @override
1048
+ */
1049
+ _onEnter(event) {
1050
+ if (this.opened) {
1051
+ // Do not submit the surrounding form.
1052
+ event.preventDefault();
1053
+ // Do not trigger global listeners.
1054
+ event.stopPropagation();
1055
+
1056
+ if (this.readonly) {
1057
+ this.close();
1058
+ } else if (this._hasValidInputValue()) {
1059
+ // Keep selected item focused after committing on Enter.
1060
+ const focusedItem = this._dropdownItems[this._focusedIndex];
1061
+ this._commitValue();
1062
+ this._focusedIndex = this._dropdownItems.indexOf(focusedItem);
1063
+ }
1064
+
1065
+ return;
1066
+ }
1067
+
1068
+ super._onEnter(event);
1069
+ }
1070
+
1071
+ /**
1072
+ * Override method inherited from the combo-box
1073
+ * to not update focused item when readonly.
1074
+ * @protected
1075
+ * @override
1076
+ */
1077
+ _onArrowDown() {
1078
+ if (!this.readonly) {
1079
+ super._onArrowDown();
1080
+ } else if (!this.opened) {
1081
+ this.open();
1082
+ }
1083
+ }
1084
+
1085
+ /**
1086
+ * Override method inherited from the combo-box
1087
+ * to not update focused item when readonly.
1088
+ * @protected
1089
+ * @override
1090
+ */
1091
+ _onArrowUp() {
1092
+ if (!this.readonly) {
1093
+ super._onArrowUp();
1094
+ } else if (!this.opened) {
1095
+ this.open();
1096
+ }
961
1097
  }
962
1098
 
963
1099
  /**
@@ -1087,36 +1223,29 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1087
1223
  }
1088
1224
  }
1089
1225
 
1090
- /** @private */
1091
- _onComboBoxChange() {
1092
- const item = this.$.comboBox.selectedItem;
1093
- if (item) {
1094
- this.__selectItem(item);
1095
- }
1096
- }
1097
-
1098
- /** @private */
1099
- _onComboBoxItemSelected(event) {
1100
- this.__selectItem(event.detail.item);
1101
- }
1102
-
1103
- /** @private */
1104
- _onCustomValueSet(event) {
1105
- // Do not set combo-box value
1106
- event.preventDefault();
1107
-
1108
- // Stop the original event
1226
+ /**
1227
+ * @param {CustomEvent} event
1228
+ * @protected
1229
+ * @override
1230
+ */
1231
+ _overlaySelectedItemChanged(event) {
1109
1232
  event.stopPropagation();
1110
1233
 
1111
- this.__clearInternalValue(true);
1234
+ // Do not un-select on click when readonly
1235
+ if (this.readonly) {
1236
+ return;
1237
+ }
1112
1238
 
1113
- this.dispatchEvent(
1114
- new CustomEvent('custom-value-set', {
1115
- detail: event.detail,
1116
- composed: true,
1117
- bubbles: true,
1118
- }),
1119
- );
1239
+ if (event.detail.item instanceof ComboBoxPlaceholder) {
1240
+ return;
1241
+ }
1242
+
1243
+ if (this.opened) {
1244
+ // Store filter value for checking if user input is matching
1245
+ // an item which is already selected, to not un-select it.
1246
+ this._lastFilter = this._inputElementValue;
1247
+ this.__selectItem(event.detail.item);
1248
+ }
1120
1249
  }
1121
1250
 
1122
1251
  /** @private */