@vaadin/multi-select-combo-box 25.3.0-rc1 → 25.4.0-alpha1
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/custom-elements.json +151 -9
- package/package.json +15 -15
- package/src/styles/vaadin-multi-select-combo-box-base-styles.js +11 -0
- package/src/styles/vaadin-multi-select-combo-box-select-all-button-base-styles.js +44 -0
- package/src/vaadin-multi-select-combo-box-highlight-mixin.d.ts +19 -0
- package/src/vaadin-multi-select-combo-box-highlight-mixin.js +126 -0
- package/src/vaadin-multi-select-combo-box-mixin.d.ts +22 -2
- package/src/vaadin-multi-select-combo-box-mixin.js +93 -101
- package/src/vaadin-multi-select-combo-box-select-all-button.js +76 -0
- package/src/vaadin-multi-select-combo-box-select-all-mixin.d.ts +26 -0
- package/src/vaadin-multi-select-combo-box-select-all-mixin.js +231 -0
- package/src/vaadin-multi-select-combo-box.d.ts +7 -0
- package/src/vaadin-multi-select-combo-box.js +8 -0
- package/web-types.json +23 -4
- package/web-types.lit.json +10 -3
|
@@ -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
|
-
|
|
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,
|
|
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 {
|
|
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.
|
|
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.
|
|
501
|
-
const focusedItem = this.
|
|
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.
|
|
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.
|
|
670
|
+
const hasInvalidOption = !this._hasHighlightedItem && this._inputElementValue !== '';
|
|
651
671
|
return this.allowCustomValue || !hasInvalidOption;
|
|
652
672
|
}
|
|
653
673
|
|
|
654
674
|
/**
|
|
655
|
-
* Override method
|
|
656
|
-
*
|
|
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.
|
|
1108
|
+
const focusedItem = this._highlightedItem;
|
|
1077
1109
|
this._commitValue();
|
|
1078
|
-
this.
|
|
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
|
|
1089
|
-
*
|
|
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
|
|
1103
|
-
*
|
|
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
|
-
|
|
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(
|
|
1159
|
+
this._onBackSpace();
|
|
1130
1160
|
break;
|
|
1131
1161
|
case 'ArrowLeft':
|
|
1132
|
-
this._onArrowLeft(chips, event);
|
|
1133
|
-
break;
|
|
1134
1162
|
case 'ArrowRight':
|
|
1135
|
-
this.
|
|
1163
|
+
this._onChipArrow(event);
|
|
1136
1164
|
break;
|
|
1137
1165
|
default:
|
|
1138
|
-
this.
|
|
1166
|
+
this._clearChipHighlight();
|
|
1139
1167
|
break;
|
|
1140
1168
|
}
|
|
1141
1169
|
}
|
|
1142
1170
|
}
|
|
1143
1171
|
|
|
1144
1172
|
/** @private */
|
|
1145
|
-
|
|
1173
|
+
_onChipArrow(event) {
|
|
1146
1174
|
if (this.inputElement.selectionStart !== 0) {
|
|
1147
1175
|
return;
|
|
1148
1176
|
}
|
|
1149
1177
|
|
|
1150
|
-
|
|
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
|
-
|
|
1173
|
-
this._focusedChipIndex = newIdx;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1182
|
+
const hadHighlightedItem = this._hasHighlightedItem;
|
|
1176
1183
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
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
|
-
|
|
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(
|
|
1203
|
+
_onBackSpace() {
|
|
1212
1204
|
if (this.inputElement.selectionStart !== 0) {
|
|
1213
1205
|
return;
|
|
1214
1206
|
}
|
|
1215
1207
|
|
|
1216
|
-
const
|
|
1217
|
-
if (
|
|
1218
|
-
this.
|
|
1208
|
+
const chip = this._highlightedChip;
|
|
1209
|
+
if (chip) {
|
|
1210
|
+
this.__removeItem(chip.item);
|
|
1211
|
+
this._clearChipHighlight();
|
|
1219
1212
|
} else {
|
|
1220
|
-
this.
|
|
1221
|
-
this._focusedChipIndex = -1;
|
|
1213
|
+
this._highlightLastChip();
|
|
1222
1214
|
}
|
|
1223
1215
|
}
|
|
1224
1216
|
|
|
1225
1217
|
/** @private */
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
announce(`${
|
|
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
|
+
};
|