@vaadin/multi-select-combo-box 25.3.0 → 25.4.0-dev.55bb86e155

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) {
@@ -1071,11 +1101,13 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1071
1101
 
1072
1102
  if (this.readonly) {
1073
1103
  this.close();
1104
+ } else if (this._isSelectAllHighlighted) {
1105
+ this._toggleSelectAll();
1074
1106
  } else if (this._hasValidInputValue()) {
1075
1107
  // Keep selected item focused after committing on Enter.
1076
- const focusedItem = this._dropdownItems[this._focusedIndex];
1108
+ const focusedItem = this._highlightedItem;
1077
1109
  this._commitValue();
1078
- this._focusedIndex = this._dropdownItems.indexOf(focusedItem);
1110
+ this._highlightItem(focusedItem);
1079
1111
  }
1080
1112
 
1081
1113
  return;
@@ -1085,8 +1117,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1085
1117
  }
1086
1118
 
1087
1119
  /**
1088
- * Override method inherited from the combo-box
1089
- * to not update focused item when readonly.
1120
+ * Override method from `ComboBoxBaseMixin` to not update focused
1121
+ * item when readonly.
1090
1122
  * @protected
1091
1123
  * @override
1092
1124
  */
@@ -1099,8 +1131,8 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1099
1131
  }
1100
1132
 
1101
1133
  /**
1102
- * Override method inherited from the combo-box
1103
- * to not update focused item when readonly.
1134
+ * Override method from `ComboBoxBaseMixin` to not update focused
1135
+ * item when readonly.
1104
1136
  * @protected
1105
1137
  * @override
1106
1138
  */
@@ -1121,120 +1153,80 @@ export const MultiSelectComboBoxMixin = (superClass) =>
1121
1153
  _onKeyDown(event) {
1122
1154
  super._onKeyDown(event);
1123
1155
 
1124
- const chips = this._chips;
1125
-
1126
- if (!this.readonly && chips.length > 0) {
1156
+ if (!this.readonly && this._chips.length > 0) {
1127
1157
  switch (event.key) {
1128
1158
  case 'Backspace':
1129
- this._onBackSpace(chips);
1159
+ this._onBackSpace();
1130
1160
  break;
1131
1161
  case 'ArrowLeft':
1132
- this._onArrowLeft(chips, event);
1133
- break;
1134
1162
  case 'ArrowRight':
1135
- this._onArrowRight(chips, event);
1163
+ this._onChipArrow(event);
1136
1164
  break;
1137
1165
  default:
1138
- this._focusedChipIndex = -1;
1166
+ this._clearChipHighlight();
1139
1167
  break;
1140
1168
  }
1141
1169
  }
1142
1170
  }
1143
1171
 
1144
1172
  /** @private */
1145
- _onArrowLeft(chips, event) {
1173
+ _onChipArrow(event) {
1146
1174
  if (this.inputElement.selectionStart !== 0) {
1147
1175
  return;
1148
1176
  }
1149
1177
 
1150
- const idx = this._focusedChipIndex;
1151
- if (idx !== -1) {
1178
+ if (this._hasHighlightedChip) {
1152
1179
  event.preventDefault();
1153
1180
  }
1154
- let newIdx;
1155
-
1156
- if (!this.__isRTL) {
1157
- if (idx === -1) {
1158
- // Focus last chip
1159
- newIdx = chips.length - 1;
1160
- } else if (idx > 0) {
1161
- // Focus prev chip
1162
- newIdx = idx - 1;
1163
- }
1164
- } else if (idx === chips.length - 1) {
1165
- // Blur last chip
1166
- newIdx = -1;
1167
- } else if (idx > -1) {
1168
- // Focus next chip
1169
- newIdx = idx + 1;
1170
- }
1171
1181
 
1172
- if (newIdx !== undefined) {
1173
- this._focusedChipIndex = newIdx;
1174
- }
1175
- }
1182
+ const hadHighlightedItem = this._hasHighlightedItem;
1176
1183
 
1177
- /** @private */
1178
- _onArrowRight(chips, event) {
1179
- if (this.inputElement.selectionStart !== 0) {
1180
- return;
1184
+ const isPrevKey = this.__isRTL ? event.key === 'ArrowRight' : event.key === 'ArrowLeft';
1185
+ if (isPrevKey) {
1186
+ this._highlightPrevChip();
1187
+ } else {
1188
+ this._highlightNextChip();
1181
1189
  }
1182
1190
 
1183
- const idx = this._focusedChipIndex;
1184
- if (idx !== -1) {
1191
+ // The label of the highlighted item was prefilled into the input, so
1192
+ // restore the filter once the highlight moved from the item to a chip.
1193
+ // Place the caret at the start of the input, so that the next arrow
1194
+ // key press continues to navigate the chips instead of moving the caret.
1195
+ if (hadHighlightedItem && this._hasHighlightedChip) {
1185
1196
  event.preventDefault();
1186
- }
1187
- let newIdx;
1188
-
1189
- if (this.__isRTL) {
1190
- if (idx === -1) {
1191
- // Focus last chip
1192
- newIdx = chips.length - 1;
1193
- } else if (idx > 0) {
1194
- // Focus prev chip
1195
- newIdx = idx - 1;
1196
- }
1197
- } else if (idx === chips.length - 1) {
1198
- // Blur last chip
1199
- newIdx = -1;
1200
- } else if (idx > -1) {
1201
- // Focus next chip
1202
- newIdx = idx + 1;
1203
- }
1204
-
1205
- if (newIdx !== undefined) {
1206
- this._focusedChipIndex = newIdx;
1197
+ this._inputElementValue = this.filter;
1198
+ this._setSelectionRange(0, 0);
1207
1199
  }
1208
1200
  }
1209
1201
 
1210
1202
  /** @private */
1211
- _onBackSpace(chips) {
1203
+ _onBackSpace() {
1212
1204
  if (this.inputElement.selectionStart !== 0) {
1213
1205
  return;
1214
1206
  }
1215
1207
 
1216
- const idx = this._focusedChipIndex;
1217
- if (idx === -1) {
1218
- this._focusedChipIndex = chips.length - 1;
1208
+ const chip = this._highlightedChip;
1209
+ if (chip) {
1210
+ this.__removeItem(chip.item);
1211
+ this._clearChipHighlight();
1219
1212
  } else {
1220
- this.__removeItem(chips[idx].item);
1221
- this._focusedChipIndex = -1;
1213
+ this._highlightLastChip();
1222
1214
  }
1223
1215
  }
1224
1216
 
1225
1217
  /** @private */
1226
- _focusedChipIndexChanged(focusedIndex, oldFocusedIndex) {
1227
- if (focusedIndex > -1 || oldFocusedIndex > -1) {
1228
- const chips = this._chips;
1229
- chips.forEach((chip, index) => {
1230
- chip.toggleAttribute('focused', index === focusedIndex);
1231
- });
1218
+ __updateChipHighlight(oldState) {
1219
+ const chips = this._chips;
1220
+
1221
+ if (oldState?.type === 'chip') {
1222
+ chips[oldState.index]?.removeAttribute('focused');
1223
+ }
1232
1224
 
1233
- // Announce focused chip
1234
- if (focusedIndex > -1) {
1235
- const item = chips[focusedIndex].item;
1236
- const itemLabel = this._getItemLabel(item);
1237
- announce(`${itemLabel} ${this.__effectiveI18n.focused}`);
1225
+ if (this._hasHighlightedChip) {
1226
+ const chip = chips[this._highlightState.index];
1227
+ if (chip) {
1228
+ chip.setAttribute('focused', '');
1229
+ announce(`${this._getItemLabel(chip.item)} ${this.__effectiveI18n.focused}`);
1238
1230
  }
1239
1231
  }
1240
1232
  }
@@ -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
+ };