@vaadin/multi-select-combo-box 25.0.0-alpha8 → 25.0.0-beta1

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 (28) hide show
  1. package/package.json +15 -18
  2. package/src/styles/vaadin-multi-select-combo-box-base-styles.js +1 -1
  3. package/src/styles/vaadin-multi-select-combo-box-chip-base-styles.js +23 -9
  4. package/src/vaadin-multi-select-combo-box-chip.js +1 -1
  5. package/src/vaadin-multi-select-combo-box-container.js +1 -0
  6. package/src/vaadin-multi-select-combo-box-item.js +1 -1
  7. package/src/vaadin-multi-select-combo-box-mixin.d.ts +18 -89
  8. package/src/vaadin-multi-select-combo-box-mixin.js +426 -307
  9. package/src/vaadin-multi-select-combo-box-overlay.js +2 -2
  10. package/src/vaadin-multi-select-combo-box-scroller.js +1 -1
  11. package/src/vaadin-multi-select-combo-box.d.ts +13 -6
  12. package/src/vaadin-multi-select-combo-box.js +37 -88
  13. package/vaadin-multi-select-combo-box.js +1 -1
  14. package/web-types.json +205 -230
  15. package/web-types.lit.json +78 -78
  16. package/src/styles/vaadin-multi-select-combo-box-chip-core-styles.js +0 -33
  17. package/src/styles/vaadin-multi-select-combo-box-core-styles.d.ts +0 -8
  18. package/src/styles/vaadin-multi-select-combo-box-core-styles.js +0 -47
  19. package/src/styles/vaadin-multi-select-combo-box-overlay-core-styles.js +0 -21
  20. package/src/styles/vaadin-multi-select-combo-box-scroller-core-styles.js +0 -27
  21. package/src/vaadin-multi-select-combo-box-internal-mixin.js +0 -446
  22. package/src/vaadin-multi-select-combo-box-internal.js +0 -57
  23. package/theme/lumo/vaadin-multi-select-combo-box-chip-styles.d.ts +0 -10
  24. package/theme/lumo/vaadin-multi-select-combo-box-chip-styles.js +0 -107
  25. package/theme/lumo/vaadin-multi-select-combo-box-styles.d.ts +0 -10
  26. package/theme/lumo/vaadin-multi-select-combo-box-styles.js +0 -121
  27. package/theme/lumo/vaadin-multi-select-combo-box.d.ts +0 -8
  28. package/theme/lumo/vaadin-multi-select-combo-box.js +0 -8
@@ -4,6 +4,10 @@
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';
10
+ import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
7
11
  import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
8
12
  import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
9
13
  import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
@@ -11,13 +15,27 @@ import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js
11
15
  import { InputController } from '@vaadin/field-base/src/input-controller.js';
12
16
  import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
13
17
 
18
+ const DEFAULT_I18N = {
19
+ cleared: 'Selection cleared',
20
+ focused: 'focused. Press Backspace to remove',
21
+ selected: 'added to selection',
22
+ deselected: 'removed from selection',
23
+ total: '{count} items selected',
24
+ };
25
+
14
26
  /**
15
27
  * @polymerMixin
28
+ * @mixes ComboBoxDataProviderMixin
29
+ * @mixes ComboBoxItemsMixin
30
+ * @mixes I18nMixin
16
31
  * @mixes InputControlMixin
17
32
  * @mixes ResizeMixin
18
33
  */
19
34
  export const MultiSelectComboBoxMixin = (superClass) =>
20
- class MultiSelectComboBoxMixinClass extends InputControlMixin(ResizeMixin(superClass)) {
35
+ class MultiSelectComboBoxMixinClass extends I18nMixin(
36
+ DEFAULT_I18N,
37
+ ComboBoxDataProviderMixin(ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass)))),
38
+ ) {
21
39
  static get properties() {
22
40
  return {
23
41
  /**
@@ -29,7 +47,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
29
47
  type: Boolean,
30
48
  value: false,
31
49
  reflectToAttribute: true,
32
- observer: '_autoExpandHorizontallyChanged',
33
50
  sync: true,
34
51
  },
35
52
 
@@ -43,37 +60,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
43
60
  type: Boolean,
44
61
  value: false,
45
62
  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
63
  sync: true,
78
64
  },
79
65
 
@@ -85,28 +71,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
85
71
  */
86
72
  itemClassNameGenerator: {
87
73
  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
74
  sync: true,
111
75
  },
112
76
 
@@ -119,43 +83,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
119
83
  sync: true,
120
84
  },
121
85
 
122
- /**
123
- * The object used to localize this component.
124
- * To change the default localization, replace the entire
125
- * _i18n_ object or just the property you want to modify.
126
- *
127
- * The object has the following JSON structure and default values:
128
- * ```
129
- * {
130
- * // Screen reader announcement on clear button click.
131
- * cleared: 'Selection cleared',
132
- * // Screen reader announcement when a chip is focused.
133
- * focused: ' focused. Press Backspace to remove',
134
- * // Screen reader announcement when item is selected.
135
- * selected: 'added to selection',
136
- * // Screen reader announcement when item is deselected.
137
- * deselected: 'removed from selection',
138
- * // Screen reader announcement of the selected items count.
139
- * // {count} is replaced with the actual count of items.
140
- * total: '{count} items selected',
141
- * }
142
- * ```
143
- * @type {!MultiSelectComboBoxI18n}
144
- * @default {English/US}
145
- */
146
- i18n: {
147
- type: Object,
148
- value: () => {
149
- return {
150
- cleared: 'Selection cleared',
151
- focused: 'focused. Press Backspace to remove',
152
- selected: 'added to selection',
153
- deselected: 'removed from selection',
154
- total: '{count} items selected',
155
- };
156
- },
157
- },
158
-
159
86
  /**
160
87
  * When true, filter string isn't cleared after selecting an item.
161
88
  */
@@ -174,23 +101,12 @@ export const MultiSelectComboBoxMixin = (superClass) =>
174
101
  sync: true,
175
102
  },
176
103
 
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
104
  /**
188
105
  * When present, it specifies that the field is read-only.
189
106
  */
190
107
  readonly: {
191
108
  type: Boolean,
192
109
  value: false,
193
- observer: '_readonlyChanged',
194
110
  reflectToAttribute: true,
195
111
  sync: true,
196
112
  },
@@ -206,53 +122,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
206
122
  sync: true,
207
123
  },
208
124
 
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
125
  /**
257
126
  * When true, the user can input a value that is not present in the items list.
258
127
  * @attr {boolean} allow-custom-value
@@ -290,26 +159,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
290
159
  sync: true,
291
160
  },
292
161
 
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
162
  /**
314
163
  * Set to true to group selected items at the top of the overlay.
315
164
  * @attr {boolean} selected-items-on-top
@@ -348,6 +197,13 @@ export const MultiSelectComboBoxMixin = (superClass) =>
348
197
  /** @private */
349
198
  _topGroup: {
350
199
  type: Array,
200
+ observer: '_topGroupChanged',
201
+ sync: true,
202
+ },
203
+
204
+ /** @private */
205
+ _inputField: {
206
+ type: Object,
351
207
  },
352
208
  };
353
209
  }
@@ -355,11 +211,44 @@ export const MultiSelectComboBoxMixin = (superClass) =>
355
211
  static get observers() {
356
212
  return [
357
213
  '_selectedItemsChanged(selectedItems)',
214
+ '__openedOrItemsChanged(opened, _dropdownItems, loading, __keepOverlayOpened)',
358
215
  '__updateOverflowChip(_overflow, _overflowItems, disabled, readonly)',
216
+ '__updateScroller(opened, _dropdownItems, _focusedIndex, _theme)',
359
217
  '__updateTopGroup(selectedItemsOnTop, selectedItems, opened)',
360
218
  ];
361
219
  }
362
220
 
221
+ /**
222
+ * The object used to localize this component. To change the default
223
+ * localization, replace this with an object that provides all properties, or
224
+ * just the individual properties you want to change.
225
+ *
226
+ * The object has the following JSON structure and default values:
227
+ * ```js
228
+ * {
229
+ * // Screen reader announcement on clear button click.
230
+ * cleared: 'Selection cleared',
231
+ * // Screen reader announcement when a chip is focused.
232
+ * focused: ' focused. Press Backspace to remove',
233
+ * // Screen reader announcement when item is selected.
234
+ * selected: 'added to selection',
235
+ * // Screen reader announcement when item is deselected.
236
+ * deselected: 'removed from selection',
237
+ * // Screen reader announcement of the selected items count.
238
+ * // {count} is replaced with the actual count of items.
239
+ * total: '{count} items selected',
240
+ * }
241
+ * ```
242
+ * @return {!MultiSelectComboBoxI18n}
243
+ */
244
+ get i18n() {
245
+ return super.i18n;
246
+ }
247
+
248
+ set i18n(value) {
249
+ super.i18n = value;
250
+ }
251
+
363
252
  /** @protected */
364
253
  get slotStyles() {
365
254
  const tag = this.localName;
@@ -399,6 +288,15 @@ export const MultiSelectComboBoxMixin = (superClass) =>
399
288
  return this.selectedItems && this.selectedItems.length > 0;
400
289
  }
401
290
 
291
+ /**
292
+ * Tag name prefix used by scroller and items.
293
+ * @protected
294
+ * @return {string}
295
+ */
296
+ get _tagNamePrefix() {
297
+ return 'vaadin-multi-select-combo-box';
298
+ }
299
+
402
300
  /** @protected */
403
301
  ready() {
404
302
  super.ready();
@@ -419,6 +317,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
419
317
  this._tooltipController.setAriaTarget(this.inputElement);
420
318
  this._tooltipController.setShouldShow((target) => !target.opened);
421
319
 
320
+ this._toggleElement = this.$.toggleButton;
422
321
  this._inputField = this.shadowRoot.querySelector('[part="input-field"]');
423
322
 
424
323
  this._overflowController = new SlotController(this, 'overflow', 'vaadin-multi-select-combo-box-chip', {
@@ -428,8 +327,41 @@ export const MultiSelectComboBoxMixin = (superClass) =>
428
327
  },
429
328
  });
430
329
  this.addController(this._overflowController);
330
+ }
431
331
 
432
- this.__updateChips();
332
+ /** @protected */
333
+ updated(props) {
334
+ super.updated(props);
335
+
336
+ ['loading', 'itemIdPath', 'itemClassNameGenerator', 'renderer'].forEach((prop) => {
337
+ if (props.has(prop)) {
338
+ this._scroller[prop] = this[prop];
339
+ }
340
+ });
341
+
342
+ if (props.has('selectedItems') && this.opened) {
343
+ this.$.overlay._updateOverlayWidth();
344
+ }
345
+
346
+ const chipProps = [
347
+ 'autoExpandHorizontally',
348
+ 'autoExpandVertically',
349
+ 'disabled',
350
+ 'readonly',
351
+ 'clearButtonVisible',
352
+ 'itemClassNameGenerator',
353
+ ];
354
+ if (chipProps.some((prop) => props.has(prop))) {
355
+ this.__updateChips();
356
+ }
357
+
358
+ if (props.has('readonly')) {
359
+ this._setDropdownItems(this.filteredItems);
360
+
361
+ if (this.dataProvider) {
362
+ this.clearCache();
363
+ }
364
+ }
433
365
  }
434
366
 
435
367
  /**
@@ -440,22 +372,37 @@ export const MultiSelectComboBoxMixin = (superClass) =>
440
372
  return this.required && !this.readonly ? this._hasValue : true;
441
373
  }
442
374
 
375
+ /**
376
+ * Opens the dropdown list.
377
+ * @override
378
+ */
379
+ open() {
380
+ // Allow opening dropdown when readonly.
381
+ if (!this.disabled && !(this.readonly && this.selectedItems.length === 0)) {
382
+ this.opened = true;
383
+ }
384
+ }
385
+
443
386
  /**
444
387
  * Clears the selected items.
445
388
  */
446
389
  clear() {
447
390
  this.__updateSelection([]);
448
391
 
449
- announce(this.i18n.cleared);
392
+ announce(this.__effectiveI18n.cleared);
450
393
  }
451
394
 
452
395
  /**
453
396
  * Clears the cached pages and reloads data from data provider when needed.
397
+ * @override
454
398
  */
455
399
  clearCache() {
456
- if (this.$ && this.$.comboBox) {
457
- this.$.comboBox.clearCache();
400
+ // Do not clear the data provider cache when read-only.
401
+ if (this.readonly) {
402
+ return;
458
403
  }
404
+
405
+ super.clearCache();
459
406
  }
460
407
 
461
408
  /**
@@ -465,34 +412,130 @@ export const MultiSelectComboBoxMixin = (superClass) =>
465
412
  * It is not guaranteed that the update happens immediately (synchronously) after it is requested.
466
413
  */
467
414
  requestContentUpdate() {
468
- if (this.$ && this.$.comboBox) {
469
- this.$.comboBox.requestContentUpdate();
415
+ if (!this._scroller) {
416
+ return;
470
417
  }
418
+
419
+ this._scroller.requestContentUpdate();
420
+
421
+ this._getItemElements().forEach((item) => {
422
+ item.requestContentUpdate();
423
+ });
471
424
  }
472
425
 
473
426
  /**
474
- * Override method inherited from `DisabledMixin` to forward disabled to chips.
427
+ * Override method from `ComboBoxBaseMixin` to implement clearing logic.
475
428
  * @protected
476
429
  * @override
477
430
  */
478
- _disabledChanged(disabled, oldDisabled) {
479
- super._disabledChanged(disabled, oldDisabled);
431
+ _onClearAction() {
432
+ this.clear();
433
+ }
480
434
 
481
- if (disabled || oldDisabled) {
482
- this.__updateChips();
435
+ /**
436
+ * Override method from `ComboBoxBaseMixin`
437
+ * to commit value on overlay closing.
438
+ * @protected
439
+ * @override
440
+ */
441
+ _onClosed() {
442
+ // Do not commit selected item again on outside click
443
+ this._ignoreCommitValue = true;
444
+
445
+ if (!this.loading || this.allowCustomValue) {
446
+ this._commitValue();
447
+ }
448
+ }
449
+
450
+ /** @private */
451
+ __updateScroller(opened, items, focusedIndex, theme) {
452
+ if (opened) {
453
+ this._scroller.style.maxHeight =
454
+ getComputedStyle(this).getPropertyValue(`--${this._tagNamePrefix}-overlay-max-height`) || '65vh';
455
+ }
456
+
457
+ this._scroller.setProperties({
458
+ items: opened ? items : [],
459
+ opened,
460
+ focusedIndex,
461
+ theme,
462
+ });
463
+ }
464
+
465
+ /** @private */
466
+ __openedOrItemsChanged(opened, items, loading, keepOverlayOpened) {
467
+ // Close the overlay if there are no items to display.
468
+ // See https://github.com/vaadin/vaadin-combo-box/pull/964
469
+ this._overlayOpened = opened && (keepOverlayOpened || loading || !!(items && items.length));
470
+ }
471
+
472
+ /**
473
+ * @protected
474
+ */
475
+ _closeOrCommit() {
476
+ if (!this.opened) {
477
+ this._commitValue();
478
+ } else {
479
+ this.close();
483
480
  }
484
481
  }
485
482
 
486
483
  /**
487
- * Override method inherited from `InputMixin` to forward the input to combo-box.
488
484
  * @protected
489
485
  * @override
490
486
  */
491
- _inputElementChanged(input) {
492
- super._inputElementChanged(input);
487
+ _commitValue() {
488
+ // Store filter value for checking if user input is matching
489
+ // an item which is already selected, to not un-select it.
490
+ this._lastFilter = this.filter;
491
+
492
+ // Do not commit focused item on not blur / outside click
493
+ if (this._ignoreCommitValue) {
494
+ this._inputElementValue = '';
495
+ this._focusedIndex = -1;
496
+ this._ignoreCommitValue = false;
497
+ } else {
498
+ this.__commitUserInput();
499
+ }
500
+
501
+ // Clear filter unless keepFilter is set
502
+ if (!this.keepFilter || !this.opened) {
503
+ this.filter = '';
504
+ }
505
+ }
493
506
 
494
- if (input) {
495
- this.$.comboBox._setInputElement(input);
507
+ /** @private */
508
+ __commitUserInput() {
509
+ if (this._focusedIndex > -1) {
510
+ const focusedItem = this._dropdownItems[this._focusedIndex];
511
+ this.__selectItem(focusedItem);
512
+ } else if (this._inputElementValue) {
513
+ // Detect if input value doesn't match an existing item
514
+ const items = [...this._dropdownItems];
515
+ const itemMatchingInputValue = items[this.__getItemIndexByLabel(items, this._inputElementValue)];
516
+
517
+ if (this.allowCustomValue && !itemMatchingInputValue) {
518
+ const customValue = this._inputElementValue;
519
+
520
+ // Store reference to the last custom value for checking it on focusout.
521
+ this._lastCustomValue = customValue;
522
+
523
+ this.__clearInternalValue(true);
524
+
525
+ this.dispatchEvent(
526
+ new CustomEvent('custom-value-set', {
527
+ detail: customValue,
528
+ composed: true,
529
+ bubbles: true,
530
+ }),
531
+ );
532
+ } else if (!this.allowCustomValue && !this.opened && itemMatchingInputValue) {
533
+ // An item matching by label was found, select it.
534
+ this.__selectItem(itemMatchingInputValue);
535
+ } else {
536
+ // Clear input value on Escape press while closed.
537
+ this._inputElementValue = '';
538
+ }
496
539
  }
497
540
  }
498
541
 
@@ -502,6 +545,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
502
545
  * @protected
503
546
  */
504
547
  _setFocused(focused) {
548
+ if (!focused) {
549
+ this._ignoreCommitValue = true;
550
+ }
551
+
505
552
  super._setFocused(focused);
506
553
 
507
554
  // Do not validate when focusout is caused by document
@@ -510,6 +557,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
510
557
  this._focusedChipIndex = -1;
511
558
  this._requestValidation();
512
559
  }
560
+
561
+ if (!focused && this.readonly && !this._closeOnBlurIsPrevented) {
562
+ this.close();
563
+ }
513
564
  }
514
565
 
515
566
  /**
@@ -541,73 +592,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
541
592
  super._delegateAttribute(name, value);
542
593
  }
543
594
 
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
595
  /** @private */
612
596
  _placeholderChanged(placeholder) {
613
597
  const tmpPlaceholder = this.__tmpA11yPlaceholder;
@@ -643,15 +627,90 @@ export const MultiSelectComboBoxMixin = (superClass) =>
643
627
 
644
628
  // Update selected for dropdown items
645
629
  this.requestContentUpdate();
630
+ }
646
631
 
647
- if (this.opened) {
648
- this.$.comboBox._updateOverlayWidth();
632
+ /** @private */
633
+ _topGroupChanged(topGroup) {
634
+ if (topGroup) {
635
+ this._setDropdownItems(this.filteredItems);
636
+ }
637
+ }
638
+
639
+ /**
640
+ * Override method from `ComboBoxBaseMixin` to handle valid value.
641
+ * @protected
642
+ * @override
643
+ */
644
+ _hasValidInputValue() {
645
+ const hasInvalidOption = this._focusedIndex < 0 && this._inputElementValue !== '';
646
+ return this.allowCustomValue || !hasInvalidOption;
647
+ }
648
+
649
+ /**
650
+ * Override method inherited from the combo-box
651
+ * to not request data provider when read-only.
652
+ *
653
+ * @protected
654
+ * @override
655
+ */
656
+ _shouldFetchData() {
657
+ if (this.readonly) {
658
+ return false;
659
+ }
660
+
661
+ return super._shouldFetchData();
662
+ }
663
+
664
+ /**
665
+ * Override combo-box method to group selected
666
+ * items at the top of the overlay.
667
+ *
668
+ * @protected
669
+ * @override
670
+ */
671
+ _setDropdownItems(items) {
672
+ if (this.readonly) {
673
+ this.__setDropdownItems(this.selectedItems);
674
+ return;
675
+ }
676
+
677
+ if (this.filter || !this.selectedItemsOnTop) {
678
+ this.__setDropdownItems(items);
679
+ return;
680
+ }
681
+
682
+ if (items && items.length && this._topGroup && this._topGroup.length) {
683
+ // Filter out items included to the top group.
684
+ const filteredItems = items.filter((item) => this._findIndex(item, this._topGroup, this.itemIdPath) === -1);
685
+
686
+ this.__setDropdownItems(this._topGroup.concat(filteredItems));
687
+ return;
649
688
  }
689
+
690
+ this.__setDropdownItems(items);
650
691
  }
651
692
 
652
693
  /** @private */
653
- _getItemLabel(item) {
654
- return this.$.comboBox._getItemLabel(item);
694
+ __setDropdownItems(newItems) {
695
+ const oldItems = this._dropdownItems;
696
+ this._dropdownItems = newItems;
697
+
698
+ // Store the currently focused item if any. The focused index preserves
699
+ // in the case when more filtered items are loading but it is reset
700
+ // when the user types in a filter query.
701
+ const focusedItem = oldItems ? oldItems[this._focusedIndex] : null;
702
+
703
+ // Try to first set focus on the item that had been focused before `newItems` were updated
704
+ // if it is still present in the `newItems` array. Otherwise, set the focused index
705
+ // depending on the selected item or the filter query.
706
+ const focusedItemIndex = this.__getItemIndexByValue(newItems, this._getItemValue(focusedItem));
707
+ if (focusedItemIndex > -1) {
708
+ this._focusedIndex = focusedItemIndex;
709
+ } else {
710
+ // When the user filled in something that is different from the current value = filtering is enabled,
711
+ // set the focused index to the item that matches the filter query.
712
+ this._focusedIndex = this.__getItemIndexByLabel(newItems, this.filter);
713
+ }
655
714
  }
656
715
 
657
716
  /** @private */
@@ -682,13 +741,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
682
741
  */
683
742
  __clearInternalValue(force = false) {
684
743
  if (!this.keepFilter || force) {
685
- // Clear both combo box value and filter.
744
+ // Clear both input value and filter.
686
745
  this.filter = '';
687
- this.$.comboBox.clear();
746
+ this._inputElementValue = '';
688
747
  } 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
748
  // Restore input to the filter value. Needed when items are
693
749
  // navigated with keyboard, which overrides the input value
694
750
  // with the item label.
@@ -699,8 +755,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
699
755
  /** @private */
700
756
  __announceItem(itemLabel, isSelected, itemCount) {
701
757
  const state = isSelected ? 'selected' : 'deselected';
702
- const total = this.i18n.total.replace('{count}', itemCount || 0);
703
- announce(`${itemLabel} ${this.i18n[state]} ${total}`);
758
+ const total = this.__effectiveI18n.total.replace('{count}', itemCount || 0);
759
+ announce(`${itemLabel} ${this.__effectiveI18n[state]} ${total}`);
704
760
  }
705
761
 
706
762
  /** @private */
@@ -925,26 +981,20 @@ export const MultiSelectComboBoxMixin = (superClass) =>
925
981
  }
926
982
  }
927
983
 
928
- /** @private */
929
- _onClearButtonTouchend(event) {
930
- // Cancel the following click and focus events
931
- event.preventDefault();
932
- // Prevent default combo box behavior which can otherwise unnecessarily
933
- // clear the input and filter
934
- event.stopPropagation();
935
-
936
- this.clear();
937
- }
938
-
939
984
  /**
940
- * Override method inherited from `InputControlMixin` and clear items.
985
+ * Override method from `ComboBoxBaseMixin` to deselect
986
+ * dropdown item by requesting content update on clear.
987
+ * @param {Event} event
941
988
  * @protected
942
- * @override
943
989
  */
944
990
  _onClearButtonClick(event) {
945
991
  event.stopPropagation();
946
992
 
947
- this.clear();
993
+ super._onClearButtonClick(event);
994
+
995
+ if (this.opened) {
996
+ this.requestContentUpdate();
997
+ }
948
998
  }
949
999
 
950
1000
  /**
@@ -969,10 +1019,86 @@ export const MultiSelectComboBoxMixin = (superClass) =>
969
1019
  * @override
970
1020
  */
971
1021
  _onEscape(event) {
972
- if (this.clearButtonVisible && this.selectedItems && this.selectedItems.length) {
1022
+ if (this.readonly) {
1023
+ event.stopPropagation();
1024
+ if (this.opened) {
1025
+ this.close();
1026
+ }
1027
+ return;
1028
+ }
1029
+
1030
+ if (this.clearButtonVisible && !this.opened && this.selectedItems && this.selectedItems.length) {
973
1031
  event.stopPropagation();
974
1032
  this.selectedItems = [];
975
1033
  }
1034
+
1035
+ super._onEscape(event);
1036
+ }
1037
+
1038
+ /**
1039
+ * Override method from `ComboBoxBaseMixin` to handle Escape pres.
1040
+ * @protected
1041
+ * @override
1042
+ */
1043
+ _onEscapeCancel() {
1044
+ this._closeOrCommit();
1045
+ }
1046
+
1047
+ /**
1048
+ * Override an event listener from `KeyboardMixin` to keep
1049
+ * overlay open when item is selected or unselected.
1050
+ * @param {!Event} event
1051
+ * @protected
1052
+ * @override
1053
+ */
1054
+ _onEnter(event) {
1055
+ if (this.opened) {
1056
+ // Do not submit the surrounding form.
1057
+ event.preventDefault();
1058
+ // Do not trigger global listeners.
1059
+ event.stopPropagation();
1060
+
1061
+ if (this.readonly) {
1062
+ this.close();
1063
+ } else if (this._hasValidInputValue()) {
1064
+ // Keep selected item focused after committing on Enter.
1065
+ const focusedItem = this._dropdownItems[this._focusedIndex];
1066
+ this._commitValue();
1067
+ this._focusedIndex = this._dropdownItems.indexOf(focusedItem);
1068
+ }
1069
+
1070
+ return;
1071
+ }
1072
+
1073
+ super._onEnter(event);
1074
+ }
1075
+
1076
+ /**
1077
+ * Override method inherited from the combo-box
1078
+ * to not update focused item when readonly.
1079
+ * @protected
1080
+ * @override
1081
+ */
1082
+ _onArrowDown() {
1083
+ if (!this.readonly) {
1084
+ super._onArrowDown();
1085
+ } else if (!this.opened) {
1086
+ this.open();
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * Override method inherited from the combo-box
1092
+ * to not update focused item when readonly.
1093
+ * @protected
1094
+ * @override
1095
+ */
1096
+ _onArrowUp() {
1097
+ if (!this.readonly) {
1098
+ super._onArrowUp();
1099
+ } else if (!this.opened) {
1100
+ this.open();
1101
+ }
976
1102
  }
977
1103
 
978
1104
  /**
@@ -1097,41 +1223,34 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1097
1223
  if (focusedIndex > -1) {
1098
1224
  const item = chips[focusedIndex].item;
1099
1225
  const itemLabel = this._getItemLabel(item);
1100
- announce(`${itemLabel} ${this.i18n.focused}`);
1226
+ announce(`${itemLabel} ${this.__effectiveI18n.focused}`);
1101
1227
  }
1102
1228
  }
1103
1229
  }
1104
1230
 
1105
- /** @private */
1106
- _onComboBoxChange() {
1107
- const item = this.$.comboBox.selectedItem;
1108
- if (item) {
1109
- this.__selectItem(item);
1110
- }
1111
- }
1112
-
1113
- /** @private */
1114
- _onComboBoxItemSelected(event) {
1115
- this.__selectItem(event.detail.item);
1116
- }
1117
-
1118
- /** @private */
1119
- _onCustomValueSet(event) {
1120
- // Do not set combo-box value
1121
- event.preventDefault();
1122
-
1123
- // Stop the original event
1231
+ /**
1232
+ * @param {CustomEvent} event
1233
+ * @protected
1234
+ * @override
1235
+ */
1236
+ _overlaySelectedItemChanged(event) {
1124
1237
  event.stopPropagation();
1125
1238
 
1126
- this.__clearInternalValue(true);
1239
+ // Do not un-select on click when readonly
1240
+ if (this.readonly) {
1241
+ return;
1242
+ }
1127
1243
 
1128
- this.dispatchEvent(
1129
- new CustomEvent('custom-value-set', {
1130
- detail: event.detail,
1131
- composed: true,
1132
- bubbles: true,
1133
- }),
1134
- );
1244
+ if (event.detail.item instanceof ComboBoxPlaceholder) {
1245
+ return;
1246
+ }
1247
+
1248
+ if (this.opened) {
1249
+ // Store filter value for checking if user input is matching
1250
+ // an item which is already selected, to not un-select it.
1251
+ this._lastFilter = this._inputElementValue;
1252
+ this.__selectItem(event.detail.item);
1253
+ }
1135
1254
  }
1136
1255
 
1137
1256
  /** @private */