@vaadin/multi-select-combo-box 25.3.2 → 25.4.0-alpha2

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.
@@ -15,6 +15,8 @@ import { TooltipController } from '@vaadin/component-base/src/tooltip-controller
15
15
  import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
16
16
  import { InputController } from '@vaadin/field-base/src/input-controller.js';
17
17
  import { LabelledInputController } from '@vaadin/field-base/src/labelled-input-controller.js';
18
+ import { MultiSelectComboBoxHighlightMixin } from './vaadin-multi-select-combo-box-highlight-mixin.js';
19
+ import { MultiSelectComboBoxSelectAllMixin } from './vaadin-multi-select-combo-box-select-all-mixin.js';
18
20
 
19
21
  const DEFAULT_I18N = {
20
22
  cleared: 'Selection cleared',
@@ -22,11 +24,21 @@ const DEFAULT_I18N = {
22
24
  selected: 'added to selection',
23
25
  deselected: 'removed from selection',
24
26
  total: '{count} items selected',
27
+ selectAll: 'Select All',
28
+ deselectAll: 'Deselect All',
29
+ selectFiltered: 'Select Filtered',
30
+ deselectFiltered: 'Deselect Filtered',
25
31
  };
26
32
 
27
33
  export const MultiSelectComboBoxMixin = (superClass) =>
28
34
  class MultiSelectComboBoxMixinClass extends I18nMixin(
29
- ComboBoxFocusIndexMixin(ComboBoxDataProviderMixin(ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass))))),
35
+ MultiSelectComboBoxSelectAllMixin(
36
+ MultiSelectComboBoxHighlightMixin(
37
+ ComboBoxFocusIndexMixin(
38
+ ComboBoxDataProviderMixin(ComboBoxItemsMixin(InputControlMixin(ResizeMixin(superClass)))),
39
+ ),
40
+ ),
41
+ ),
30
42
  ) {
31
43
  static get properties() {
32
44
  return {
@@ -177,13 +189,6 @@ export const MultiSelectComboBoxMixin = (superClass) =>
177
189
  sync: true,
178
190
  },
179
191
 
180
- /** @private */
181
- _focusedChipIndex: {
182
- type: Number,
183
- value: -1,
184
- observer: '_focusedChipIndexChanged',
185
- },
186
-
187
192
  /** @private */
188
193
  _lastFilter: {
189
194
  type: String,
@@ -218,9 +223,11 @@ export const MultiSelectComboBoxMixin = (superClass) =>
218
223
 
219
224
  /**
220
225
  * The object used to localize this component. To change the default
221
- * localization, replace this with an object that provides all properties, or
226
+ * localization, set this to an object that provides all properties, or
222
227
  * just the individual properties you want to change.
223
228
  *
229
+ * When not set, defaults to `undefined`.
230
+ *
224
231
  * The object has the following JSON structure and default values:
225
232
  * ```js
226
233
  * {
@@ -235,9 +242,19 @@ export const MultiSelectComboBoxMixin = (superClass) =>
235
242
  * // Screen reader announcement of the selected items count.
236
243
  * // {count} is replaced with the actual count of items.
237
244
  * total: '{count} items selected',
245
+ * // Text of the select all button when no filter is set.
246
+ * selectAll: 'Select All',
247
+ * // Text of the select all button when no filter is set
248
+ * // and all items are selected.
249
+ * deselectAll: 'Deselect All',
250
+ * // Text of the select all button when a filter is set.
251
+ * selectFiltered: 'Select Filtered',
252
+ * // Text of the select all button when a filter is set
253
+ * // and all items matching the filter are selected.
254
+ * deselectFiltered: 'Deselect Filtered',
238
255
  * }
239
256
  * ```
240
- * @type {!MultiSelectComboBoxI18n}
257
+ * @type {MultiSelectComboBoxI18n | undefined}
241
258
  */
242
259
  get i18n() {
243
260
  return super.i18n;
@@ -354,6 +371,10 @@ export const MultiSelectComboBoxMixin = (superClass) =>
354
371
  this.__updateChips();
355
372
  }
356
373
 
374
+ if (props.has('_highlightState')) {
375
+ this.__updateChipHighlight(props.get('_highlightState'));
376
+ }
377
+
357
378
  if (props.has('readonly')) {
358
379
  this._setDropdownItems(this.filteredItems);
359
380
 
@@ -387,8 +408,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
387
408
  */
388
409
  clear() {
389
410
  this.__updateSelection([]);
390
-
391
- announce(this.__effectiveI18n.cleared);
411
+ this.__announceSelection();
392
412
  }
393
413
 
394
414
  /** @private */
@@ -483,7 +503,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
483
503
  // Do not commit focused item on not blur / outside click
484
504
  if (this._ignoreCommitValue) {
485
505
  this._inputElementValue = '';
486
- this._focusedIndex = -1;
506
+ this._clearItemHighlight();
487
507
  this._ignoreCommitValue = false;
488
508
  } else {
489
509
  this.__commitUserInput();
@@ -497,8 +517,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
497
517
 
498
518
  /** @private */
499
519
  __commitUserInput() {
500
- if (this._focusedIndex > -1) {
501
- const focusedItem = this._dropdownItems[this._focusedIndex];
520
+ if (this._hasHighlightedItem) {
521
+ const focusedItem = this._highlightedItem;
502
522
  // Do not unselect an already selected item when it was focused by
503
523
  // filtering, in which case the input value still equals the filter.
504
524
  if (
@@ -559,7 +579,7 @@ export const MultiSelectComboBoxMixin = (superClass) =>
559
579
  // Do not validate when focusout is caused by document
560
580
  // losing focus, which happens on browser tab switch.
561
581
  if (blurred && document.hasFocus()) {
562
- this._focusedChipIndex = -1;
582
+ this._clearChipHighlight();
563
583
  this._requestValidation();
564
584
  }
565
585
 
@@ -647,13 +667,13 @@ export const MultiSelectComboBoxMixin = (superClass) =>
647
667
  * @override
648
668
  */
649
669
  _hasValidInputValue() {
650
- const hasInvalidOption = this._focusedIndex < 0 && this._inputElementValue !== '';
670
+ const hasInvalidOption = !this._hasHighlightedItem && this._inputElementValue !== '';
651
671
  return this.allowCustomValue || !hasInvalidOption;
652
672
  }
653
673
 
654
674
  /**
655
- * Override method inherited from the combo-box
656
- * to not request data provider when read-only.
675
+ * Override method from `ComboBoxDataProviderMixin` to not request
676
+ * data provider when read-only.
657
677
  *
658
678
  * @protected
659
679
  * @override
@@ -783,6 +803,16 @@ export const MultiSelectComboBoxMixin = (superClass) =>
783
803
  this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
784
804
  }
785
805
 
806
+ /**
807
+ * Announces the number of selected items to screen readers.
808
+ * @private
809
+ */
810
+ __announceSelection() {
811
+ const { cleared, total } = this.__effectiveI18n;
812
+ const count = this.selectedItems.length;
813
+ announce(count === 0 ? cleared : total.replace('{count}', count));
814
+ }
815
+
786
816
  /** @private */
787
817
  __updateTopGroup(selectedItemsOnTop, selectedItems, opened) {
788
818
  if (!selectedItemsOnTop) {
@@ -1101,11 +1131,13 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1101
1131
 
1102
1132
  if (this.readonly) {
1103
1133
  this.close();
1134
+ } else if (this._isSelectAllHighlighted) {
1135
+ this._toggleSelectAll();
1104
1136
  } else if (this._hasValidInputValue()) {
1105
1137
  // Keep selected item focused after committing on Enter.
1106
- const focusedItem = this._dropdownItems[this._focusedIndex];
1138
+ const focusedItem = this._highlightedItem;
1107
1139
  this._commitValue();
1108
- this._focusedIndex = this._dropdownItems.indexOf(focusedItem);
1140
+ this._highlightItem(focusedItem);
1109
1141
  }
1110
1142
 
1111
1143
  return;
@@ -1115,8 +1147,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1115
1147
  }
1116
1148
 
1117
1149
  /**
1118
- * Override method inherited from the combo-box
1119
- * to not update focused item when readonly.
1150
+ * Override method from `ComboBoxBaseMixin` to not update focused
1151
+ * item when readonly.
1120
1152
  * @protected
1121
1153
  * @override
1122
1154
  */
@@ -1129,8 +1161,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1129
1161
  }
1130
1162
 
1131
1163
  /**
1132
- * Override method inherited from the combo-box
1133
- * to not update focused item when readonly.
1164
+ * Override method from `ComboBoxBaseMixin` to not update focused
1165
+ * item when readonly.
1134
1166
  * @protected
1135
1167
  * @override
1136
1168
  */
@@ -1151,120 +1183,80 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1151
1183
  _onKeyDown(event) {
1152
1184
  super._onKeyDown(event);
1153
1185
 
1154
- const chips = this._chips;
1155
-
1156
- if (!this.readonly && chips.length > 0) {
1186
+ if (!this.readonly && this._chips.length > 0) {
1157
1187
  switch (event.key) {
1158
1188
  case 'Backspace':
1159
- this._onBackSpace(chips);
1189
+ this._onBackSpace();
1160
1190
  break;
1161
1191
  case 'ArrowLeft':
1162
- this._onArrowLeft(chips, event);
1163
- break;
1164
1192
  case 'ArrowRight':
1165
- this._onArrowRight(chips, event);
1193
+ this._onChipArrow(event);
1166
1194
  break;
1167
1195
  default:
1168
- this._focusedChipIndex = -1;
1196
+ this._clearChipHighlight();
1169
1197
  break;
1170
1198
  }
1171
1199
  }
1172
1200
  }
1173
1201
 
1174
1202
  /** @private */
1175
- _onArrowLeft(chips, event) {
1203
+ _onChipArrow(event) {
1176
1204
  if (this.inputElement.selectionStart !== 0) {
1177
1205
  return;
1178
1206
  }
1179
1207
 
1180
- const idx = this._focusedChipIndex;
1181
- if (idx !== -1) {
1208
+ if (this._hasHighlightedChip) {
1182
1209
  event.preventDefault();
1183
1210
  }
1184
- let newIdx;
1185
-
1186
- if (!this.__isRTL) {
1187
- if (idx === -1) {
1188
- // Focus last chip
1189
- newIdx = chips.length - 1;
1190
- } else if (idx > 0) {
1191
- // Focus prev chip
1192
- newIdx = idx - 1;
1193
- }
1194
- } else if (idx === chips.length - 1) {
1195
- // Blur last chip
1196
- newIdx = -1;
1197
- } else if (idx > -1) {
1198
- // Focus next chip
1199
- newIdx = idx + 1;
1200
- }
1201
1211
 
1202
- if (newIdx !== undefined) {
1203
- this._focusedChipIndex = newIdx;
1204
- }
1205
- }
1212
+ const hadHighlightedItem = this._hasHighlightedItem;
1206
1213
 
1207
- /** @private */
1208
- _onArrowRight(chips, event) {
1209
- if (this.inputElement.selectionStart !== 0) {
1210
- return;
1214
+ const isPrevKey = this.__isRTL ? event.key === 'ArrowRight' : event.key === 'ArrowLeft';
1215
+ if (isPrevKey) {
1216
+ this._highlightPrevChip();
1217
+ } else {
1218
+ this._highlightNextChip();
1211
1219
  }
1212
1220
 
1213
- const idx = this._focusedChipIndex;
1214
- if (idx !== -1) {
1221
+ // The label of the highlighted item was prefilled into the input, so
1222
+ // restore the filter once the highlight moved from the item to a chip.
1223
+ // Place the caret at the start of the input, so that the next arrow
1224
+ // key press continues to navigate the chips instead of moving the caret.
1225
+ if (hadHighlightedItem && this._hasHighlightedChip) {
1215
1226
  event.preventDefault();
1216
- }
1217
- let newIdx;
1218
-
1219
- if (this.__isRTL) {
1220
- if (idx === -1) {
1221
- // Focus last chip
1222
- newIdx = chips.length - 1;
1223
- } else if (idx > 0) {
1224
- // Focus prev chip
1225
- newIdx = idx - 1;
1226
- }
1227
- } else if (idx === chips.length - 1) {
1228
- // Blur last chip
1229
- newIdx = -1;
1230
- } else if (idx > -1) {
1231
- // Focus next chip
1232
- newIdx = idx + 1;
1233
- }
1234
-
1235
- if (newIdx !== undefined) {
1236
- this._focusedChipIndex = newIdx;
1227
+ this._inputElementValue = this.filter;
1228
+ this._setSelectionRange(0, 0);
1237
1229
  }
1238
1230
  }
1239
1231
 
1240
1232
  /** @private */
1241
- _onBackSpace(chips) {
1233
+ _onBackSpace() {
1242
1234
  if (this.inputElement.selectionStart !== 0) {
1243
1235
  return;
1244
1236
  }
1245
1237
 
1246
- const idx = this._focusedChipIndex;
1247
- if (idx === -1) {
1248
- this._focusedChipIndex = chips.length - 1;
1238
+ const chip = this._highlightedChip;
1239
+ if (chip) {
1240
+ this.__removeItem(chip.item);
1241
+ this._clearChipHighlight();
1249
1242
  } else {
1250
- this.__removeItem(chips[idx].item);
1251
- this._focusedChipIndex = -1;
1243
+ this._highlightLastChip();
1252
1244
  }
1253
1245
  }
1254
1246
 
1255
1247
  /** @private */
1256
- _focusedChipIndexChanged(focusedIndex, oldFocusedIndex) {
1257
- if (focusedIndex > -1 || oldFocusedIndex > -1) {
1258
- const chips = this._chips;
1259
- chips.forEach((chip, index) => {
1260
- chip.toggleAttribute('focused', index === focusedIndex);
1261
- });
1248
+ __updateChipHighlight(oldState) {
1249
+ const chips = this._chips;
1250
+
1251
+ if (oldState?.type === 'chip') {
1252
+ chips[oldState.index]?.removeAttribute('focused');
1253
+ }
1262
1254
 
1263
- // Announce focused chip
1264
- if (focusedIndex > -1) {
1265
- const item = chips[focusedIndex].item;
1266
- const itemLabel = this._getItemLabel(item);
1267
- announce(`${itemLabel} ${this.__effectiveI18n.focused}`);
1255
+ if (this._hasHighlightedChip) {
1256
+ const chip = chips[this._highlightState.index];
1257
+ if (chip) {
1258
+ chip.setAttribute('focused', '');
1259
+ announce(`${this._getItemLabel(chip.item)} ${this.__effectiveI18n.focused}`);
1268
1260
  }
1269
1261
  }
1270
1262
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { html, LitElement } from 'lit';
7
+ import { defineCustomElement } from '@vaadin/component-base/src/define.js';
8
+ import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
9
+ import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
10
+ import { multiSelectComboBoxSelectAllButtonStyles } from './styles/vaadin-multi-select-combo-box-select-all-button-base-styles.js';
11
+
12
+ /**
13
+ * An element used by `<vaadin-multi-select-combo-box>` to select or deselect
14
+ * all items shown in the dropdown. The element is not focusable itself, the
15
+ * keyboard highlight is reflected with the `focused` attribute while the DOM
16
+ * focus stays in the input.
17
+ *
18
+ * ### Styling
19
+ *
20
+ * The following state attributes are available for styling:
21
+ *
22
+ * Attribute | Description
23
+ * ------------|-------------
24
+ * `focused` | Set when the button is highlighted with the keyboard
25
+ *
26
+ * See [Styling Components](https://vaadin.com/docs/latest/styling/styling-components) documentation.
27
+ *
28
+ * @customElement vaadin-multi-select-combo-box-select-all-button
29
+ * @extends HTMLElement
30
+ * @private
31
+ */
32
+ class MultiSelectComboBoxSelectAllButton extends PolylitMixin(LumoInjectionMixin(LitElement)) {
33
+ static get is() {
34
+ return 'vaadin-multi-select-combo-box-select-all-button';
35
+ }
36
+
37
+ static get styles() {
38
+ return multiSelectComboBoxSelectAllButtonStyles;
39
+ }
40
+
41
+ static get properties() {
42
+ return {
43
+ /**
44
+ * The text shown in the button.
45
+ */
46
+ label: {
47
+ type: String,
48
+ sync: true,
49
+ },
50
+
51
+ /**
52
+ * True when the button is highlighted with the keyboard.
53
+ */
54
+ focused: {
55
+ type: Boolean,
56
+ value: false,
57
+ reflectToAttribute: true,
58
+ sync: true,
59
+ },
60
+ };
61
+ }
62
+
63
+ /** @protected */
64
+ render() {
65
+ return html`${this.label}`;
66
+ }
67
+
68
+ /** @protected */
69
+ firstUpdated() {
70
+ super.firstUpdated();
71
+
72
+ this.setAttribute('role', 'button');
73
+ }
74
+ }
75
+
76
+ defineCustomElement(MultiSelectComboBoxSelectAllButton);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import type { Constructor } from '@open-wc/dedupe-mixin';
7
+
8
+ /**
9
+ * A mixin that manages the select all button of `<vaadin-multi-select-combo-box>`.
10
+ */
11
+ export declare function MultiSelectComboBoxSelectAllMixin<T extends Constructor<HTMLElement>>(
12
+ base: T,
13
+ ): Constructor<MultiSelectComboBoxSelectAllMixinClass> & T;
14
+
15
+ export declare class MultiSelectComboBoxSelectAllMixinClass {
16
+ /**
17
+ * Set to true to show a button above the dropdown items for selecting
18
+ * or deselecting all items matching the current filter at once. Items
19
+ * that do not match the filter keep their selection state.
20
+ *
21
+ * The button is only supported with the `items` API. It is not shown
22
+ * when using `dataProvider`.
23
+ * @attr {boolean} select-all-button-visible
24
+ */
25
+ selectAllButtonVisible: boolean;
26
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2021 - 2026 Vaadin Ltd.
4
+ * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5
+ */
6
+ import { ComboBoxPlaceholder } from '@vaadin/combo-box/src/vaadin-combo-box-placeholder.js';
7
+ import { generateUniqueId } from '@vaadin/component-base/src/unique-id-utils.js';
8
+
9
+ /**
10
+ * A mixin that manages the select all button of `<vaadin-multi-select-combo-box>`.
11
+ */
12
+ export const MultiSelectComboBoxSelectAllMixin = (superClass) =>
13
+ class MultiSelectComboBoxSelectAllMixinClass extends superClass {
14
+ static get properties() {
15
+ return {
16
+ /**
17
+ * Set to true to show a button above the dropdown items for selecting
18
+ * or deselecting all items matching the current filter at once. Items
19
+ * that do not match the filter keep their selection state.
20
+ *
21
+ * The button is only supported with the `items` API. It is not shown
22
+ * when using `dataProvider`.
23
+ * @attr {boolean} select-all-button-visible
24
+ */
25
+ selectAllButtonVisible: {
26
+ type: Boolean,
27
+ value: false,
28
+ sync: true,
29
+ },
30
+
31
+ /**
32
+ * Shows the select all button with `dataProvider`, and is called instead of
33
+ * the local toggle while `_allSelected` is set. The function must update
34
+ * `selectedItems` and may return a promise. The component announces the
35
+ * result after it resolves, without firing `change`.
36
+ * Internal API for the Flow component.
37
+ * @private
38
+ */
39
+ _toggleSelectAllHandler: {
40
+ type: Function,
41
+ attribute: false,
42
+ sync: true,
43
+ },
44
+
45
+ /**
46
+ * Whether all items matching the filter are selected, set by the server
47
+ * with a data provider. When not set, the component computes it locally.
48
+ * @private
49
+ */
50
+ _allSelected: {
51
+ type: Boolean,
52
+ attribute: false,
53
+ sync: true,
54
+ },
55
+ };
56
+ }
57
+
58
+ #button;
59
+
60
+ constructor() {
61
+ super();
62
+
63
+ const button = document.createElement('vaadin-multi-select-combo-box-select-all-button');
64
+ button.setAttribute('slot', 'select-all');
65
+ button.id = `select-all-${this.localName}-${generateUniqueId()}`;
66
+ button.addEventListener('click', () => this._toggleSelectAll());
67
+ this.#button = button;
68
+ }
69
+
70
+ /** @protected */
71
+ get _hasSelectAllButton() {
72
+ return (
73
+ this.selectAllButtonVisible &&
74
+ !this.readonly &&
75
+ (!this.dataProvider || this.#hasActiveHandler) &&
76
+ this.#hasLoadedFilteredItems()
77
+ );
78
+ }
79
+
80
+ /** @protected */
81
+ willUpdate(props) {
82
+ super.willUpdate(props);
83
+
84
+ if (props.has('filter') || !this.opened || !this._hasSelectAllButton) {
85
+ this._clearSelectAllHighlight();
86
+ }
87
+ }
88
+
89
+ /** @protected */
90
+ updated(props) {
91
+ super.updated(props);
92
+
93
+ if (props.has('_highlightState')) {
94
+ this.#button.focused = this._isSelectAllHighlighted;
95
+
96
+ if (props.get('_highlightState')?.type === 'item' && this._isSelectAllHighlighted) {
97
+ // When highlighting the button, revert input value to the filter
98
+ this._revertInputValue();
99
+ }
100
+ }
101
+
102
+ // Properties that affect whether the button is shown, or the label
103
+ // of the button
104
+ const buttonProps = [
105
+ 'selectAllButtonVisible',
106
+ '_toggleSelectAllHandler',
107
+ '_allSelected',
108
+ 'readonly',
109
+ 'dataProvider',
110
+ 'filteredItems',
111
+ 'selectedItems',
112
+ 'itemIdPath',
113
+ 'filter',
114
+ '__effectiveI18n',
115
+ ];
116
+
117
+ if (buttonProps.some((prop) => props.has(prop))) {
118
+ this.#updateButton();
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Override method from `ComboBoxBaseMixin` to reference the select all
124
+ * button from the input while the button is highlighted.
125
+ * @protected
126
+ * @override
127
+ */
128
+ _updateActiveDescendant() {
129
+ if (this._isSelectAllHighlighted) {
130
+ this.inputElement?.setAttribute('aria-activedescendant', this.#button.id);
131
+ } else {
132
+ super._updateActiveDescendant();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Selects all items matching the current filter, or deselects
138
+ * them when all of them are already selected. Delegates to
139
+ * `_toggleSelectAllHandler` while `_allSelected` is set.
140
+ * @protected
141
+ */
142
+ _toggleSelectAll() {
143
+ if (this.#hasServerState) {
144
+ this.#toggleSelectAllWithHandler();
145
+ } else {
146
+ this.#toggleSelectAllLocally();
147
+ }
148
+ }
149
+
150
+ get #hasActiveHandler() {
151
+ return !!this.dataProvider && !!this._toggleSelectAllHandler;
152
+ }
153
+
154
+ get #hasServerState() {
155
+ return this.#hasActiveHandler && this._allSelected != null;
156
+ }
157
+
158
+ #updateButton() {
159
+ this.toggleAttribute('has-select-all', this._hasSelectAllButton);
160
+
161
+ if (!this._hasSelectAllButton) {
162
+ this.#button.remove();
163
+ return;
164
+ }
165
+
166
+ const button = this.#button;
167
+ button.label = this.#getButtonLabel();
168
+
169
+ if (button.parentNode !== this) {
170
+ this.appendChild(button);
171
+ }
172
+ }
173
+
174
+ #getButtonLabel() {
175
+ const { selectAll, deselectAll, selectFiltered, deselectFiltered } = this.__effectiveI18n;
176
+ const allSelected = this.#hasServerState ? this._allSelected : this.#areAllFilteredItemsSelected();
177
+
178
+ if (this.filter) {
179
+ return allSelected ? deselectFiltered : selectFiltered;
180
+ }
181
+
182
+ return allSelected ? deselectAll : selectAll;
183
+ }
184
+
185
+ async #toggleSelectAllWithHandler() {
186
+ // The handler owns the selection and is expected to update
187
+ // `selectedItems` itself. Intentionally do not fire `change` and do
188
+ // not request validation here, only announce the result.
189
+ try {
190
+ await this._toggleSelectAllHandler();
191
+ } catch (error) {
192
+ console.error(error);
193
+ return;
194
+ }
195
+
196
+ this.__announceSelection();
197
+ }
198
+
199
+ #toggleSelectAllLocally() {
200
+ const filteredItems = this.#getFilteredItems();
201
+ let selectedItems;
202
+
203
+ if (this.#areAllFilteredItemsSelected()) {
204
+ selectedItems = this.selectedItems.filter((item) => !this.#includesItem(filteredItems, item));
205
+ } else {
206
+ const missingItems = filteredItems.filter((item) => !this.#includesItem(this.selectedItems, item));
207
+ selectedItems = [...this.selectedItems, ...missingItems];
208
+ }
209
+
210
+ this.__updateSelection(selectedItems);
211
+ this.__announceSelection();
212
+ }
213
+
214
+ #areAllFilteredItemsSelected() {
215
+ const items = this.#getFilteredItems();
216
+ return items.length > 0 && items.every((item) => this.#includesItem(this.selectedItems, item));
217
+ }
218
+
219
+ #hasLoadedFilteredItems() {
220
+ // Hide the button while no page is loaded, for example after `clearCache()`
221
+ return (this.filteredItems || []).some((item) => !(item instanceof ComboBoxPlaceholder));
222
+ }
223
+
224
+ #getFilteredItems() {
225
+ return this.filteredItems || [];
226
+ }
227
+
228
+ #includesItem(items, item) {
229
+ return items.some((other) => this._isSameItem(item, other));
230
+ }
231
+ };