@vaadin/multi-select-combo-box 24.8.4 → 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 (44) hide show
  1. package/README.md +0 -23
  2. package/package.json +18 -19
  3. package/src/{vaadin-multi-select-combo-box-styles.d.ts → styles/vaadin-multi-select-combo-box-base-styles.d.ts} +1 -3
  4. package/src/styles/vaadin-multi-select-combo-box-base-styles.js +58 -0
  5. package/src/styles/vaadin-multi-select-combo-box-chip-base-styles.js +112 -0
  6. package/src/styles/vaadin-multi-select-combo-box-chip-core-styles.js +33 -0
  7. package/{theme/material/vaadin-multi-select-combo-box-chip-styles.d.ts → src/styles/vaadin-multi-select-combo-box-core-styles.d.ts} +3 -3
  8. package/src/{vaadin-multi-select-combo-box-styles.js → styles/vaadin-multi-select-combo-box-core-styles.js} +6 -29
  9. package/src/styles/vaadin-multi-select-combo-box-overlay-base-styles.js +19 -0
  10. package/src/styles/vaadin-multi-select-combo-box-overlay-core-styles.js +21 -0
  11. package/src/styles/vaadin-multi-select-combo-box-scroller-base-styles.js +8 -0
  12. package/src/styles/vaadin-multi-select-combo-box-scroller-core-styles.js +27 -0
  13. package/src/vaadin-multi-select-combo-box-chip.js +17 -11
  14. package/src/vaadin-multi-select-combo-box-container.js +27 -34
  15. package/src/vaadin-multi-select-combo-box-item.js +13 -12
  16. package/src/vaadin-multi-select-combo-box-mixin.d.ts +9 -82
  17. package/src/vaadin-multi-select-combo-box-mixin.js +380 -268
  18. package/src/vaadin-multi-select-combo-box-overlay.js +15 -25
  19. package/src/vaadin-multi-select-combo-box-scroller.js +10 -26
  20. package/src/vaadin-multi-select-combo-box.d.ts +14 -10
  21. package/src/vaadin-multi-select-combo-box.js +52 -69
  22. package/theme/lumo/vaadin-multi-select-combo-box-styles.js +4 -1
  23. package/web-types.json +207 -230
  24. package/web-types.lit.json +78 -78
  25. package/src/vaadin-lit-multi-select-combo-box-chip.js +0 -88
  26. package/src/vaadin-lit-multi-select-combo-box-container.js +0 -66
  27. package/src/vaadin-lit-multi-select-combo-box-internal.js +0 -56
  28. package/src/vaadin-lit-multi-select-combo-box-item.js +0 -68
  29. package/src/vaadin-lit-multi-select-combo-box-overlay.js +0 -64
  30. package/src/vaadin-lit-multi-select-combo-box-scroller.js +0 -96
  31. package/src/vaadin-lit-multi-select-combo-box.js +0 -146
  32. package/src/vaadin-multi-select-combo-box-internal-mixin.js +0 -449
  33. package/src/vaadin-multi-select-combo-box-internal.js +0 -51
  34. package/theme/lumo/vaadin-lit-multi-select-combo-box.d.ts +0 -3
  35. package/theme/lumo/vaadin-lit-multi-select-combo-box.js +0 -3
  36. package/theme/material/vaadin-lit-multi-select-combo-box.d.ts +0 -3
  37. package/theme/material/vaadin-lit-multi-select-combo-box.js +0 -3
  38. package/theme/material/vaadin-multi-select-combo-box-chip-styles.js +0 -102
  39. package/theme/material/vaadin-multi-select-combo-box-styles.d.ts +0 -9
  40. package/theme/material/vaadin-multi-select-combo-box-styles.js +0 -92
  41. package/theme/material/vaadin-multi-select-combo-box.d.ts +0 -8
  42. package/theme/material/vaadin-multi-select-combo-box.js +0 -8
  43. package/vaadin-lit-multi-select-combo-box.d.ts +0 -1
  44. package/vaadin-lit-multi-select-combo-box.js +0 -2
@@ -4,9 +4,11 @@
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
- import { processTemplates } from '@vaadin/component-base/src/templates.js';
10
12
  import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
11
13
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
12
14
  import { InputController } from '@vaadin/field-base/src/input-controller.js';
@@ -14,11 +16,15 @@ import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-c
14
16
 
15
17
  /**
16
18
  * @polymerMixin
19
+ * @mixes ComboBoxDataProviderMixin
20
+ * @mixes ComboBoxItemsMixin
17
21
  * @mixes InputControlMixin
18
22
  * @mixes ResizeMixin
19
23
  */
20
24
  export const MultiSelectComboBoxMixin = (superClass) =>
21
- class MultiSelectComboBoxMixinClass extends InputControlMixin(ResizeMixin(superClass)) {
25
+ class MultiSelectComboBoxMixinClass extends ComboBoxDataProviderMixin(
26
+ ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass))),
27
+ ) {
22
28
  static get properties() {
23
29
  return {
24
30
  /**
@@ -30,7 +36,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
30
36
  type: Boolean,
31
37
  value: false,
32
38
  reflectToAttribute: true,
33
- observer: '_autoExpandHorizontallyChanged',
34
39
  sync: true,
35
40
  },
36
41
 
@@ -44,37 +49,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
44
49
  type: Boolean,
45
50
  value: false,
46
51
  reflectToAttribute: true,
47
- observer: '_autoExpandVerticallyChanged',
48
- sync: true,
49
- },
50
-
51
- /**
52
- * Set true to prevent the overlay from opening automatically.
53
- * @attr {boolean} auto-open-disabled
54
- */
55
- autoOpenDisabled: {
56
- type: Boolean,
57
- sync: true,
58
- },
59
-
60
- /**
61
- * Set to true to display the clear icon which clears the input.
62
- * @attr {boolean} clear-button-visible
63
- */
64
- clearButtonVisible: {
65
- type: Boolean,
66
- reflectToAttribute: true,
67
- observer: '_clearButtonVisibleChanged',
68
- value: false,
69
- sync: true,
70
- },
71
-
72
- /**
73
- * A full set of items to filter the visible options from.
74
- * The items can be of either `String` or `Object` type.
75
- */
76
- items: {
77
- type: Array,
78
52
  sync: true,
79
53
  },
80
54
 
@@ -86,28 +60,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
86
60
  */
87
61
  itemClassNameGenerator: {
88
62
  type: Object,
89
- observer: '__itemClassNameGeneratorChanged',
90
- sync: true,
91
- },
92
-
93
- /**
94
- * The item property used for a visual representation of the item.
95
- * @attr {string} item-label-path
96
- */
97
- itemLabelPath: {
98
- type: String,
99
- value: 'label',
100
- sync: true,
101
- },
102
-
103
- /**
104
- * Path for the value of the item. If `items` is an array of objects,
105
- * this property is used as a string value for the selected item.
106
- * @attr {string} item-value-path
107
- */
108
- itemValuePath: {
109
- type: String,
110
- value: 'value',
111
63
  sync: true,
112
64
  },
113
65
 
@@ -175,23 +127,12 @@ export const MultiSelectComboBoxMixin = (superClass) =>
175
127
  sync: true,
176
128
  },
177
129
 
178
- /**
179
- * A space-delimited list of CSS class names to set on the overlay element.
180
- *
181
- * @attr {string} overlay-class
182
- */
183
- overlayClass: {
184
- type: String,
185
- sync: true,
186
- },
187
-
188
130
  /**
189
131
  * When present, it specifies that the field is read-only.
190
132
  */
191
133
  readonly: {
192
134
  type: Boolean,
193
135
  value: false,
194
- observer: '_readonlyChanged',
195
136
  reflectToAttribute: true,
196
137
  sync: true,
197
138
  },
@@ -207,53 +148,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
207
148
  sync: true,
208
149
  },
209
150
 
210
- /**
211
- * True if the dropdown is open, false otherwise.
212
- */
213
- opened: {
214
- type: Boolean,
215
- notify: true,
216
- value: false,
217
- reflectToAttribute: true,
218
- sync: true,
219
- },
220
-
221
- /**
222
- * Total number of items.
223
- */
224
- size: {
225
- type: Number,
226
- sync: true,
227
- },
228
-
229
- /**
230
- * Number of items fetched at a time from the data provider.
231
- * @attr {number} page-size
232
- */
233
- pageSize: {
234
- type: Number,
235
- value: 50,
236
- observer: '_pageSizeChanged',
237
- sync: true,
238
- },
239
-
240
- /**
241
- * Function that provides items lazily. Receives two arguments:
242
- *
243
- * - `params` - Object with the following properties:
244
- * - `params.page` Requested page index
245
- * - `params.pageSize` Current page size
246
- * - `params.filter` Currently applied filter
247
- *
248
- * - `callback(items, size)` - Callback function with arguments:
249
- * - `items` Current page of items
250
- * - `size` Total number of items.
251
- */
252
- dataProvider: {
253
- type: Object,
254
- sync: true,
255
- },
256
-
257
151
  /**
258
152
  * When true, the user can input a value that is not present in the items list.
259
153
  * @attr {boolean} allow-custom-value
@@ -271,6 +165,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
271
165
  placeholder: {
272
166
  type: String,
273
167
  observer: '_placeholderChanged',
168
+ reflectToAttribute: true,
274
169
  sync: true,
275
170
  },
276
171
 
@@ -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,10 +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);
433
330
 
434
- processTemplates(this);
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
+ }
435
360
  }
436
361
 
437
362
  /**
@@ -442,6 +367,17 @@ export const MultiSelectComboBoxMixin = (superClass) =>
442
367
  return this.required && !this.readonly ? this._hasValue : true;
443
368
  }
444
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
+
445
381
  /**
446
382
  * Clears the selected items.
447
383
  */
@@ -453,11 +389,15 @@ export const MultiSelectComboBoxMixin = (superClass) =>
453
389
 
454
390
  /**
455
391
  * Clears the cached pages and reloads data from data provider when needed.
392
+ * @override
456
393
  */
457
394
  clearCache() {
458
- if (this.$ && this.$.comboBox) {
459
- this.$.comboBox.clearCache();
395
+ // Do not clear the data provider cache when read-only.
396
+ if (this.readonly) {
397
+ return;
460
398
  }
399
+
400
+ super.clearCache();
461
401
  }
462
402
 
463
403
  /**
@@ -467,34 +407,130 @@ export const MultiSelectComboBoxMixin = (superClass) =>
467
407
  * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
468
408
  */
469
409
  requestContentUpdate() {
470
- if (this.$ && this.$.comboBox) {
471
- this.$.comboBox.requestContentUpdate();
410
+ if (!this._scroller) {
411
+ return;
472
412
  }
413
+
414
+ this._scroller.requestContentUpdate();
415
+
416
+ this._getItemElements().forEach((item) => {
417
+ item.requestContentUpdate();
418
+ });
473
419
  }
474
420
 
475
421
  /**
476
- * Override method inherited from `DisabledMixin` to forward disabled to chips.
422
+ * Override method from `ComboBoxBaseMixin` to implement clearing logic.
477
423
  * @protected
478
424
  * @override
479
425
  */
480
- _disabledChanged(disabled, oldDisabled) {
481
- super._disabledChanged(disabled, oldDisabled);
426
+ _onClearAction() {
427
+ this.clear();
428
+ }
482
429
 
483
- if (disabled || oldDisabled) {
484
- 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();
485
475
  }
486
476
  }
487
477
 
488
478
  /**
489
- * Override method inherited from `InputMixin` to forward the input to combo-box.
490
479
  * @protected
491
480
  * @override
492
481
  */
493
- _inputElementChanged(input) {
494
- 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
+ }
495
495
 
496
- if (input) {
497
- 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
+ }
498
534
  }
499
535
  }
500
536
 
@@ -504,6 +540,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
504
540
  * @protected
505
541
  */
506
542
  _setFocused(focused) {
543
+ if (!focused) {
544
+ this._ignoreCommitValue = true;
545
+ }
546
+
507
547
  super._setFocused(focused);
508
548
 
509
549
  // Do not validate when focusout is caused by document
@@ -512,6 +552,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
512
552
  this._focusedChipIndex = -1;
513
553
  this._requestValidation();
514
554
  }
555
+
556
+ if (!focused && this.readonly && !this._closeOnBlurIsPrevented) {
557
+ this.close();
558
+ }
515
559
  }
516
560
 
517
561
  /**
@@ -543,73 +587,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
543
587
  super._delegateAttribute(name, value);
544
588
  }
545
589
 
546
- /** @private */
547
- _autoExpandHorizontallyChanged(autoExpand, oldAutoExpand) {
548
- if (autoExpand || oldAutoExpand) {
549
- this.__updateChips();
550
- }
551
- }
552
-
553
- /** @private */
554
- _autoExpandVerticallyChanged(autoExpand, oldAutoExpand) {
555
- if (autoExpand || oldAutoExpand) {
556
- this.__updateChips();
557
- }
558
- }
559
-
560
- /**
561
- * Setting clear button visible reduces total space available
562
- * for rendering chips, and making it hidden increases it.
563
- * @private
564
- */
565
- _clearButtonVisibleChanged(visible, oldVisible) {
566
- if (visible || oldVisible) {
567
- this.__updateChips();
568
- }
569
- }
570
-
571
- /**
572
- * Implement two-way binding for the `filteredItems` property
573
- * that can be set on the internal combo-box element.
574
- *
575
- * @param {CustomEvent} event
576
- * @private
577
- */
578
- _onFilteredItemsChanged(event) {
579
- const { value } = event.detail;
580
- if (Array.isArray(value) || value == null) {
581
- this.filteredItems = value;
582
- }
583
- }
584
-
585
- /** @private */
586
- _readonlyChanged(readonly, oldReadonly) {
587
- if (readonly || oldReadonly) {
588
- this.__updateChips();
589
- }
590
-
591
- if (this.dataProvider) {
592
- this.clearCache();
593
- }
594
- }
595
-
596
- /** @private */
597
- __itemClassNameGeneratorChanged(generator, oldGenerator) {
598
- if (generator || oldGenerator) {
599
- this.__updateChips();
600
- }
601
- }
602
-
603
- /** @private */
604
- _pageSizeChanged(pageSize, oldPageSize) {
605
- if (Math.floor(pageSize) !== pageSize || pageSize <= 0) {
606
- this.pageSize = oldPageSize;
607
- console.error('"pageSize" value must be an integer > 0');
608
- }
609
-
610
- this.$.comboBox.pageSize = this.pageSize;
611
- }
612
-
613
590
  /** @private */
614
591
  _placeholderChanged(placeholder) {
615
592
  const tmpPlaceholder = this.__tmpA11yPlaceholder;
@@ -645,15 +622,90 @@ export const MultiSelectComboBoxMixin = (superClass) =>
645
622
 
646
623
  // Update selected for dropdown items
647
624
  this.requestContentUpdate();
625
+ }
648
626
 
649
- if (this.opened) {
650
- 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;
651
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;
683
+ }
684
+
685
+ this.__setDropdownItems(items);
652
686
  }
653
687
 
654
688
  /** @private */
655
- _getItemLabel(item) {
656
- 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
+ }
657
709
  }
658
710
 
659
711
  /** @private */
@@ -684,13 +736,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
684
736
  */
685
737
  __clearInternalValue(force = false) {
686
738
  if (!this.keepFilter || force) {
687
- // Clear both combo box value and filter.
739
+ // Clear both input value and filter.
688
740
  this.filter = '';
689
- this.$.comboBox.clear();
741
+ this._inputElementValue = '';
690
742
  } else {
691
- // Only clear combo box value. This effectively resets _lastCommittedValue
692
- // which allows toggling the same item multiple times via keyboard.
693
- this.$.comboBox.clear();
694
743
  // Restore input to the filter value. Needed when items are
695
744
  // navigated with keyboard, which overrides the input value
696
745
  // with the item label.
@@ -927,26 +976,20 @@ export const MultiSelectComboBoxMixin = (superClass) =>
927
976
  }
928
977
  }
929
978
 
930
- /** @private */
931
- _onClearButtonTouchend(event) {
932
- // Cancel the following click and focus events
933
- event.preventDefault();
934
- // Prevent default combo box behavior which can otherwise unnecessarily
935
- // clear the input and filter
936
- event.stopPropagation();
937
-
938
- this.clear();
939
- }
940
-
941
979
  /**
942
- * 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
943
983
  * @protected
944
- * @override
945
984
  */
946
985
  _onClearButtonClick(event) {
947
986
  event.stopPropagation();
948
987
 
949
- this.clear();
988
+ super._onClearButtonClick(event);
989
+
990
+ if (this.opened) {
991
+ this.requestContentUpdate();
992
+ }
950
993
  }
951
994
 
952
995
  /**
@@ -971,10 +1014,86 @@ export const MultiSelectComboBoxMixin = (superClass) =>
971
1014
  * @override
972
1015
  */
973
1016
  _onEscape(event) {
974
- 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) {
975
1026
  event.stopPropagation();
976
1027
  this.selectedItems = [];
977
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
+ }
978
1097
  }
979
1098
 
980
1099
  /**
@@ -1104,36 +1223,29 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1104
1223
  }
1105
1224
  }
1106
1225
 
1107
- /** @private */
1108
- _onComboBoxChange() {
1109
- const item = this.$.comboBox.selectedItem;
1110
- if (item) {
1111
- this.__selectItem(item);
1112
- }
1113
- }
1114
-
1115
- /** @private */
1116
- _onComboBoxItemSelected(event) {
1117
- this.__selectItem(event.detail.item);
1118
- }
1119
-
1120
- /** @private */
1121
- _onCustomValueSet(event) {
1122
- // Do not set combo-box value
1123
- event.preventDefault();
1124
-
1125
- // Stop the original event
1226
+ /**
1227
+ * @param {CustomEvent} event
1228
+ * @protected
1229
+ * @override
1230
+ */
1231
+ _overlaySelectedItemChanged(event) {
1126
1232
  event.stopPropagation();
1127
1233
 
1128
- this.__clearInternalValue(true);
1234
+ // Do not un-select on click when readonly
1235
+ if (this.readonly) {
1236
+ return;
1237
+ }
1129
1238
 
1130
- this.dispatchEvent(
1131
- new CustomEvent('custom-value-set', {
1132
- detail: event.detail,
1133
- composed: true,
1134
- bubbles: true,
1135
- }),
1136
- );
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
+ }
1137
1249
  }
1138
1250
 
1139
1251
  /** @private */