@vaadin/multi-select-combo-box 23.1.0-alpha4 → 23.1.0-beta3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/multi-select-combo-box",
3
- "version": "23.1.0-alpha4",
3
+ "version": "23.1.0-beta3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,18 +33,18 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@polymer/polymer": "^3.0.0",
36
- "@vaadin/combo-box": "23.1.0-alpha4",
37
- "@vaadin/component-base": "23.1.0-alpha4",
38
- "@vaadin/field-base": "23.1.0-alpha4",
39
- "@vaadin/input-container": "23.1.0-alpha4",
40
- "@vaadin/vaadin-lumo-styles": "23.1.0-alpha4",
41
- "@vaadin/vaadin-material-styles": "23.1.0-alpha4",
42
- "@vaadin/vaadin-themable-mixin": "23.1.0-alpha4"
36
+ "@vaadin/combo-box": "23.1.0-beta3",
37
+ "@vaadin/component-base": "23.1.0-beta3",
38
+ "@vaadin/field-base": "23.1.0-beta3",
39
+ "@vaadin/input-container": "23.1.0-beta3",
40
+ "@vaadin/vaadin-lumo-styles": "23.1.0-beta3",
41
+ "@vaadin/vaadin-material-styles": "23.1.0-beta3",
42
+ "@vaadin/vaadin-themable-mixin": "23.1.0-beta3"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@esm-bundle/chai": "^4.3.4",
46
46
  "@vaadin/testing-helpers": "^0.3.2",
47
47
  "sinon": "^13.0.2"
48
48
  },
49
- "gitHead": "aacdb7fe09811894751f0378ff7fb66071892c71"
49
+ "gitHead": "c787ceb8a312f88631c6d429ff320d5f89b1b838"
50
50
  }
@@ -30,6 +30,16 @@ class MultiSelectComboBoxChip extends ThemableMixin(PolymerElement) {
30
30
 
31
31
  static get properties() {
32
32
  return {
33
+ disabled: {
34
+ type: Boolean,
35
+ reflectToAttribute: true,
36
+ },
37
+
38
+ readonly: {
39
+ type: Boolean,
40
+ reflectToAttribute: true,
41
+ },
42
+
33
43
  label: {
34
44
  type: String,
35
45
  },
@@ -49,7 +59,6 @@ class MultiSelectComboBoxChip extends ThemableMixin(PolymerElement) {
49
59
  align-self: center;
50
60
  white-space: nowrap;
51
61
  box-sizing: border-box;
52
- min-width: 0;
53
62
  }
54
63
 
55
64
  [part='label'] {
@@ -57,7 +66,7 @@ class MultiSelectComboBoxChip extends ThemableMixin(PolymerElement) {
57
66
  text-overflow: ellipsis;
58
67
  }
59
68
 
60
- :host([part~='overflow']) [part='remove-button'] {
69
+ :host(:is([readonly], [disabled], [part~='overflow'])) [part='remove-button'] {
61
70
  display: none !important;
62
71
  }
63
72
  </style>
@@ -9,10 +9,9 @@ import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themab
9
9
  registerStyles(
10
10
  'vaadin-multi-select-combo-box-container',
11
11
  css`
12
- .wrapper {
12
+ #wrapper {
13
13
  display: flex;
14
14
  width: 100%;
15
- min-width: 0;
16
15
  }
17
16
  `,
18
17
  {
@@ -40,7 +39,7 @@ class MultiSelectComboBoxContainer extends InputContainer {
40
39
  const slots = content.querySelectorAll('slot');
41
40
 
42
41
  const wrapper = document.createElement('div');
43
- wrapper.setAttribute('class', 'wrapper');
42
+ wrapper.setAttribute('id', 'wrapper');
44
43
  content.insertBefore(wrapper, slots[2]);
45
44
 
46
45
  wrapper.appendChild(slots[0]);
@@ -179,6 +179,10 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
179
179
  * @override
180
180
  */
181
181
  _onFocusout(event) {
182
+ // Disable combo-box logic that updates selectedItem
183
+ // based on the overlay focused index on input blur
184
+ this._ignoreCommitValue = true;
185
+
182
186
  super._onFocusout(event);
183
187
 
184
188
  if (this.readonly && !this._closeOnBlurIsPrevented) {
@@ -186,6 +190,26 @@ class MultiSelectComboBoxInternal extends ComboBoxDataProviderMixin(ComboBoxMixi
186
190
  }
187
191
  }
188
192
 
193
+ /**
194
+ * Override method inherited from the combo-box
195
+ * to not commit an already selected item again
196
+ * on blur, which would result in un-selecting.
197
+ * @protected
198
+ * @override
199
+ */
200
+ _detectAndDispatchChange() {
201
+ if (this._ignoreCommitValue) {
202
+ this._ignoreCommitValue = false;
203
+
204
+ // Reset internal combo-box state
205
+ this.selectedItem = null;
206
+ this._inputElementValue = '';
207
+ return;
208
+ }
209
+
210
+ super._detectAndDispatchChange();
211
+ }
212
+
189
213
  /**
190
214
  * @param {CustomEvent} event
191
215
  * @protected
@@ -17,6 +17,19 @@ class MultiSelectComboBoxScroller extends ComboBoxScroller {
17
17
  return 'vaadin-multi-select-combo-box-scroller';
18
18
  }
19
19
 
20
+ /** @protected */
21
+ ready() {
22
+ super.ready();
23
+
24
+ this.setAttribute('aria-multiselectable', 'true');
25
+ }
26
+
27
+ /** @private */
28
+ __getAriaSelected(_focusedIndex, itemIndex) {
29
+ const item = this.items[itemIndex];
30
+ return this.__isItemSelected(item, null, this.itemIdPath).toString();
31
+ }
32
+
20
33
  /** @private */
21
34
  __isItemSelected(item, _selectedItem, itemIdPath) {
22
35
  if (item instanceof ComboBoxPlaceholder) {
@@ -20,6 +20,14 @@ import { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js';
20
20
  import { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
21
21
  import { ThemableMixinClass } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
22
22
 
23
+ export interface MultiSelectComboBoxI18n {
24
+ cleared: string;
25
+ focused: string;
26
+ selected: string;
27
+ deselected: string;
28
+ total: string;
29
+ }
30
+
23
31
  /**
24
32
  * Fired when the user commits a value change.
25
33
  */
@@ -30,7 +38,7 @@ export type MultiSelectComboBoxChangeEvent<TItem> = Event & {
30
38
  /**
31
39
  * Fired when the user sets a custom value.
32
40
  */
33
- export type MultiSelectComboBoxCustomValuesSetEvent = CustomEvent<string>;
41
+ export type MultiSelectComboBoxCustomValueSetEvent = CustomEvent<string>;
34
42
 
35
43
  /**
36
44
  * Fired when the `filter` property changes.
@@ -50,7 +58,7 @@ export type MultiSelectComboBoxSelectedItemsChangedEvent<TItem> = CustomEvent<{
50
58
  export interface MultiSelectComboBoxEventMap<TItem> extends HTMLElementEventMap {
51
59
  change: MultiSelectComboBoxChangeEvent<TItem>;
52
60
 
53
- 'custom-values-set': MultiSelectComboBoxCustomValuesSetEvent;
61
+ 'custom-value-set': MultiSelectComboBoxCustomValueSetEvent;
54
62
 
55
63
  'filter-changed': MultiSelectComboBoxFilterChangedEvent;
56
64
 
@@ -79,6 +87,7 @@ export interface MultiSelectComboBoxEventMap<TItem> extends HTMLElementEventMap
79
87
  *
80
88
  * Part name | Description
81
89
  * -----------------------|----------------
90
+ * `chips` | The element that wraps chips for selected items
82
91
  * `chip` | Chip shown for every selected item
83
92
  * `label` | The label element
84
93
  * `input-field` | The element that wraps prefix, value and suffix
@@ -112,7 +121,6 @@ export interface MultiSelectComboBoxEventMap<TItem> extends HTMLElementEventMap
112
121
  * -----------------------------------------------------|----------------------------|--------
113
122
  * `--vaadin-field-default-width` | Default width of the field | `12em`
114
123
  * `--vaadin-multi-select-combo-box-overlay-max-height` | Max height of the overlay | `65vh`
115
- * `--vaadin-multi-select-combo-box-chip-min-width` | Min width of the chip | `60px`
116
124
  * `--vaadin-multi-select-combo-box-input-min-width` | Min width of the input | `4em`
117
125
  *
118
126
  * ### Internal components
@@ -130,7 +138,7 @@ export interface MultiSelectComboBoxEventMap<TItem> extends HTMLElementEventMap
130
138
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
131
139
  *
132
140
  * @fires {Event} change - Fired when the user commits a value change.
133
- * @fires {CustomEvent} custom-values-set - Fired when the user sets a custom value.
141
+ * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
134
142
  * @fires {CustomEvent} filter-changed - Fired when the `filter` property changes.
135
143
  * @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
136
144
  * @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
@@ -138,9 +146,9 @@ export interface MultiSelectComboBoxEventMap<TItem> extends HTMLElementEventMap
138
146
  declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLElement {
139
147
  /**
140
148
  * When true, the user can input a value that is not present in the items list.
141
- * @attr {boolean} allow-custom-values
149
+ * @attr {boolean} allow-custom-value
142
150
  */
143
- allowCustomValues: boolean;
151
+ allowCustomValue: boolean;
144
152
 
145
153
  /**
146
154
  * Set true to prevent the overlay from opening automatically.
@@ -199,6 +207,30 @@ declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLEleme
199
207
  */
200
208
  itemValuePath: string;
201
209
 
210
+ /**
211
+ * The object used to localize this component.
212
+ * To change the default localization, replace the entire
213
+ * _i18n_ object or just the property you want to modify.
214
+ *
215
+ * The object has the following JSON structure and default values:
216
+ * ```
217
+ * {
218
+ * // Screen reader announcement on clear button click.
219
+ * cleared: 'Selection cleared',
220
+ * // Screen reader announcement when a chip is focused.
221
+ * focused: ' focused. Press Backspace to remove',
222
+ * // Screen reader announcement when item is selected.
223
+ * selected: 'added to selection',
224
+ * // Screen reader announcement when item is deselected.
225
+ * deselected: 'removed from selection',
226
+ * // Screen reader announcement of the selected items count.
227
+ * // {count} is replaced with the actual count of items.
228
+ * total: '{count} items selected',
229
+ * }
230
+ * ```
231
+ */
232
+ i18n: MultiSelectComboBoxI18n;
233
+
202
234
  /**
203
235
  * True if the dropdown is open, false otherwise.
204
236
  */
@@ -210,6 +242,13 @@ declare class MultiSelectComboBox<TItem = ComboBoxDefaultItem> extends HTMLEleme
210
242
  */
211
243
  pageSize: number;
212
244
 
245
+ /**
246
+ * A hint to the user of what can be entered in the control.
247
+ * The placeholder will be only displayed in the case when
248
+ * there is no item selected.
249
+ */
250
+ placeholder: string;
251
+
213
252
  /**
214
253
  * Custom function for rendering the content of every item.
215
254
  * Receives three arguments:
@@ -7,6 +7,7 @@ import './vaadin-multi-select-combo-box-chip.js';
7
7
  import './vaadin-multi-select-combo-box-container.js';
8
8
  import './vaadin-multi-select-combo-box-internal.js';
9
9
  import { html, PolymerElement } from '@polymer/polymer/polymer-element.js';
10
+ import { announce } from '@vaadin/component-base/src/a11y-announcer.js';
10
11
  import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
11
12
  import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
12
13
  import { processTemplates } from '@vaadin/component-base/src/templates.js';
@@ -18,7 +19,6 @@ import { css, registerStyles, ThemableMixin } from '@vaadin/vaadin-themable-mixi
18
19
 
19
20
  const multiSelectComboBox = css`
20
21
  :host {
21
- --chip-min-width: var(--vaadin-multi-select-combo-box-chip-min-width, 4em);
22
22
  --input-min-width: var(--vaadin-multi-select-combo-box-input-min-width, 4em);
23
23
  }
24
24
 
@@ -26,6 +26,11 @@ const multiSelectComboBox = css`
26
26
  display: none !important;
27
27
  }
28
28
 
29
+ #chips {
30
+ display: flex;
31
+ align-items: center;
32
+ }
33
+
29
34
  :host([has-value]) ::slotted(input:placeholder-shown) {
30
35
  color: transparent !important;
31
36
  }
@@ -37,11 +42,12 @@ const multiSelectComboBox = css`
37
42
 
38
43
  [part='chip'] {
39
44
  flex: 0 1 auto;
40
- min-width: var(--chip-min-width);
41
45
  }
42
46
 
43
- :host([readonly]) [part~='chip'] {
44
- pointer-events: none;
47
+ :host(:is([readonly], [disabled])) ::slotted(input) {
48
+ flex-grow: 0;
49
+ flex-basis: 0;
50
+ padding: 0;
45
51
  }
46
52
  `;
47
53
 
@@ -69,6 +75,7 @@ registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectCo
69
75
  *
70
76
  * Part name | Description
71
77
  * -----------------------|----------------
78
+ * `chips` | The element that wraps chips for selected items
72
79
  * `chip` | Chip shown for every selected item
73
80
  * `label` | The label element
74
81
  * `input-field` | The element that wraps prefix, value and suffix
@@ -102,7 +109,6 @@ registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectCo
102
109
  * -----------------------------------------------------|----------------------------|--------
103
110
  * `--vaadin-field-default-width` | Default width of the field | `12em`
104
111
  * `--vaadin-multi-select-combo-box-overlay-max-height` | Max height of the overlay | `65vh`
105
- * `--vaadin-multi-select-combo-box-chip-min-width` | Min width of the chip | `60px`
106
112
  * `--vaadin-multi-select-combo-box-input-min-width` | Min width of the input | `4em`
107
113
  *
108
114
  * ### Internal components
@@ -120,7 +126,7 @@ registerStyles('vaadin-multi-select-combo-box', [inputFieldShared, multiSelectCo
120
126
  * See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
121
127
  *
122
128
  * @fires {Event} change - Fired when the user commits a value change.
123
- * @fires {CustomEvent} custom-values-set - Fired when the user sets a custom value.
129
+ * @fires {CustomEvent} custom-value-set - Fired when the user sets a custom value.
124
130
  * @fires {CustomEvent} filter-changed - Fired when the `filter` property changes.
125
131
  * @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
126
132
  * @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
@@ -153,7 +159,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
153
159
  disabled="[[disabled]]"
154
160
  readonly="[[readonly]]"
155
161
  auto-open-disabled="[[autoOpenDisabled]]"
156
- allow-custom-value="[[allowCustomValues]]"
162
+ allow-custom-value="[[allowCustomValue]]"
157
163
  data-provider="[[dataProvider]]"
158
164
  filter="{{filter}}"
159
165
  filtered-items="[[filteredItems]]"
@@ -172,14 +178,17 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
172
178
  theme$="[[theme]]"
173
179
  >
174
180
  <vaadin-multi-select-combo-box-chip
181
+ id="overflow"
175
182
  slot="prefix"
176
183
  part$="[[_getOverflowPart(_overflowItems.length)]]"
177
184
  disabled="[[disabled]]"
185
+ readonly="[[readonly]]"
178
186
  label="[[_getOverflowLabel(_overflowItems.length)]]"
179
187
  title$="[[_getOverflowTitle(_overflowItems)]]"
180
188
  hidden$="[[_isOverflowHidden(_overflowItems.length)]]"
181
189
  on-mousedown="_preventBlur"
182
190
  ></vaadin-multi-select-combo-box-chip>
191
+ <div id="chips" part="chips" slot="prefix"></div>
183
192
  <slot name="input"></slot>
184
193
  <div id="clearButton" part="clear-button" slot="suffix"></div>
185
194
  <div id="toggleButton" class="toggle-button" part="toggle-button" slot="suffix"></div>
@@ -249,6 +258,43 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
249
258
  type: String,
250
259
  },
251
260
 
261
+ /**
262
+ * The object used to localize this component.
263
+ * To change the default localization, replace the entire
264
+ * _i18n_ object or just the property you want to modify.
265
+ *
266
+ * The object has the following JSON structure and default values:
267
+ * ```
268
+ * {
269
+ * // Screen reader announcement on clear button click.
270
+ * cleared: 'Selection cleared',
271
+ * // Screen reader announcement when a chip is focused.
272
+ * focused: ' focused. Press Backspace to remove',
273
+ * // Screen reader announcement when item is selected.
274
+ * selected: 'added to selection',
275
+ * // Screen reader announcement when item is deselected.
276
+ * deselected: 'removed from selection',
277
+ * // Screen reader announcement of the selected items count.
278
+ * // {count} is replaced with the actual count of items.
279
+ * total: '{count} items selected',
280
+ * }
281
+ * ```
282
+ * @type {!MultiSelectComboBoxI18n}
283
+ * @default {English/US}
284
+ */
285
+ i18n: {
286
+ type: Object,
287
+ value: () => {
288
+ return {
289
+ cleared: 'Selection cleared',
290
+ focused: 'focused. Press Backspace to remove',
291
+ selected: 'added to selection',
292
+ deselected: 'removed from selection',
293
+ total: '{count} items selected',
294
+ };
295
+ },
296
+ },
297
+
252
298
  /**
253
299
  * When present, it specifies that the field is read-only.
254
300
  */
@@ -308,13 +354,24 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
308
354
 
309
355
  /**
310
356
  * When true, the user can input a value that is not present in the items list.
311
- * @attr {boolean} allow-custom-values
357
+ * @attr {boolean} allow-custom-value
312
358
  */
313
- allowCustomValues: {
359
+ allowCustomValue: {
314
360
  type: Boolean,
315
361
  value: false,
316
362
  },
317
363
 
364
+ /**
365
+ * A hint to the user of what can be entered in the control.
366
+ * The placeholder will be only displayed in the case when
367
+ * there is no item selected.
368
+ */
369
+ placeholder: {
370
+ type: String,
371
+ value: '',
372
+ observer: '_placeholderChanged',
373
+ },
374
+
318
375
  /**
319
376
  * Custom function for rendering the content of every item.
320
377
  * Receives three arguments:
@@ -355,6 +412,13 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
355
412
  type: Array,
356
413
  value: () => [],
357
414
  },
415
+
416
+ /** @private */
417
+ _focusedChipIndex: {
418
+ type: Number,
419
+ value: -1,
420
+ observer: '_focusedChipIndexChanged',
421
+ },
358
422
  };
359
423
  }
360
424
 
@@ -401,7 +465,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
401
465
  * @return {boolean}
402
466
  */
403
467
  checkValidity() {
404
- return this.required ? this._hasValue : true;
468
+ return this.required && !this.readonly ? this._hasValue : true;
405
469
  }
406
470
 
407
471
  /**
@@ -413,9 +477,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
413
477
  super._disabledChanged(disabled, oldDisabled);
414
478
 
415
479
  if (disabled || oldDisabled) {
416
- this._chips.forEach((chip) => {
417
- chip.toggleAttribute('disabled', disabled);
418
- });
480
+ this.__updateChips();
419
481
  }
420
482
  }
421
483
 
@@ -441,6 +503,7 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
441
503
  super._setFocused(focused);
442
504
 
443
505
  if (!focused) {
506
+ this._focusedChipIndex = -1;
444
507
  this.validate();
445
508
  }
446
509
  }
@@ -464,6 +527,26 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
464
527
  this.__updateChips();
465
528
  }
466
529
 
530
+ /**
531
+ * Override method from `DelegateStateMixin` to set required state
532
+ * using `aria-required` attribute instead of `required`, in order
533
+ * to prevent screen readers from announcing "invalid entry".
534
+ * @protected
535
+ * @override
536
+ */
537
+ _delegateAttribute(name, value) {
538
+ if (!this.stateTarget) {
539
+ return;
540
+ }
541
+
542
+ if (name === 'required') {
543
+ this._delegateAttribute('aria-required', value ? 'true' : false);
544
+ return;
545
+ }
546
+
547
+ super._delegateAttribute(name, value);
548
+ }
549
+
467
550
  /**
468
551
  * Setting clear button visible reduces total space available
469
552
  * for rendering chips, and making it hidden increases it.
@@ -480,18 +563,11 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
480
563
  if (readonly) {
481
564
  this.__savedItems = this.$.comboBox._getOverlayItems();
482
565
  this.$.comboBox._setOverlayItems(Array.from(this.selectedItems));
483
-
484
- // Update chips to hide remove button
485
- this._chips.forEach((chip) => {
486
- chip.setAttribute('readonly', '');
487
- });
566
+ this.__updateChips();
488
567
  } else if (oldReadonly) {
489
568
  this.$.comboBox._setOverlayItems(this.__savedItems);
490
569
  this.__savedItems = null;
491
-
492
- this._chips.forEach((chip) => {
493
- chip.removeAttribute('readonly');
494
- });
570
+ this.__updateChips();
495
571
  }
496
572
  }
497
573
 
@@ -505,12 +581,35 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
505
581
  this.$.comboBox.pageSize = this.pageSize;
506
582
  }
507
583
 
584
+ /** @private */
585
+ _placeholderChanged(placeholder) {
586
+ const tmpPlaceholder = this.__tmpA11yPlaceholder;
587
+ // Do not store temporary placeholder
588
+ if (tmpPlaceholder !== placeholder) {
589
+ this.__savedPlaceholder = placeholder;
590
+
591
+ if (tmpPlaceholder) {
592
+ this.placeholder = tmpPlaceholder;
593
+ }
594
+ }
595
+ }
596
+
508
597
  /** @private */
509
598
  _selectedItemsChanged(selectedItems) {
510
599
  this._hasValue = Boolean(selectedItems && selectedItems.length);
511
600
 
512
601
  this._toggleHasValue();
513
602
 
603
+ // Use placeholder for announcing items
604
+ if (this._hasValue) {
605
+ const tmpPlaceholder = selectedItems.map((item) => this._getItemLabel(item, this.itemLabelPath)).join(', ');
606
+ this.__tmpA11yPlaceholder = tmpPlaceholder;
607
+ this.placeholder = tmpPlaceholder;
608
+ } else {
609
+ delete this.__tmpA11yPlaceholder;
610
+ this.placeholder = this.__savedPlaceholder;
611
+ }
612
+
514
613
  // Re-render chips
515
614
  this.__updateChips();
516
615
 
@@ -580,11 +679,19 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
580
679
  this.$.comboBox.clear();
581
680
  }
582
681
 
682
+ /** @private */
683
+ __announceItem(itemLabel, isSelected, itemCount) {
684
+ const state = isSelected ? 'selected' : 'deselected';
685
+ const total = this.i18n.total.replace('{count}', itemCount || 0);
686
+ announce(`${itemLabel} ${this.i18n[state]} ${total}`);
687
+ }
688
+
583
689
  /** @private */
584
690
  __removeItem(item) {
585
691
  const itemsCopy = [...this.selectedItems];
586
692
  itemsCopy.splice(itemsCopy.indexOf(item), 1);
587
693
  this.__updateSelection(itemsCopy);
694
+ this.__announceItem(item, false, itemsCopy.length);
588
695
  }
589
696
 
590
697
  /** @private */
@@ -592,9 +699,13 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
592
699
  const itemsCopy = [...this.selectedItems];
593
700
 
594
701
  const index = this._findIndex(item, itemsCopy, this.itemIdPath);
702
+ const itemLabel = this._getItemLabel(item, this.itemLabelPath);
703
+
704
+ let isSelected = false;
705
+
595
706
  if (index !== -1) {
596
707
  // Do not unselect when manually typing and committing an already selected item.
597
- if (this.filter.toLowerCase() === this._getItemLabel(item, this.itemLabelPath).toLowerCase()) {
708
+ if (this.filter.toLowerCase() === itemLabel.toLowerCase()) {
598
709
  this.__clearFilter();
599
710
  return;
600
711
  }
@@ -602,12 +713,15 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
602
713
  itemsCopy.splice(index, 1);
603
714
  } else {
604
715
  itemsCopy.push(item);
716
+ isSelected = true;
605
717
  }
606
718
 
607
719
  this.__updateSelection(itemsCopy);
608
720
 
609
721
  // Suppress `value-changed` event.
610
722
  this.__clearFilter();
723
+
724
+ this.__announceItem(itemLabel, isSelected, itemsCopy.length);
611
725
  }
612
726
 
613
727
  /** @private */
@@ -626,8 +740,8 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
626
740
  chip.setAttribute('slot', 'prefix');
627
741
 
628
742
  chip.item = item;
629
- chip.toggleAttribute('disabled', this.disabled);
630
- chip.toggleAttribute('readonly', this.readonly);
743
+ chip.disabled = this.disabled;
744
+ chip.readonly = this.readonly;
631
745
 
632
746
  const label = this._getItemLabel(item, this.itemLabelPath);
633
747
  chip.label = label;
@@ -640,18 +754,21 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
640
754
  }
641
755
 
642
756
  /** @private */
643
- __getMinWidth(chip) {
757
+ __getOverflowWidth() {
758
+ const chip = this.$.overflow;
759
+
644
760
  chip.style.visibility = 'hidden';
645
- chip.style.display = 'block';
646
- chip.style.minWidth = 'var(--chip-min-width)';
761
+ chip.removeAttribute('hidden');
647
762
 
648
- const result = parseInt(getComputedStyle(chip).minWidth);
763
+ // Detect max possible width of the overflow chip
764
+ chip.setAttribute('part', 'chip overflow');
765
+ const overflowStyle = getComputedStyle(chip);
766
+ const overflowWidth = chip.clientWidth + parseInt(overflowStyle.marginInlineStart);
649
767
 
650
- chip.style.minWidth = '';
651
- chip.style.display = '';
768
+ chip.setAttribute('hidden', '');
652
769
  chip.style.visibility = '';
653
770
 
654
- return result;
771
+ return overflowWidth;
655
772
  }
656
773
 
657
774
  /** @private */
@@ -661,46 +778,36 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
661
778
  }
662
779
 
663
780
  // Clear all chips except the overflow
664
- const chips = Array.from(this._chips).reverse();
665
- const overflow = chips.pop();
666
-
667
- chips.forEach((chip) => {
668
- chip.remove();
781
+ Array.from(this._chips).forEach((chip) => {
782
+ if (chip !== this.$.overflow) {
783
+ chip.remove();
784
+ }
669
785
  });
670
786
 
671
787
  const items = [...this.selectedItems];
672
788
 
673
- let refNode = overflow.nextElementSibling;
674
-
675
- // Use overflow chip to measure min-width
676
- const chipMinWidth = this.__getMinWidth(overflow);
677
- const inputMinWidth = parseInt(getComputedStyle(this.inputElement).flexBasis);
678
- const containerStyle = getComputedStyle(this._inputField);
789
+ // Detect available remaining width for chips
790
+ const totalWidth = this._inputField.$.wrapper.clientWidth;
791
+ const inputWidth = parseInt(getComputedStyle(this.inputElement).flexBasis);
679
792
 
680
- // Detect available width for chips
681
- let totalWidth =
682
- parseInt(containerStyle.width) -
683
- parseInt(containerStyle.paddingLeft) -
684
- parseInt(containerStyle.paddingRight) -
685
- this.$.toggleButton.clientWidth -
686
- inputMinWidth;
793
+ let remainingWidth = totalWidth - inputWidth;
687
794
 
688
- if (this.clearButtonVisible) {
689
- totalWidth -= this.$.clearButton.clientWidth;
795
+ if (items.length > 1) {
796
+ remainingWidth -= this.__getOverflowWidth();
690
797
  }
691
798
 
692
- for (let i = items.length - 1; i >= 0; i--) {
693
- // Ensure there is enough space for another chip
694
- if (totalWidth < chipMinWidth) {
799
+ // Add chips until remaining width is exceeded
800
+ for (let i = items.length - 1, refNode = null; i >= 0; i--) {
801
+ const chip = this.__createChip(items[i]);
802
+ this.$.chips.insertBefore(chip, refNode);
803
+
804
+ if (this.$.chips.clientWidth > remainingWidth) {
805
+ chip.remove();
695
806
  break;
696
807
  }
697
808
 
698
- const item = items.pop();
699
- const chip = this.__createChip(item);
700
- this._inputField.insertBefore(chip, refNode);
701
-
809
+ items.pop();
702
810
  refNode = chip;
703
- totalWidth -= chipMinWidth;
704
811
  }
705
812
 
706
813
  this._overflowItems = items;
@@ -715,6 +822,8 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
715
822
  event.stopPropagation();
716
823
 
717
824
  this.__updateSelection([]);
825
+
826
+ announce(this.i18n.cleared);
718
827
  }
719
828
 
720
829
  /**
@@ -725,8 +834,121 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
725
834
  */
726
835
  _onKeyDown(event) {
727
836
  const items = this.selectedItems || [];
728
- if (!this.readonly && event.key === 'Backspace' && items.length && this.inputElement.value === '') {
729
- this.__removeItem(items[items.length - 1]);
837
+
838
+ if (event.key === 'Escape' && this.clearButtonVisible && items.length) {
839
+ this.selectedItems = [];
840
+ return;
841
+ }
842
+
843
+ const chips = Array.from(this._chips).slice(1);
844
+
845
+ if (!this.readonly && chips.length > 0) {
846
+ switch (event.key) {
847
+ case 'Backspace':
848
+ this._onBackSpace(chips);
849
+ break;
850
+ case 'ArrowLeft':
851
+ this._onArrowLeft(chips);
852
+ break;
853
+ case 'ArrowRight':
854
+ this._onArrowRight(chips);
855
+ break;
856
+ default:
857
+ this._focusedChipIndex = -1;
858
+ break;
859
+ }
860
+ }
861
+ }
862
+
863
+ /** @private */
864
+ _onArrowLeft(chips) {
865
+ if (this.inputElement.value !== '' || this.opened) {
866
+ return;
867
+ }
868
+
869
+ const idx = this._focusedChipIndex;
870
+ let newIdx;
871
+
872
+ if (this.getAttribute('dir') !== 'rtl') {
873
+ if (idx === -1) {
874
+ // Focus last chip
875
+ newIdx = chips.length - 1;
876
+ } else if (idx > 0) {
877
+ // Focus prev chip
878
+ newIdx = idx - 1;
879
+ }
880
+ } else if (idx === chips.length - 1) {
881
+ // Blur last chip
882
+ newIdx = -1;
883
+ } else if (idx > -1) {
884
+ // Focus next chip
885
+ newIdx = idx + 1;
886
+ }
887
+
888
+ if (newIdx !== undefined) {
889
+ this._focusedChipIndex = newIdx;
890
+ }
891
+ }
892
+
893
+ /** @private */
894
+ _onArrowRight(chips) {
895
+ if (this.inputElement.value !== '' || this.opened) {
896
+ return;
897
+ }
898
+
899
+ const idx = this._focusedChipIndex;
900
+ let newIdx;
901
+
902
+ if (this.getAttribute('dir') === 'rtl') {
903
+ if (idx === -1) {
904
+ // Focus last chip
905
+ newIdx = chips.length - 1;
906
+ } else if (idx > 0) {
907
+ // Focus prev chip
908
+ newIdx = idx - 1;
909
+ }
910
+ } else if (idx === chips.length - 1) {
911
+ // Blur last chip
912
+ newIdx = -1;
913
+ } else if (idx > -1) {
914
+ // Focus next chip
915
+ newIdx = idx + 1;
916
+ }
917
+
918
+ if (newIdx !== undefined) {
919
+ this._focusedChipIndex = newIdx;
920
+ }
921
+ }
922
+
923
+ /** @private */
924
+ _onBackSpace(chips) {
925
+ if (this.inputElement.value !== '' || this.opened) {
926
+ return;
927
+ }
928
+
929
+ const idx = this._focusedChipIndex;
930
+ if (idx === -1) {
931
+ this._focusedChipIndex = chips.length - 1;
932
+ } else {
933
+ this.__removeItem(chips[idx].item);
934
+ this._focusedChipIndex = -1;
935
+ }
936
+ }
937
+
938
+ /** @private */
939
+ _focusedChipIndexChanged(focusedIndex, oldFocusedIndex) {
940
+ if (focusedIndex > -1 || oldFocusedIndex > -1) {
941
+ const chips = Array.from(this._chips).slice(1);
942
+ chips.forEach((chip, index) => {
943
+ chip.toggleAttribute('focused', index === focusedIndex);
944
+ });
945
+
946
+ // Announce focused chip
947
+ if (focusedIndex > -1) {
948
+ const item = chips[focusedIndex].item;
949
+ const itemLabel = this._getItemLabel(item, this.itemLabelPath);
950
+ announce(`${itemLabel} ${this.i18n.focused}`);
951
+ }
730
952
  }
731
953
  }
732
954
 
@@ -748,10 +970,13 @@ class MultiSelectComboBox extends ResizeMixin(InputControlMixin(ThemableMixin(El
748
970
  // Do not set combo-box value
749
971
  event.preventDefault();
750
972
 
973
+ // Stop the original event
974
+ event.stopPropagation();
975
+
751
976
  this.__clearFilter();
752
977
 
753
978
  this.dispatchEvent(
754
- new CustomEvent('custom-values-set', {
979
+ new CustomEvent('custom-value-set', {
755
980
  detail: event.detail,
756
981
  composed: true,
757
982
  bubbles: true,
@@ -22,6 +22,15 @@ const chip = css`
22
22
  cursor: var(--lumo-clickable-cursor);
23
23
  }
24
24
 
25
+ :host([focused]) {
26
+ background-color: var(--lumo-primary-color);
27
+ color: var(--lumo-primary-contrast-color);
28
+ }
29
+
30
+ :host([focused]) [part='remove-button'] {
31
+ color: inherit;
32
+ }
33
+
25
34
  :host(:not([part~='overflow']):not([readonly]):not([disabled])) {
26
35
  padding-inline-end: 0;
27
36
  }
@@ -36,19 +45,19 @@ const chip = css`
36
45
  :host([part~='overflow'])::after {
37
46
  position: absolute;
38
47
  content: '';
39
- width: 3px;
40
- height: calc(1.875em - 1px);
41
- border-left: 2px solid;
42
- border-radius: 4px 0 0 4px;
48
+ width: 100%;
49
+ height: 100%;
50
+ border-left: calc(var(--lumo-space-s) / 4) solid;
51
+ border-radius: var(--lumo-border-radius-s);
43
52
  border-color: var(--lumo-contrast-30pct);
44
53
  }
45
54
 
46
55
  :host([part~='overflow'])::before {
47
- left: -4px;
56
+ left: calc(-1 * var(--lumo-space-s) / 2);
48
57
  }
49
58
 
50
59
  :host([part~='overflow'])::after {
51
- left: -8px;
60
+ left: calc(-1 * var(--lumo-space-s));
52
61
  }
53
62
 
54
63
  :host([part~='overflow-two']) {
@@ -79,9 +88,11 @@ const chip = css`
79
88
  justify-content: center;
80
89
  margin-top: -0.3125em;
81
90
  margin-bottom: -0.3125em;
91
+ margin-inline-start: auto;
82
92
  width: 1.25em;
83
93
  height: 1.25em;
84
94
  font-size: 1.5em;
95
+ transition: none;
85
96
  }
86
97
 
87
98
  [part='remove-button']::before {
@@ -93,11 +104,6 @@ const chip = css`
93
104
  -webkit-text-fill-color: var(--lumo-disabled-text-color);
94
105
  pointer-events: none;
95
106
  }
96
-
97
- :host([readonly]) [part='remove-button'],
98
- :host([disabled]) [part='remove-button'] {
99
- display: none;
100
- }
101
107
  `;
102
108
 
103
109
  registerStyles('vaadin-multi-select-combo-box-chip', [fieldButton, chip], {
@@ -30,14 +30,18 @@ const multiSelectComboBox = css`
30
30
  padding-inline-start: 0;
31
31
  }
32
32
 
33
- :host([readonly]) [part~='chip'] {
34
- opacity: 0.7;
33
+ :host([has-value]) ::slotted(input:placeholder-shown) {
34
+ caret-color: var(--lumo-body-text-color) !important;
35
35
  }
36
36
 
37
37
  [part~='chip']:not(:last-of-type) {
38
38
  margin-inline-end: var(--lumo-space-xs);
39
39
  }
40
40
 
41
+ [part~='overflow']:not([hidden]) + :not(:empty) {
42
+ margin-inline-start: var(--lumo-space-xs);
43
+ }
44
+
41
45
  [part='toggle-button']::before {
42
46
  content: var(--lumo-icons-dropdown);
43
47
  }
@@ -15,11 +15,15 @@ const chip = css`
15
15
  margin-inline-end: 0.25rem;
16
16
  padding: 0 0.5rem;
17
17
  border-radius: 4px;
18
- background-color: hsla(214, 53%, 23%, 0.1);
18
+ background-color: rgba(0, 0, 0, 0.08);
19
19
  cursor: default;
20
20
  font-family: var(--material-font-family);
21
21
  }
22
22
 
23
+ :host([focused]) {
24
+ background-color: rgba(0, 0, 0, 0.16);
25
+ }
26
+
23
27
  :host(:not([part~='overflow']):not([readonly]):not([disabled])) {
24
28
  padding-inline-end: 0;
25
29
  }
@@ -33,19 +37,19 @@ const chip = css`
33
37
  :host([part~='overflow'])::after {
34
38
  position: absolute;
35
39
  content: '';
36
- width: 3px;
37
- height: 20px;
38
- border-left: 2px solid;
39
- border-radius: 4px 0 0 4px;
40
- border-color: hsla(214, 53%, 23%, 0.1);
40
+ width: 100%;
41
+ height: 100%;
42
+ border-left: 0.125rem solid;
43
+ border-radius: 0.25rem;
44
+ border-color: rgba(0, 0, 0, 0.08);
41
45
  }
42
46
 
43
47
  :host([part~='overflow'])::before {
44
- left: -4px;
48
+ left: -0.25rem;
45
49
  }
46
50
 
47
51
  :host([part~='overflow'])::after {
48
- left: -8px;
52
+ left: -0.5rem;
49
53
  }
50
54
 
51
55
  :host([part~='overflow-two']) {
@@ -79,6 +83,7 @@ const chip = css`
79
83
  box-sizing: border-box;
80
84
  width: 20px;
81
85
  height: 20px;
86
+ margin-inline-start: auto;
82
87
  line-height: 20px;
83
88
  padding: 0;
84
89
  font-size: 0.75em;
@@ -93,11 +98,6 @@ const chip = css`
93
98
  -webkit-text-fill-color: var(--material-disabled-text-color);
94
99
  pointer-events: none;
95
100
  }
96
-
97
- :host([readonly]) [part='remove-button'],
98
- :host([disabled]) [part='remove-button'] {
99
- display: none;
100
- }
101
101
  `;
102
102
 
103
103
  registerStyles('vaadin-multi-select-combo-box-chip', [fieldButton, chip], {
@@ -25,8 +25,8 @@ registerStyles(
25
25
  );
26
26
 
27
27
  const multiSelectComboBox = css`
28
- :host([readonly]) [part~='chip'] {
29
- opacity: 0.5;
28
+ :host([has-value]) ::slotted(input:placeholder-shown) {
29
+ caret-color: var(--material-body-text-color) !important;
30
30
  }
31
31
 
32
32
  [part='input-field'] {