@crowdstrike/glide-core 0.29.2 → 0.30.0
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/dist/accordion.js +240 -1
- package/dist/accordion.styles.js +13 -7
- package/dist/button-group.button.js +143 -1
- package/dist/button-group.button.styles.js +43 -15
- package/dist/button-group.js +249 -1
- package/dist/button-group.styles.js +10 -5
- package/dist/button.js +206 -1
- package/dist/button.styles.js +12 -7
- package/dist/checkbox-group.js +479 -14
- package/dist/checkbox-group.styles.js +5 -2
- package/dist/checkbox.js +519 -32
- package/dist/checkbox.styles.js +10 -5
- package/dist/drawer.js +168 -1
- package/dist/drawer.styles.js +5 -2
- package/dist/dropdown.js +2423 -123
- package/dist/dropdown.option.js +536 -1
- package/dist/dropdown.option.styles.js +5 -2
- package/dist/dropdown.styles.js +14 -7
- package/dist/form-controls-layout.js +102 -1
- package/dist/form-controls-layout.styles.js +5 -2
- package/dist/icon-button.js +139 -1
- package/dist/icon-button.styles.js +19 -7
- package/dist/icons/checked.js +28 -1
- package/dist/icons/chevron.js +21 -1
- package/dist/icons/magnifying-glass.js +23 -1
- package/dist/icons/pencil.js +21 -1
- package/dist/icons/severity-critical.js +20 -1
- package/dist/icons/severity-informational.js +20 -1
- package/dist/icons/severity-medium.js +20 -1
- package/dist/icons/x.js +21 -1
- package/dist/inline-alert.js +118 -1
- package/dist/inline-alert.styles.js +5 -2
- package/dist/input.d.ts +8 -2
- package/dist/input.js +505 -41
- package/dist/input.styles.js +25 -4
- package/dist/label.js +303 -1
- package/dist/label.styles.js +11 -5
- package/dist/library/assert-slot.js +136 -1
- package/dist/library/expect-unhandled-rejection.js +14 -1
- package/dist/library/expect-window-error.js +26 -1
- package/dist/library/final.js +18 -1
- package/dist/library/form-control.js +1 -1
- package/dist/library/localize.js +10 -1
- package/dist/library/mouse.js +35 -1
- package/dist/library/on-resize.js +24 -1
- package/dist/library/required.js +35 -1
- package/dist/library/shadow-root-mode.js +4 -1
- package/dist/library/unique-id.js +3 -1
- package/dist/link.js +92 -1
- package/dist/link.styles.js +10 -5
- package/dist/menu.d.ts +3 -2
- package/dist/menu.js +1259 -1
- package/dist/menu.styles.js +34 -17
- package/dist/modal.d.ts +4 -0
- package/dist/modal.icon-button.js +60 -1
- package/dist/modal.icon-button.styles.js +5 -2
- package/dist/modal.js +473 -1
- package/dist/modal.styles.js +71 -22
- package/dist/option.d.ts +74 -0
- package/dist/option.js +498 -0
- package/dist/option.styles.js +140 -0
- package/dist/{menu.options.d.ts → options.d.ts} +5 -6
- package/dist/options.js +130 -0
- package/dist/options.styles.js +21 -0
- package/dist/popover.js +620 -1
- package/dist/popover.styles.js +11 -5
- package/dist/radio-group.js +624 -17
- package/dist/radio-group.radio.js +211 -1
- package/dist/radio-group.radio.styles.js +9 -4
- package/dist/radio-group.styles.js +5 -2
- package/dist/slider.js +1040 -61
- package/dist/slider.styles.js +9 -4
- package/dist/spinner.js +60 -1
- package/dist/spinner.styles.js +5 -2
- package/dist/split-button.js +116 -1
- package/dist/split-button.primary-button.js +100 -1
- package/dist/split-button.primary-button.styles.js +13 -6
- package/dist/split-button.primary-link.js +102 -1
- package/dist/split-button.secondary-button.d.ts +2 -3
- package/dist/split-button.secondary-button.js +121 -1
- package/dist/split-button.secondary-button.styles.js +12 -7
- package/dist/split-button.styles.js +9 -4
- package/dist/styles/focus-outline.js +9 -3
- package/dist/styles/fonts.css +6 -1
- package/dist/styles/opacity-and-scale-animation.js +6 -3
- package/dist/styles/skeleton.js +6 -3
- package/dist/styles/variables.css +410 -1
- package/dist/styles/visually-hidden.js +6 -3
- package/dist/tab.group.js +386 -1
- package/dist/tab.group.styles.js +5 -2
- package/dist/tab.js +133 -1
- package/dist/tab.panel.js +93 -1
- package/dist/tab.panel.styles.js +11 -5
- package/dist/tab.styles.js +9 -4
- package/dist/tag.js +207 -1
- package/dist/tag.styles.js +10 -5
- package/dist/textarea.js +353 -19
- package/dist/textarea.styles.js +23 -4
- package/dist/toast.js +130 -1
- package/dist/toast.toasts.js +248 -25
- package/dist/toast.toasts.styles.js +9 -4
- package/dist/toggle.js +178 -1
- package/dist/toggle.styles.js +25 -5
- package/dist/tooltip.container.js +130 -1
- package/dist/tooltip.container.styles.js +5 -2
- package/dist/tooltip.js +484 -1
- package/dist/tooltip.styles.js +21 -5
- package/dist/translations/en.js +36 -1
- package/dist/translations/fr.js +37 -1
- package/dist/translations/ja.js +37 -1
- package/package.json +8 -12
- package/dist/menu.button.d.ts +0 -42
- package/dist/menu.button.js +0 -1
- package/dist/menu.button.styles.js +0 -32
- package/dist/menu.link.d.ts +0 -44
- package/dist/menu.link.js +0 -1
- package/dist/menu.link.styles.js +0 -35
- package/dist/menu.options.js +0 -1
- package/dist/menu.options.styles.d.ts +0 -2
- package/dist/menu.options.styles.js +0 -20
- /package/dist/{menu.button.styles.d.ts → option.styles.d.ts} +0 -0
- /package/dist/{menu.link.styles.d.ts → options.styles.d.ts} +0 -0
package/dist/dropdown.js
CHANGED
@@ -1,16 +1,512 @@
|
|
1
|
-
var __decorate
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
6
|
+
};
|
7
|
+
import './icon-button.js';
|
8
|
+
import './label.js';
|
9
|
+
import './tooltip.js';
|
10
|
+
import { html, LitElement } from 'lit';
|
11
|
+
import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom';
|
12
|
+
import { classMap } from 'lit/directives/class-map.js';
|
13
|
+
import { createRef, ref } from 'lit/directives/ref.js';
|
14
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
15
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
16
|
+
import { repeat } from 'lit/directives/repeat.js';
|
17
|
+
import { range } from 'lit/directives/range.js';
|
18
|
+
import { map } from 'lit/directives/map.js';
|
19
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
20
|
+
import { when } from 'lit/directives/when.js';
|
21
|
+
import packageJson from '../package.json' with { type: 'json' };
|
22
|
+
import onResize from './library/on-resize.js';
|
23
|
+
import DropdownOption from './dropdown.option.js';
|
24
|
+
import { LocalizeController } from './library/localize.js';
|
25
|
+
import Tag from './tag.js';
|
26
|
+
import chevronIcon from './icons/chevron.js';
|
27
|
+
import magnifyingGlassIcon from './icons/magnifying-glass.js';
|
28
|
+
import pencilIcon from './icons/pencil.js';
|
29
|
+
import styles from './dropdown.styles.js';
|
30
|
+
import assertSlot from './library/assert-slot.js';
|
31
|
+
import shadowRootMode from './library/shadow-root-mode.js';
|
32
|
+
import final from './library/final.js';
|
33
|
+
import required from './library/required.js';
|
34
|
+
import uniqueId from './library/unique-id.js';
|
35
|
+
/**
|
36
|
+
* @attr {string} label
|
37
|
+
* @attr {boolean} [add-button=false]
|
38
|
+
* @attr {boolean} [disabled=false]
|
39
|
+
* @attr {boolean} [filterable=false]
|
40
|
+
* @attr {boolean} [hide-label=false]
|
41
|
+
* @attr {boolean} [loading=false]
|
42
|
+
* @attr {boolean} [multiple=false]
|
43
|
+
* @attr {string} [name='']
|
44
|
+
* @attr {boolean} [open=false]
|
45
|
+
* @attr {'horizontal'|'vertical'} [orientation='horizontal']
|
46
|
+
* @attr {string} [placeholder]
|
47
|
+
* @attr {boolean} [readonly=false]
|
48
|
+
* @attr {boolean} [required=false]
|
49
|
+
* @attr {boolean} [select-all=false]
|
50
|
+
* @attr {string} [tooltip]
|
51
|
+
* @attr {string[]} [value=[]]
|
52
|
+
* @attr {'quiet'} [variant]
|
53
|
+
*
|
54
|
+
* @readonly
|
55
|
+
* @attr {string} [version]
|
56
|
+
*
|
57
|
+
* @slot {DropdownOption}
|
58
|
+
* @slot {Element | string} [description] - Additional information or context
|
59
|
+
* @slot {Element} [icon:value] - Icons for the selected Dropdown Option(s). Slot one icon per Dropdown Option. `<value>` should be equal to the `value` of each Dropdown Option.
|
60
|
+
*
|
61
|
+
* @fires {CustomEvent} add
|
62
|
+
* @fires {Event} change
|
63
|
+
* @fires {Event} input
|
64
|
+
* @fires {Event} invalid
|
65
|
+
* @fires {Event} toggle
|
66
|
+
*
|
67
|
+
* @readonly
|
68
|
+
* @prop {HTMLFormElement | null} form
|
69
|
+
*
|
70
|
+
* @readonly
|
71
|
+
* @prop {ValidityState} validity
|
72
|
+
*
|
73
|
+
* @method checkValidity
|
74
|
+
* @returns boolean
|
75
|
+
*
|
76
|
+
* @method filter
|
77
|
+
* @param {string} query
|
78
|
+
* @returns Promise<DropdownOption[] | undefined | void>
|
79
|
+
*
|
80
|
+
* @method formAssociatedCallback
|
81
|
+
* @method formResetCallback
|
82
|
+
*
|
83
|
+
* @method reportValidity
|
84
|
+
* @returns boolean
|
85
|
+
*
|
86
|
+
* @method resetValidityFeedback
|
87
|
+
*
|
88
|
+
* @method setCustomValidity
|
89
|
+
* @param {string} message
|
90
|
+
*
|
91
|
+
* @method setValidity
|
92
|
+
* @param {ValidityStateFlags} [flags]
|
93
|
+
* @param {string} [message]
|
94
|
+
*/
|
95
|
+
let Dropdown = class Dropdown extends LitElement {
|
96
|
+
static { this.formAssociated = true; }
|
97
|
+
static { this.shadowRootOptions = {
|
98
|
+
...LitElement.shadowRootOptions,
|
99
|
+
mode: shadowRootMode,
|
100
|
+
}; }
|
101
|
+
static { this.styles = styles; }
|
102
|
+
/**
|
103
|
+
* @default false
|
104
|
+
*/
|
105
|
+
get disabled() {
|
106
|
+
return this.#isDisabled;
|
107
|
+
}
|
108
|
+
set disabled(isDisabled) {
|
109
|
+
this.#isDisabled = isDisabled;
|
110
|
+
if (this.open && isDisabled) {
|
111
|
+
this.#hide();
|
112
|
+
}
|
113
|
+
else if (this.open) {
|
114
|
+
this.#show();
|
115
|
+
}
|
116
|
+
}
|
117
|
+
/**
|
118
|
+
* @default false
|
119
|
+
*/
|
120
|
+
get filterable() {
|
121
|
+
return this.#isFilterable;
|
122
|
+
}
|
123
|
+
set filterable(isFilterable) {
|
124
|
+
if (this.#isFilterable !== isFilterable && isFilterable && !this.multiple) {
|
125
|
+
if (this.#inputElementRef.value &&
|
126
|
+
this.lastSelectedAndEnabledOption?.label) {
|
127
|
+
this.#inputElementRef.value.value =
|
128
|
+
this.lastSelectedAndEnabledOption.label;
|
129
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
130
|
+
this.isInputOverflowing =
|
131
|
+
this.#inputElementRef.value.scrollWidth >
|
132
|
+
this.#inputElementRef.value.clientWidth;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
else if (this.#isFilterable !== isFilterable) {
|
136
|
+
this.#unfilter();
|
137
|
+
}
|
138
|
+
this.#isFilterable = isFilterable;
|
139
|
+
}
|
140
|
+
/**
|
141
|
+
* @default false
|
142
|
+
*/
|
143
|
+
get open() {
|
144
|
+
return this.#isOpen;
|
145
|
+
}
|
146
|
+
set open(isOpen) {
|
147
|
+
const hasChanged = isOpen !== this.#isOpen;
|
148
|
+
this.#isOpen = isOpen;
|
149
|
+
if (isOpen && hasChanged && !this.disabled) {
|
150
|
+
this.#show();
|
151
|
+
this.dispatchEvent(new Event('toggle', { bubbles: true, composed: true }));
|
152
|
+
return;
|
153
|
+
}
|
154
|
+
if (!this.open && hasChanged) {
|
155
|
+
if (!this.multiple &&
|
156
|
+
this.#inputElementRef.value &&
|
157
|
+
this.lastSelectedAndEnabledOption?.label) {
|
158
|
+
this.#inputElementRef.value.value =
|
159
|
+
this.lastSelectedAndEnabledOption.label;
|
160
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
161
|
+
}
|
162
|
+
else if (!this.multiple &&
|
163
|
+
this.#inputElementRef.value &&
|
164
|
+
this.selectedAndEnabledOptions.length === 0) {
|
165
|
+
this.#inputElementRef.value.value = '';
|
166
|
+
this.inputValue = '';
|
167
|
+
}
|
168
|
+
else if (this.multiple && this.#inputElementRef.value) {
|
169
|
+
this.#inputElementRef.value.value = '';
|
170
|
+
this.inputValue = '';
|
171
|
+
}
|
172
|
+
this.#unfilter();
|
173
|
+
this.#hide();
|
174
|
+
this.dispatchEvent(new Event('toggle', { bubbles: true, composed: true }));
|
175
|
+
}
|
176
|
+
}
|
177
|
+
/**
|
178
|
+
* @default false
|
179
|
+
*/
|
180
|
+
get multiple() {
|
181
|
+
return this.#isMultiple;
|
182
|
+
}
|
183
|
+
set multiple(isMultiple) {
|
184
|
+
const wasMultiple = this.#isMultiple && !isMultiple;
|
185
|
+
const wasSingle = !this.#isMultiple && isMultiple;
|
186
|
+
this.#isMultiple = isMultiple;
|
187
|
+
for (const option of this.#optionElements) {
|
188
|
+
option.privateMultiple = isMultiple;
|
189
|
+
// A single-select Dropdown can only have one option selected. So all but the
|
190
|
+
// last selected and enabled option is deselected.
|
191
|
+
if (wasMultiple && option !== this.lastSelectedAndEnabledOption) {
|
192
|
+
option.selected = false;
|
193
|
+
}
|
194
|
+
}
|
195
|
+
if (wasMultiple && this.lastSelectedAndEnabledOption) {
|
196
|
+
this.#value = this.lastSelectedAndEnabledOption?.value
|
197
|
+
? [this.lastSelectedAndEnabledOption.value]
|
198
|
+
: [];
|
199
|
+
this.selectedAndEnabledOptions = [this.lastSelectedAndEnabledOption];
|
200
|
+
this.isShowSingleSelectIcon = Boolean(this.lastSelectedAndEnabledOption?.value);
|
201
|
+
if (this.#inputElementRef.value &&
|
202
|
+
this.lastSelectedAndEnabledOption?.label) {
|
203
|
+
this.#inputElementRef.value.value =
|
204
|
+
this.lastSelectedAndEnabledOption.label;
|
205
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
else if (wasSingle && this.lastSelectedAndEnabledOption) {
|
209
|
+
// If Dropdown was single-select and filterable and an option is selected,
|
210
|
+
// then the value of its `<input>` is set to the label of the selected option.
|
211
|
+
// That behavior doesn't apply to multiselect Dropdown because its selected
|
212
|
+
// option or options are represented by tags. So we clear input field.
|
213
|
+
if (this.#inputElementRef.value) {
|
214
|
+
this.#inputElementRef.value.value = '';
|
215
|
+
this.inputValue = '';
|
216
|
+
}
|
217
|
+
this.lastSelectedAndEnabledOption.privateUpdateCheckbox();
|
218
|
+
this.isShowSingleSelectIcon = false;
|
219
|
+
}
|
220
|
+
}
|
221
|
+
// Intentionally not reflected to match native.
|
222
|
+
/**
|
223
|
+
* @default []
|
224
|
+
*/
|
225
|
+
get value() {
|
226
|
+
return this.#value;
|
227
|
+
}
|
228
|
+
set value(value) {
|
229
|
+
if (!this.multiple && value.length > 1) {
|
230
|
+
throw new Error('Only one value is allowed when not `multiple`.');
|
231
|
+
}
|
232
|
+
this.#value = value;
|
233
|
+
// `#onOptionsSelectedChange()` is called when an option is selected. It updates
|
234
|
+
// `this.selectedAndEnabledOptions`. Deselecting every option before reselecting
|
235
|
+
// those in `value` ensures tags appear in the same order as in `value`.
|
236
|
+
for (const option of this.selectedAndEnabledOptions) {
|
237
|
+
this.#isSelectionFromValueSetter = true;
|
238
|
+
option.selected = false;
|
239
|
+
this.#isSelectionFromValueSetter = false;
|
240
|
+
}
|
241
|
+
for (const optionValue of value) {
|
242
|
+
// We select only the first option with a given value so what the user sees as
|
243
|
+
// selected matches what is submitted with the form. If we were to select every
|
244
|
+
// matching option here, then the user would be under the impression that multiple
|
245
|
+
// instances of that value will be submitted with the form.
|
246
|
+
//
|
247
|
+
// Imagine a case where there are two options whose value is "one" and `value`
|
248
|
+
// is `['one']`.
|
249
|
+
const option = this.#optionElements.find((option) => option.value === optionValue &&
|
250
|
+
// There may be more than one of the same value in `value`. If there is, we want
|
251
|
+
// more than one option to be selected. Without this condition, only the first
|
252
|
+
// option with the value would be selected.
|
253
|
+
!option.selected);
|
254
|
+
if (option) {
|
255
|
+
this.#isSelectionFromValueSetter = true;
|
256
|
+
option.selected = true;
|
257
|
+
this.#isSelectionFromValueSetter = false;
|
258
|
+
}
|
259
|
+
}
|
260
|
+
// When an option is selected, `#inputElementRef.value.value` is set the `label`
|
261
|
+
// of the selected option. If Dropdown's `value` has been emptied, it means an
|
262
|
+
// option is no longer selected. So the `value` of `#inputElementRef.value` should
|
263
|
+
// be emptied too.
|
264
|
+
if (!this.multiple &&
|
265
|
+
this.value.length === 0 &&
|
266
|
+
this.#inputElementRef.value) {
|
267
|
+
this.#inputElementRef.value.value = '';
|
268
|
+
this.inputValue = '';
|
269
|
+
}
|
270
|
+
else if (!this.multiple &&
|
271
|
+
this.lastSelectedAndEnabledOption?.label &&
|
272
|
+
this.#inputElementRef.value) {
|
273
|
+
this.#inputElementRef.value.value =
|
274
|
+
this.lastSelectedAndEnabledOption.label;
|
275
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
get activeOption() {
|
279
|
+
return this.#optionElementsIncludingSelectAll?.find(({ privateActive }) => privateActive);
|
280
|
+
}
|
281
|
+
get areAllOptionsSelected() {
|
282
|
+
return (this.#optionElements.length > 0 &&
|
283
|
+
this.#optionElements.filter(({ selected }) => selected).length ===
|
284
|
+
this.#optionElements.filter(({ disabled }) => !disabled).length);
|
285
|
+
}
|
286
|
+
get areSomeOptionsSelected() {
|
287
|
+
return this.#optionElements.some(({ selected }) => selected);
|
288
|
+
}
|
289
|
+
checkValidity() {
|
290
|
+
this.isCheckingValidity = true;
|
291
|
+
const isValid = this.#internals.checkValidity();
|
292
|
+
this.isCheckingValidity = false;
|
293
|
+
return isValid;
|
294
|
+
}
|
295
|
+
click() {
|
296
|
+
if (this.filterable || this.isFilterable) {
|
297
|
+
this.#inputElementRef.value?.click();
|
298
|
+
this.#inputElementRef.value?.select();
|
299
|
+
}
|
300
|
+
else {
|
301
|
+
this.#primaryButtonElementRef.value?.click();
|
302
|
+
}
|
303
|
+
}
|
304
|
+
get lastSelectedAndEnabledOption() {
|
305
|
+
return this.selectedAndEnabledOptions.at(-1);
|
306
|
+
}
|
307
|
+
get internalLabel() {
|
308
|
+
const isFilterable = this.filterable || this.isFilterable;
|
309
|
+
return !isFilterable && !this.lastSelectedAndEnabledOption
|
310
|
+
? this.placeholder
|
311
|
+
: !this.multiple && !isFilterable && this.lastSelectedAndEnabledOption
|
312
|
+
? this.lastSelectedAndEnabledOption.label
|
313
|
+
: '';
|
314
|
+
}
|
315
|
+
connectedCallback() {
|
316
|
+
super.connectedCallback();
|
317
|
+
document.addEventListener('click', this.#onDocumentClick, {
|
318
|
+
// 1. The consumer has a click handler on a button.
|
319
|
+
// 2. The user clicks the button.
|
320
|
+
// 3. The button's click handler is called and it sets `this.open` to `true`.
|
321
|
+
// 4. The "click" event bubbles up and is handled by `#onDocumentClick`.
|
322
|
+
// 5. That handler sets `open` to `false` because the click came from outside
|
323
|
+
// Dropdown.
|
324
|
+
// 6. Dropdown is opened then closed in the same frame and so never opens.
|
325
|
+
//
|
326
|
+
// `capture` ensures `#onDocumentClick` is called before #3, so the button click
|
327
|
+
// handler setting `open` to `true` isn't overwritten by this handler setting
|
328
|
+
// `open` to `false`.
|
329
|
+
capture: true,
|
330
|
+
});
|
331
|
+
}
|
332
|
+
createRenderRoot() {
|
333
|
+
this.#shadowRoot = super.createRenderRoot();
|
334
|
+
return this.#shadowRoot;
|
335
|
+
}
|
336
|
+
disconnectedCallback() {
|
337
|
+
super.disconnectedCallback();
|
338
|
+
this.form?.removeEventListener('formdata', this.#onFormdata);
|
339
|
+
document.removeEventListener('click', this.#onDocumentClick, {
|
340
|
+
capture: true,
|
341
|
+
});
|
342
|
+
}
|
343
|
+
// `async` so consumers can fetch and return a promise when they override.
|
344
|
+
//
|
345
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
346
|
+
async filter(query) {
|
347
|
+
return this.#optionElements.filter(({ label }) => {
|
348
|
+
return label?.toLowerCase().includes(query.toLowerCase().trim());
|
349
|
+
});
|
350
|
+
}
|
351
|
+
firstUpdated() {
|
352
|
+
if (this.#optionsAndFeedbackElementRef.value) {
|
353
|
+
// `popover` is used so the options can break out of Modal or another container
|
354
|
+
// that has `overflow: hidden`. Elements with `popover` are positioned relative
|
355
|
+
// to the viewport. Thus Floating UI in addition to `popover`.
|
356
|
+
//
|
357
|
+
// Set here instead of in the template to escape Lit Analyzer, which isn't aware
|
358
|
+
// of `popover` and doesn't have a way to disable its "no-unknown-attribute" rule.
|
359
|
+
//
|
360
|
+
// "auto" means only one popover can be open at a time. Consumers, however, may
|
361
|
+
// have popovers in their own components that need to be open while this one is
|
362
|
+
// open.
|
363
|
+
//
|
364
|
+
// "auto" also automatically opens the popover when its target is clicked. We want
|
365
|
+
// it to remain closed when clicked when there are no options. We also want it to
|
366
|
+
// close when every option has been filtered out.
|
367
|
+
this.#optionsAndFeedbackElementRef.value.popover = 'manual';
|
368
|
+
}
|
369
|
+
if (this.open && !this.disabled) {
|
370
|
+
this.#show();
|
371
|
+
}
|
372
|
+
this.selectedAndEnabledOptions = this.#optionElements.filter(({ selected, disabled }) => selected && !disabled);
|
373
|
+
if (!this.multiple &&
|
374
|
+
this.lastSelectedAndEnabledOption &&
|
375
|
+
this.#inputElementRef.value &&
|
376
|
+
this.lastSelectedAndEnabledOption.label) {
|
377
|
+
this.#inputElementRef.value.value =
|
378
|
+
this.lastSelectedAndEnabledOption.label;
|
379
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
380
|
+
}
|
381
|
+
const hasNoSelectedOptions = this.#optionElements.every(({ selected }) => !selected);
|
382
|
+
// When `value` is set on initial render, its setter is called before
|
383
|
+
// `connectedCallback()` and thus before the default slot has any assigned
|
384
|
+
// elements. So we select options here after the initial render is complete
|
385
|
+
// and `this.#optionElements` isn't empty.
|
386
|
+
//
|
387
|
+
// Additionally, `#onDefaultSlotChange()` is called after `firstUpdated()`
|
388
|
+
// and sets `value` based on which options are selected. And the initial `value`
|
389
|
+
// may conflict with the one derived from which options are selected.
|
390
|
+
//
|
391
|
+
// So we have a decision to make. On first render, do we defer to the initial
|
392
|
+
// `value` and select and deselect options below? Or do we defer to
|
393
|
+
// `#onDefaultSlotChange()` and let that method change `value` from its initial
|
394
|
+
// value based on which options are selected?
|
395
|
+
//
|
396
|
+
// It's largely a toss-up. But the latter seems like the logical choice given
|
397
|
+
// `#onDefaultSlotChange()` is called after `firstUpdated()`. In other words, we
|
398
|
+
// defer to the lifecycle. `#onDefaultSlotChange()` is called second. So it gets to
|
399
|
+
// override what `value` was initially.
|
400
|
+
//
|
401
|
+
// If no options are selected, then it's obvious that the consumer's intention is
|
402
|
+
// to select options based on the initial `value` and that the initial `value` is
|
403
|
+
// the intended one. So we proceed.
|
404
|
+
if (hasNoSelectedOptions) {
|
405
|
+
for (const optionValue of this.value) {
|
406
|
+
const option = this.#optionElements.find((option) => option.value === optionValue &&
|
407
|
+
// There may be more than one of the same value in `value`. If there is, we want
|
408
|
+
// more than one option to be selected. Without this condition, only the first
|
409
|
+
// option with the value would be selected.
|
410
|
+
!option.selected);
|
411
|
+
if (option) {
|
412
|
+
option.selected = true;
|
413
|
+
}
|
414
|
+
}
|
415
|
+
}
|
416
|
+
}
|
417
|
+
// The button doesn't receive focus when `shadowRoot.delegatesFocus` is set,
|
418
|
+
// and the inherited `this.focus` is called. It's not clear why. Thus the override.
|
419
|
+
focus(options) {
|
420
|
+
if (this.filterable || this.isFilterable) {
|
421
|
+
this.#inputElementRef.value?.focus(options);
|
422
|
+
}
|
423
|
+
else {
|
424
|
+
this.#primaryButtonElementRef.value?.focus(options);
|
425
|
+
}
|
426
|
+
}
|
427
|
+
get form() {
|
428
|
+
return this.#internals.form;
|
429
|
+
}
|
430
|
+
get validity() {
|
431
|
+
if (this.required && this.selectedAndEnabledOptions.length === 0) {
|
432
|
+
// A validation message is required but unused because we disable native validation
|
433
|
+
// feedback. And an empty string isn't allowed. Thus a single space.
|
434
|
+
this.#internals.setValidity({ customError: Boolean(this.validityMessage), valueMissing: true }, ' ', this.filterable || this.isFilterable
|
435
|
+
? this.#inputElementRef.value
|
436
|
+
: this.#primaryButtonElementRef.value);
|
437
|
+
return this.#internals.validity;
|
438
|
+
}
|
439
|
+
if (this.required &&
|
440
|
+
this.#internals.validity.valueMissing &&
|
441
|
+
this.selectedAndEnabledOptions.length > 0) {
|
442
|
+
this.#internals.setValidity({});
|
443
|
+
return this.#internals.validity;
|
444
|
+
}
|
445
|
+
return this.#internals.validity;
|
446
|
+
}
|
447
|
+
formAssociatedCallback() {
|
448
|
+
this.form?.addEventListener('formdata', this.#onFormdata);
|
449
|
+
}
|
450
|
+
formResetCallback() {
|
451
|
+
for (const option of this.#optionElements) {
|
452
|
+
// `#onOptionsSelectedChange()` is called when an option is selected. It updates
|
453
|
+
// `this.selectedAndEnabledOptions`. Deselecting every option before reselecting
|
454
|
+
// those in `value` ensures tags appear in the same order they were initially.
|
455
|
+
option.selected = false;
|
456
|
+
const isInitiallySelected = option.hasAttribute('selected') && !option.disabled;
|
457
|
+
if (isInitiallySelected) {
|
458
|
+
option.selected = true;
|
459
|
+
}
|
460
|
+
}
|
461
|
+
}
|
462
|
+
render() {
|
463
|
+
// `hidden` is frowned upon because it adds a second source of truth for styling.
|
464
|
+
// However, it's the simplest way to hide slotted options and to later check if
|
465
|
+
// they're hidden. Select All is different because we can conditionally render it
|
466
|
+
// and check if it exists. `hidden` is used with it nonetheless for consistency and
|
467
|
+
// to simplify the logic that checks an option's visibility, such as in
|
468
|
+
// `#optionElementsNotHiddenIncludingSelectAll`.
|
469
|
+
// ".tag-overflow-text" is hidden from screen readers because it's redundant. The
|
470
|
+
// selected options are announced when Dropdown receives focus.
|
471
|
+
// The linter checks that all ULs have LIs as children. It doesn't account for
|
472
|
+
// slots, which can contain LIs.
|
473
|
+
// The linter wants a "focus" handler on the slot, but there's nothing to be done
|
474
|
+
// with one in this case.
|
475
|
+
// The linter wants a "keydown" handler on '.dropdown'. Instead, there's one on
|
476
|
+
// `.dropdown-and-options` because much of the logic in the handler also applies
|
477
|
+
// to options, which can receive focus, and "keydown" events won't be emitted on
|
478
|
+
// ".dropdown" when it doesn't have focus.
|
479
|
+
// 'aria-selected="false"' on the Add button because every `"role="option"` must
|
480
|
+
// have that attribute. The attribute's value is hardcoded to "false" because the
|
481
|
+
// Add button can't be selected in the usual, persistent sense.
|
482
|
+
//
|
483
|
+
// Admittedly, it's a hack with respect to accessibility. But screenreader users
|
484
|
+
// should be able to understand what's going on.
|
485
|
+
//
|
486
|
+
// The alternative, an Add button that's not an option but instead tabbed to, would
|
487
|
+
// certainly be a worse user experience in general because users would have to tab
|
488
|
+
// out of the input field while filtering to add a new option. Then they would have
|
489
|
+
// to tab back to the input field to continue filtering. This would also likely be
|
490
|
+
// worse for screenreader users because they wouldn't discover the Add button until
|
491
|
+
// they move focus off the input field.
|
492
|
+
/* eslint-disable lit-a11y/mouse-events-have-key-events, lit-a11y/click-events-have-key-events */
|
493
|
+
return html `<div
|
494
|
+
class=${classMap({
|
495
|
+
component: true,
|
496
|
+
horizontal: this.orientation === 'horizontal',
|
497
|
+
vertical: this.orientation === 'vertical',
|
498
|
+
})}
|
499
|
+
@mouseup=${this.#onComponentMouseup}
|
500
|
+
${onResize(this.#setTagOverflowLimit.bind(this))}
|
501
|
+
${ref(this.#componentElementRef)}
|
6
502
|
>
|
7
503
|
<glide-core-private-label
|
8
504
|
label=${ifDefined(this.label)}
|
9
505
|
orientation=${this.orientation}
|
10
|
-
split=${ifDefined(this.privateSplit??
|
506
|
+
split=${ifDefined(this.privateSplit ?? undefined)}
|
11
507
|
tooltip=${ifDefined(this.tooltip)}
|
12
508
|
?disabled=${this.disabled}
|
13
|
-
?error=${this.#
|
509
|
+
?error=${this.#isShowValidationFeedback}
|
14
510
|
?hide=${this.hideLabel}
|
15
511
|
?required=${this.required}
|
16
512
|
>
|
@@ -19,45 +515,66 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
19
515
|
<div
|
20
516
|
class="dropdown-and-options"
|
21
517
|
slot="control"
|
22
|
-
@focusout=${this.#
|
23
|
-
@keydown=${this.#
|
518
|
+
@focusout=${this.#onDropdownAndOptionsFocusout}
|
519
|
+
@keydown=${this.#onDropdownAndOptionsKeydown}
|
24
520
|
>
|
25
521
|
<div
|
26
|
-
class=${classMap({
|
27
|
-
|
28
|
-
|
29
|
-
|
522
|
+
class=${classMap({
|
523
|
+
dropdown: true,
|
524
|
+
quiet: this.variant === 'quiet',
|
525
|
+
disabled: this.disabled,
|
526
|
+
error: this.#isShowValidationFeedback,
|
527
|
+
readonly: this.readonly,
|
528
|
+
multiple: this.multiple,
|
529
|
+
})}
|
530
|
+
@click=${this.#onDropdownClick}
|
531
|
+
@mousedown=${this.#onDropdownMousedown}
|
532
|
+
${ref(this.#dropdownElementRef)}
|
30
533
|
>
|
31
534
|
<span class="selected-option-labels" id="selected-option-labels">
|
32
|
-
${this.selectedAndEnabledOptions
|
33
|
-
|
34
|
-
|
535
|
+
${this.selectedAndEnabledOptions
|
536
|
+
.filter((option) => {
|
537
|
+
return this.multiple
|
538
|
+
? true
|
539
|
+
: option === this.selectedAndEnabledOptions.at(-1);
|
540
|
+
})
|
541
|
+
.map(({ label }) => {
|
542
|
+
return html `<span data-test="selected-option-label">
|
543
|
+
${label},
|
544
|
+
</span>`;
|
545
|
+
})}
|
35
546
|
</span>
|
36
547
|
|
37
|
-
${when(this.multiple&&this.selectedAndEnabledOptions.length>0,(
|
548
|
+
${when(this.multiple && this.selectedAndEnabledOptions.length > 0, () => {
|
549
|
+
return html `<ul
|
38
550
|
aria-describedby="tag-overflow-text"
|
39
551
|
class="tags"
|
40
|
-
${ref(this.#
|
552
|
+
${ref(this.#tagsElementRef)}
|
41
553
|
>
|
42
|
-
${repeat(this.selectedAndEnabledOptions,(
|
43
|
-
|
554
|
+
${repeat(this.selectedAndEnabledOptions, ({ id }) => id, (option, index) => {
|
555
|
+
return html `<li
|
556
|
+
class=${classMap({
|
557
|
+
'tag-container': true,
|
558
|
+
hidden: index > this.tagOverflowLimit - 1,
|
559
|
+
})}
|
44
560
|
data-test="tag-container"
|
45
|
-
data-test-hidden=${
|
561
|
+
data-test-hidden=${index > this.tagOverflowLimit - 1}
|
46
562
|
>
|
47
563
|
<glide-core-tag
|
48
564
|
data-test="tag"
|
49
|
-
data-id=${
|
50
|
-
label=${ifDefined(
|
565
|
+
data-id=${option.id}
|
566
|
+
label=${ifDefined(option.label)}
|
51
567
|
removable
|
52
|
-
?disabled=${this.disabled||this.readonly}
|
53
|
-
?private-editable=${
|
54
|
-
@edit=${this.#
|
55
|
-
@remove=${this.#
|
568
|
+
?disabled=${this.disabled || this.readonly}
|
569
|
+
?private-editable=${option.editable}
|
570
|
+
@edit=${this.#onTagEdit}
|
571
|
+
@remove=${this.#onTagRemove.bind(this, option)}
|
56
572
|
>
|
57
|
-
${when(
|
573
|
+
${when(option.value, () => {
|
574
|
+
return html `
|
58
575
|
<slot
|
59
576
|
data-test="multiselect-icon-slot"
|
60
|
-
name="icon:${
|
577
|
+
name="icon:${option.value}"
|
61
578
|
slot="icon"
|
62
579
|
>
|
63
580
|
<!--
|
@@ -69,24 +586,36 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
69
586
|
@type {Element}
|
70
587
|
-->
|
71
588
|
</slot>
|
72
|
-
|
589
|
+
`;
|
590
|
+
})}
|
73
591
|
</glide-core-tag>
|
74
|
-
</li
|
75
|
-
|
76
|
-
|
592
|
+
</li>`;
|
593
|
+
})}
|
594
|
+
</ul>`;
|
595
|
+
})}
|
596
|
+
${when(this.multiple &&
|
597
|
+
this.selectedAndEnabledOptions.length > this.tagOverflowLimit, () => {
|
598
|
+
return html `<div
|
77
599
|
aria-hidden="true"
|
78
600
|
class="tag-overflow-text"
|
79
601
|
id="tag-overflow-text"
|
80
602
|
>
|
81
603
|
+
|
82
604
|
<span data-test="tag-overflow-count">
|
83
|
-
${this.selectedAndEnabledOptions.length-
|
605
|
+
${this.selectedAndEnabledOptions.length -
|
606
|
+
this.tagOverflowLimit}
|
84
607
|
</span>
|
85
608
|
|
86
609
|
more
|
87
|
-
</div
|
88
|
-
|
89
|
-
|
610
|
+
</div>`;
|
611
|
+
})}
|
612
|
+
${when(this.isShowSingleSelectIcon &&
|
613
|
+
this.lastSelectedAndEnabledOption?.value, () => {
|
614
|
+
return html `<slot
|
615
|
+
class=${classMap({
|
616
|
+
'single-select-icon-slot': true,
|
617
|
+
quiet: this.variant === 'quiet',
|
618
|
+
})}
|
90
619
|
data-test="single-select-icon-slot"
|
91
620
|
name="icon:${this.lastSelectedAndEnabledOption?.value}"
|
92
621
|
>
|
@@ -94,16 +623,20 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
94
623
|
@type {Element}
|
95
624
|
@ignore
|
96
625
|
-->
|
97
|
-
</slot
|
626
|
+
</slot>`;
|
627
|
+
})}
|
98
628
|
|
99
629
|
<glide-core-tooltip
|
100
|
-
class=${classMap({
|
630
|
+
class=${classMap({
|
631
|
+
'input-tooltip': true,
|
632
|
+
visible: this.filterable || this.isFilterable,
|
633
|
+
})}
|
101
634
|
data-test="input-tooltip"
|
102
635
|
label=${this.inputValue}
|
103
636
|
offset=${8}
|
104
|
-
?disabled=${this.open
|
105
|
-
?open=${!this.open&&this.isInputTooltipOpen}
|
106
|
-
@toggle=${this.#
|
637
|
+
?disabled=${this.open || !this.isInputOverflowing}
|
638
|
+
?open=${!this.open && this.isInputTooltipOpen}
|
639
|
+
@toggle=${this.#onTooltipToggle}
|
107
640
|
screenreader-hidden
|
108
641
|
>
|
109
642
|
<div class="input-container" slot="target">
|
@@ -111,33 +644,47 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
111
644
|
aria-activedescendant=${this.ariaActivedescendant}
|
112
645
|
aria-controls="options"
|
113
646
|
aria-describedby="description"
|
114
|
-
aria-expanded=${this.open
|
115
|
-
aria-labelledby="selected-option-labels label loading-feedback ${this
|
647
|
+
aria-expanded=${this.open && !this.disabled}
|
648
|
+
aria-labelledby="selected-option-labels label loading-feedback ${this
|
649
|
+
.isCommunicateItemCountToScreenreaders
|
650
|
+
? 'item-count'
|
651
|
+
: ''}"
|
116
652
|
autocapitalize="off"
|
117
653
|
autocomplete="off"
|
118
|
-
class=${classMap({
|
654
|
+
class=${classMap({
|
655
|
+
input: true,
|
656
|
+
quiet: this.variant === 'quiet',
|
657
|
+
})}
|
119
658
|
data-test="input"
|
120
659
|
id="input"
|
121
|
-
placeholder=${this.multiple
|
660
|
+
placeholder=${this.multiple ||
|
661
|
+
!this.lastSelectedAndEnabledOption?.label
|
662
|
+
? (this.placeholder ?? '')
|
663
|
+
: ''}
|
122
664
|
role="combobox"
|
123
665
|
spellcheck="false"
|
124
|
-
tabindex=${this.disabled?
|
666
|
+
tabindex=${this.disabled ? '-1' : '0'}
|
125
667
|
?disabled=${this.disabled}
|
126
668
|
?readonly=${this.readonly}
|
127
|
-
@blur=${this.#
|
128
|
-
@focus=${this.#
|
129
|
-
@input=${this.#
|
130
|
-
@keydown=${this.#
|
131
|
-
${onResize(this.#
|
132
|
-
${ref(this.#
|
669
|
+
@blur=${this.#onInputBlur}
|
670
|
+
@focus=${this.#onInputFocus}
|
671
|
+
@input=${this.#onInputInput}
|
672
|
+
@keydown=${this.#onInputKeydown}
|
673
|
+
${onResize(this.#onInputResize.bind(this))}
|
674
|
+
${ref(this.#inputElementRef)}
|
133
675
|
/>
|
134
676
|
|
135
|
-
${when(!this.multiple&&
|
677
|
+
${when(!this.multiple &&
|
678
|
+
this.isInputOverflowing &&
|
679
|
+
this.inputValue ===
|
680
|
+
this.lastSelectedAndEnabledOption?.label, () => {
|
681
|
+
return html `<span aria-hidden="true" data-test="ellipsis">
|
136
682
|
…
|
137
|
-
</span
|
683
|
+
</span>`;
|
684
|
+
})}
|
138
685
|
|
139
686
|
<span
|
140
|
-
aria-label=${this.#
|
687
|
+
aria-label=${this.#localize.term('itemCount', this.itemCount.toString())}
|
141
688
|
aria-live="assertive"
|
142
689
|
class="item-count"
|
143
690
|
data-test="item-count"
|
@@ -148,105 +695,148 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
148
695
|
</glide-core-tooltip>
|
149
696
|
|
150
697
|
<glide-core-tooltip
|
151
|
-
class=${classMap({
|
698
|
+
class=${classMap({
|
699
|
+
'internal-label-tooltip': true,
|
700
|
+
visible: Boolean(this.internalLabel),
|
701
|
+
})}
|
152
702
|
data-test="internal-label-tooltip"
|
153
|
-
label=${this.internalLabel??
|
703
|
+
label=${this.internalLabel ?? ''}
|
154
704
|
offset=${8}
|
155
|
-
?disabled=${this.open||
|
156
|
-
|
157
|
-
|
705
|
+
?disabled=${this.open ||
|
706
|
+
this.multiple ||
|
707
|
+
this.filterable ||
|
708
|
+
this.isFilterable ||
|
709
|
+
!this.isInternalLabelOverflowing}
|
710
|
+
?open=${!this.open && this.isInternalLabelTooltipOpen}
|
711
|
+
@toggle=${this.#onTooltipToggle}
|
158
712
|
screenreader-hidden
|
159
713
|
>
|
160
714
|
<div
|
161
715
|
class="internal-label"
|
162
716
|
data-test="internal-label"
|
163
717
|
slot="target"
|
164
|
-
${onResize(this.#
|
165
|
-
${ref(this.#
|
718
|
+
${onResize(this.#onInternalLabelResize.bind(this))}
|
719
|
+
${ref(this.#internalLabelElementRef)}
|
166
720
|
>
|
167
|
-
${when(this.internalLabel===this.placeholder,(
|
168
|
-
|
721
|
+
${when(this.internalLabel === this.placeholder, () => {
|
722
|
+
return html `<span
|
723
|
+
class=${classMap({
|
724
|
+
placeholder: true,
|
725
|
+
quiet: this.variant === 'quiet',
|
726
|
+
})}
|
169
727
|
>
|
170
728
|
${this.internalLabel}
|
171
|
-
</span
|
729
|
+
</span>`;
|
730
|
+
}, () => this.internalLabel)}
|
172
731
|
</div>
|
173
732
|
</glide-core-tooltip>
|
174
733
|
|
175
734
|
<div class="buttons">
|
176
|
-
${when(!this.multiple&&
|
735
|
+
${when(!this.multiple &&
|
736
|
+
this.lastSelectedAndEnabledOption?.editable &&
|
737
|
+
!this.isFiltering, () => {
|
738
|
+
return html `<glide-core-icon-button
|
177
739
|
class="edit-button"
|
178
740
|
data-test="edit-button"
|
179
|
-
label=${this.#
|
180
|
-
|
741
|
+
label=${this.#localize.term('editOption',
|
742
|
+
// `this.lastSelectedAndEnabledOption` is guaranteed to be defined by the
|
743
|
+
// `when()` above. And its `label` property is always defined because it's
|
744
|
+
// required.
|
745
|
+
//
|
746
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
747
|
+
this.lastSelectedAndEnabledOption.label)}
|
748
|
+
tabindex=${this.disabled || this.readonly ? '-1' : '0'}
|
181
749
|
variant="tertiary"
|
182
|
-
?disabled=${this.disabled||this.readonly}
|
183
|
-
@click=${this.#
|
184
|
-
${ref(this.#
|
750
|
+
?disabled=${this.disabled || this.readonly}
|
751
|
+
@click=${this.#onEditButtonClick}
|
752
|
+
${ref(this.#editButtonElementRef)}
|
185
753
|
>
|
186
754
|
${pencilIcon}
|
187
|
-
</glide-core-icon-button
|
755
|
+
</glide-core-icon-button>`;
|
756
|
+
})}
|
188
757
|
|
189
758
|
<button
|
190
759
|
aria-controls="options"
|
191
760
|
aria-describedby="description"
|
192
|
-
aria-expanded=${this.open
|
761
|
+
aria-expanded=${this.open && !this.disabled}
|
193
762
|
aria-haspopup="listbox"
|
194
|
-
aria-hidden=${this.filterable||this.isFilterable}
|
763
|
+
aria-hidden=${this.filterable || this.isFilterable}
|
195
764
|
aria-labelledby="selected-option-labels label loading-feedback"
|
196
765
|
class="primary-button"
|
197
766
|
data-test="primary-button"
|
198
767
|
id="primary-button"
|
199
|
-
tabindex=${this.filterable||this.isFilterable||this.disabled
|
768
|
+
tabindex=${this.filterable || this.isFilterable || this.disabled
|
769
|
+
? '-1'
|
770
|
+
: '0'}
|
200
771
|
type="button"
|
201
772
|
?disabled=${this.disabled}
|
202
|
-
@focusin=${this.#
|
203
|
-
@focusout=${this.#
|
204
|
-
${ref(this.#
|
773
|
+
@focusin=${this.#onPrimaryButtonFocusin}
|
774
|
+
@focusout=${this.#onPrimaryButtonFocusout}
|
775
|
+
${ref(this.#primaryButtonElementRef)}
|
205
776
|
>
|
206
|
-
${when(this.isFiltering,(
|
777
|
+
${when(this.isFiltering, () => {
|
778
|
+
return html `<div data-test="magnifying-glass-icon">
|
207
779
|
${magnifyingGlassIcon}
|
208
|
-
</div
|
780
|
+
</div>`;
|
781
|
+
}, () => chevronIcon)}
|
209
782
|
</button>
|
210
783
|
</div>
|
211
784
|
</div>
|
212
785
|
|
213
786
|
<div
|
214
|
-
aria-labelledby=${this.filterable||this.isFilterable
|
215
|
-
|
787
|
+
aria-labelledby=${this.filterable || this.isFilterable
|
788
|
+
? 'input'
|
789
|
+
: 'primary-button'}
|
790
|
+
class=${classMap({
|
791
|
+
'options-and-feedback': true,
|
792
|
+
optionless: (this.hasNoAvailableOptions || this.hasNoMatchingOptions) &&
|
793
|
+
!this.loading &&
|
794
|
+
!this.isAddButtonVisible,
|
795
|
+
})}
|
216
796
|
role="listbox"
|
217
797
|
tabindex="-1"
|
218
|
-
${ref(this.#
|
798
|
+
${ref(this.#optionsAndFeedbackElementRef)}
|
219
799
|
>
|
220
800
|
<div
|
221
|
-
class=${classMap({
|
801
|
+
class=${classMap({
|
802
|
+
options: true,
|
803
|
+
hidden: !this.isAddButtonVisible &&
|
804
|
+
(this.hasNoAvailableOptions ||
|
805
|
+
this.hasNoMatchingOptions ||
|
806
|
+
this.loading),
|
807
|
+
})}
|
222
808
|
data-test="options"
|
223
809
|
id="options"
|
224
|
-
@change=${this.#
|
225
|
-
@click=${this.#
|
226
|
-
@focusin=${this.#
|
227
|
-
@mousedown=${this.#
|
228
|
-
@mouseover=${this.#
|
229
|
-
@private-disabled-change=${this.#
|
230
|
-
@private-editable-change=${this.#
|
231
|
-
@private-label-change=${this.#
|
232
|
-
@private-value-change=${this.#
|
810
|
+
@change=${this.#onOptionsChange}
|
811
|
+
@click=${this.#onOptionsClick}
|
812
|
+
@focusin=${this.#onOptionsFocusin}
|
813
|
+
@mousedown=${this.#onOptionsMousedown}
|
814
|
+
@mouseover=${this.#onOptionsMouseover}
|
815
|
+
@private-disabled-change=${this.#onOptionsDisabledChange}
|
816
|
+
@private-editable-change=${this.#onOptionsEditableChange}
|
817
|
+
@private-label-change=${this.#onOptionsLabelChange}
|
818
|
+
@private-value-change=${this.#onOptionsValueChange}
|
233
819
|
>
|
234
820
|
<glide-core-dropdown-option
|
235
821
|
class="select-all"
|
236
822
|
data-test="select-all"
|
237
|
-
label=${this.#
|
823
|
+
label=${this.#localize.term('selectAll')}
|
238
824
|
private-multiple
|
239
|
-
?hidden=${!this.selectAll
|
240
|
-
?private-indeterminate=${this.areSomeOptionsSelected
|
241
|
-
|
825
|
+
?hidden=${!this.selectAll || !this.multiple || this.isFiltering}
|
826
|
+
?private-indeterminate=${this.areSomeOptionsSelected &&
|
827
|
+
!this.areAllOptionsSelected}
|
828
|
+
${ref(this.#selectAllElementRef)}
|
242
829
|
></glide-core-dropdown-option>
|
243
830
|
|
244
831
|
<slot
|
245
|
-
class=${classMap({
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
832
|
+
class=${classMap({
|
833
|
+
'default-slot': true,
|
834
|
+
optionless: this.hasNoMatchingOptions,
|
835
|
+
})}
|
836
|
+
@private-selected-change=${this.#onOptionsSelectedChange}
|
837
|
+
@slotchange=${this.#onDefaultSlotChange}
|
838
|
+
${assertSlot([DropdownOption, Text], true)}
|
839
|
+
${ref(this.#defaultSlotElementRef)}
|
250
840
|
>
|
251
841
|
<!--
|
252
842
|
@required
|
@@ -255,22 +845,29 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
255
845
|
</slot>
|
256
846
|
</div>
|
257
847
|
|
258
|
-
${when(this.isAddButtonVisible,(
|
848
|
+
${when(this.isAddButtonVisible, () => {
|
849
|
+
return html `
|
259
850
|
<div
|
260
|
-
class=${classMap({
|
851
|
+
class=${classMap({
|
852
|
+
'add-button-container': true,
|
853
|
+
bordered: !this.hasNoAvailableOptions && !this.hasNoMatchingOptions,
|
854
|
+
})}
|
261
855
|
>
|
262
856
|
<button
|
263
857
|
aria-selected="false"
|
264
|
-
class=${classMap({
|
858
|
+
class=${classMap({
|
859
|
+
'add-button': true,
|
860
|
+
active: this.isAddButtonActive,
|
861
|
+
})}
|
265
862
|
data-test="add-button"
|
266
863
|
data-test-active=${this.isAddButtonActive}
|
267
|
-
id=${this.#
|
864
|
+
id=${this.#addButtonId}
|
268
865
|
role="option"
|
269
866
|
tabindex="-1"
|
270
867
|
type="button"
|
271
|
-
@click=${this.#
|
272
|
-
@mouseover=${this.#
|
273
|
-
${ref(this.#
|
868
|
+
@click=${this.#selectAddButton}
|
869
|
+
@mouseover=${this.#onAddButtonMouseover}
|
870
|
+
${ref(this.#addButtonElementRef)}
|
274
871
|
>
|
275
872
|
<div class="add-button-label">
|
276
873
|
${this.inputValue.trim()}
|
@@ -279,28 +876,40 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
279
876
|
|
280
877
|
|
281
878
|
<div class="add-button-description">
|
282
|
-
(${this.#
|
879
|
+
(${this.#localize.term('add')})
|
283
880
|
</div>
|
284
881
|
</button>
|
285
882
|
</div>
|
286
|
-
|
287
|
-
|
288
|
-
|
883
|
+
`;
|
884
|
+
})}
|
885
|
+
${when(this.loading, () => {
|
886
|
+
return html `<div
|
887
|
+
aria-label=${this.#localize.term('loading')}
|
289
888
|
class="loading-feedback"
|
290
889
|
data-test="loading-feedback"
|
291
890
|
id="loading-feedback"
|
292
891
|
>
|
293
|
-
${map(range(7),(
|
294
|
-
</div
|
295
|
-
|
296
|
-
|
297
|
-
|
892
|
+
${map(range(7), () => html `<div></div>`)}
|
893
|
+
</div>`;
|
894
|
+
})}
|
895
|
+
${when((this.hasNoAvailableOptions || this.hasNoMatchingOptions) &&
|
896
|
+
!this.loading &&
|
897
|
+
!this.isAddButtonVisible, () => {
|
898
|
+
return html `<div data-test="optionless-feedback">
|
899
|
+
${this.hasNoAvailableOptions
|
900
|
+
? this.#localize.term('noAvailableOptions')
|
901
|
+
: this.#localize.term('noMatchingOptions')}
|
902
|
+
</div>`;
|
903
|
+
})}
|
298
904
|
</div>
|
299
905
|
</div>
|
300
906
|
|
301
907
|
<div id="description" slot="description">
|
302
908
|
<slot
|
303
|
-
class=${classMap({
|
909
|
+
class=${classMap({
|
910
|
+
description: true,
|
911
|
+
hidden: Boolean(this.#isShowValidationFeedback && this.validityMessage),
|
912
|
+
})}
|
304
913
|
name="description"
|
305
914
|
>
|
306
915
|
<!--
|
@@ -309,9 +918,1700 @@ var __decorate=this&&this.__decorate||function(t,e,i,s){var n,l=arguments.length
|
|
309
918
|
-->
|
310
919
|
</slot>
|
311
920
|
|
312
|
-
${when(this.#
|
921
|
+
${when(this.#isShowValidationFeedback && this.validityMessage, () => html `<span class="validity-message" data-test="validity-message"
|
313
922
|
>${unsafeHTML(this.validityMessage)}</span
|
314
|
-
>`)
|
923
|
+
>`)}
|
315
924
|
</div>
|
316
925
|
</glide-core-private-label>
|
317
|
-
</div>`}reportValidity(){this.isReportValidityOrSubmit=!0;const t=this.#c.reportValidity();return this.requestUpdate(),t}resetValidityFeedback(){this.isReportValidityOrSubmit=!1}setCustomValidity(t){this.validityMessage=t,""===t?this.#c.setValidity({customError:!1},"",this.filterable||this.isFilterable?this.#n.value:this.#u.value):this.#c.setValidity({customError:!0,valueMissing:this.#c.validity.valueMissing}," ",this.filterable||this.isFilterable?this.#n.value:this.#u.value)}setValidity(t,e){this.validityMessage=e,this.#c.setValidity(t," ",this.filterable||this.isFilterable?this.#n.value:this.#u.value)}constructor(){super(),this.addButton=!1,this.hideLabel=!1,this.loading=!1,this.name="",this.orientation="horizontal",this.readonly=!1,this.selectAll=!1,this.required=!1,this.version=packageJson.version,this.ariaActivedescendant="",this.hasNoAvailableOptions=!1,this.hasNoMatchingOptions=!1,this.inputValue="",this.isAddButtonActive=!1,this.isAddButtonVisible=!1,this.isBlurring=!1,this.isCheckingValidity=!1,this.isCommunicateItemCountToScreenreaders=!1,this.isFilterable=!1,this.isFiltering=!1,this.isInputOverflowing=!1,this.isInputTooltipOpen=!1,this.isInternalLabelOverflowing=!1,this.isInternalLabelTooltipOpen=!1,this.isReportValidityOrSubmit=!1,this.isShowSingleSelectIcon=!1,this.itemCount=0,this.selectedAndEnabledOptions=[],this.tagOverflowLimit=0,this.#lt=createRef(),this.#it=uniqueId(),this.#O=createRef(),this.#et=createRef(),this.#$=createRef(),this.#j=createRef(),this.#n=createRef(),this.#N=createRef(),this.#ot=!1,this.#t=!1,this.#at=!1,this.#s=!1,this.#dt=!0,this.#a=!1,this.#o=!1,this.#rt=!1,this.#pt=!1,this.#p=!1,this.#ht=!1,this.#L=new LocalizeController(this),this.#b=createRef(),this.#u=createRef(),this.#Y=createRef(),this.#I=createRef(),this.#r=[],this.#v=()=>{this.#ot?setTimeout((()=>{this.#ot=!1})):this.open=!1},this.#f=({formData:t})=>{this.name&&this.value.length>0&&!this.disabled&&t.append(this.name,JSON.stringify(this.value))},this.#c=this.attachInternals(),this.addEventListener("invalid",(t=>{if(t.preventDefault(),this.isCheckingValidity||this.isBlurring)return;this.isReportValidityOrSubmit=!0;this.form?.querySelector(":invalid")===this&&this.focus()}))}#lt;#it;#ct;#O;#et;#$;#j;#n;#N;#c;#ot;#t;#at;#s;#dt;#a;#o;#rt;#pt;#p;#ht;#L;#b;#ut;#u;#Y;#m;#I;#r;#v;#f;#e(){this.#ct?.(),this.#b.value?.hidePopover(),this.ariaActivedescendant="",this.activeOption&&(this.#ut=this.activeOption,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1)}get#A(){return!this.disabled&&!this.validity.valid&&this.isReportValidityOrSubmit}#nt(){this.isAddButtonActive=!0,this.#lt.value&&(this.ariaActivedescendant=this.#lt.value.id),this.activeOption&&(this.#ut=this.activeOption,this.activeOption.privateActive=!1)}#E(){this.#ot=!0}async#tt(){if(this.open){const t=this.#vt?.find((t=>!t.disabled));this.activeOption&&!this.activeOption?.disabled||!t||(this.#ut=t,this.ariaActivedescendant=t.id,t.privateActive=!0)}this.#Y.value&&(this.#Y.value.selected=this.areAllOptionsSelected),this.hasNoAvailableOptions=0===this.#d.length,this.selectedAndEnabledOptions=this.#d.filter((t=>t.selected&&!t.disabled)),this.#dt&&(this.isFilterable=this.#d.length>10,this.#dt=!1),this.multiple?(this.#r=this.selectedAndEnabledOptions.filter((({value:t})=>Boolean(t))).map((({value:t})=>t)),this.tagOverflowLimit=this.selectedAndEnabledOptions.length,this.#g()):(this.lastSelectedAndEnabledOption?.value&&(this.#r=[this.lastSelectedAndEnabledOption.value]),this.isShowSingleSelectIcon=Boolean(this.lastSelectedAndEnabledOption?.value),await this.updateComplete,this.#n.value&&this.lastSelectedAndEnabledOption?.label?(this.#n.value.value=this.lastSelectedAndEnabledOption.label,this.inputValue=this.lastSelectedAndEnabledOption.label,this.isInputOverflowing=this.#n.value.scrollWidth>this.#n.value.clientWidth):this.#n.value&&!this.isFiltering&&(this.#n.value.value="",this.inputValue="",this.isAddButtonVisible=!1,this.isInputOverflowing=this.#n.value.scrollWidth>this.#n.value.clientWidth));for(const t of this.#d)t.privateMultiple=this.multiple,!this.multiple&&t.selected&&t.requestUpdate()}#y(t){(null===t.relatedTarget||t.relatedTarget instanceof Node&&!this.#m?.contains(t.relatedTarget)&&!this.contains(t.relatedTarget))&&!this.#at&&(this.open=!1,this.isBlurring=!0,this.reportValidity(),this.isBlurring=!1)}#w(t){if(this.disabled||this.readonly)return;if(("Enter"===t.key||" "===t.key)&&t.target===this.#j.value)return void(this.#ot=!0);if(!this.open&&"Enter"===t.key&&!this.#at)return void this.form?.requestSubmit();if("Escape"===t.key)return t.preventDefault(),void(this.open=!1);const e=t.target===this.#u.value||t.target===this.#n.value||t.target instanceof DropdownOption;if(!this.multiple||e){if(!this.open&&[" ","ArrowUp","ArrowDown"].includes(t.key))return t.preventDefault(),this.open=!0,void(this.activeOption&&(this.activeOption.privateIsTooltipOpen=!this.activeOption.privateIsEditActive));if(this.isAddButtonActive&&this.open){if("ArrowUp"===t.key&&t.metaKey||["Home","PageUp"].includes(t.key)){t.preventDefault();const e=this.#mt?.at(0);e&&(e.privateActive=!0,e.privateIsTooltipOpen=!e.editable,this.isAddButtonActive=!1,this.ariaActivedescendant=e.id,e.scrollIntoView())}if("ArrowUp"===t.key){t.preventDefault();const e=this.#ut&&!this.#ut.hidden?this.#ut:this.#mt?.at(-1);e&&(e.privateActive=!0,e.privateIsEditActive=e.editable,e.privateIsTooltipOpen=!e.editable,this.isAddButtonActive=!1,this.ariaActivedescendant=e.id)}"Enter"===t.key&&this.#st()}else if(this.activeOption&&this.open){if("Enter"===t.key||" "===t.key){if(this.activeOption.privateIsEditActive)return this.activeOption.privateEdit(),void(this.open=!1);if("Enter"===t.key&&this.#mt&&this.#mt.length>0||" "===t.key&&!this.filterable&&!this.isFilterable)return this.#ht=!0,t.preventDefault(),this.activeOption.selected=!this.multiple||!this.activeOption.selected,this.activeOption===this.#Y.value&&this.#ft(),this.#ht=!1,this.#l(),this.multiple?(this.#n.value&&(this.#n.value.value=""),this.inputValue=""):(this.#n.value&&void 0!==this.activeOption.label&&(this.#n.value.value=this.activeOption.label,this.inputValue=this.activeOption.label,this.isInputOverflowing=this.#n.value.scrollWidth-1>this.#n.value.clientWidth),this.open=!1,this.isInputTooltipOpen=!1),this.dispatchEvent(new Event("input",{bubbles:!0,composed:!0})),void this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))}const e=this.#vt?.indexOf(this.activeOption);if("ArrowUp"===t.key&&!t.metaKey&&this.#vt&&"number"==typeof e){t.preventDefault();const i=this.#vt.findLast(((t,i)=>!t.disabled&&i<e));return void(this.activeOption?.privateIsEditActive?(this.activeOption.privateIsEditActive=!1,this.activeOption.privateIsTooltipOpen=!0):i&&(this.#ut=this.activeOption,this.activeOption.privateIsEditActive=!1,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1,this.ariaActivedescendant=i.id,i.privateActive=!0,i.privateIsEditActive=i.editable,i.privateIsTooltipOpen=!i.editable,i.scrollIntoView({block:"center"})))}if("ArrowDown"===t.key&&!t.metaKey&&this.#vt&&"number"==typeof e){t.preventDefault();const i=this.#vt.find(((t,i)=>!t.disabled&&i>e));return void(this.activeOption.editable&&!this.activeOption.privateIsEditActive?(this.activeOption.privateIsEditActive=!0,this.activeOption.privateIsTooltipOpen=!1):i?(this.#ut=this.activeOption,this.activeOption.privateIsEditActive=!1,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1,this.ariaActivedescendant=i.id,i.privateActive=!0,i.privateIsTooltipOpen=!0,i.scrollIntoView({block:"center"})):this.isAddButtonVisible&&this.#lt.value&&(this.#ut=this.activeOption,this.activeOption.privateIsEditActive=!1,this.activeOption.privateActive=!1,this.isAddButtonActive=!0,this.ariaActivedescendant=this.#lt.value.id))}if(("ArrowUp"===t.key&&t.metaKey||"Home"===t.key||"PageUp"===t.key)&&this.#vt){t.preventDefault();const e=[...this.#vt].reverse().findLast((t=>!t.disabled));return void(e&&(this.#ut=this.activeOption,this.activeOption.privateIsEditActive=!1,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1,this.ariaActivedescendant=e.id,e.privateActive=!0,e.privateIsTooltipOpen=!0,e.scrollIntoView()))}if(("ArrowDown"===t.key&&t.metaKey||"End"===t.key||"PageDown"===t.key)&&this.#vt){t.preventDefault();const e=[...this.#vt].findLast((t=>!t.disabled));return void(this.isAddButtonVisible&&this.#lt.value?(this.#ut=this.activeOption,this.activeOption.privateIsEditActive=!1,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1,this.isAddButtonActive=!0,this.ariaActivedescendant=this.#lt.value.id):e&&this.activeOption&&(this.#ut=this.activeOption,this.activeOption.privateIsEditActive=!1,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1,this.ariaActivedescendant=e.id,e.privateActive=!0,e.privateIsTooltipOpen=!0,e.scrollIntoView()))}}}}#R(t){if(this.disabled||this.readonly)return;if(this.#at)return void(this.#at=!1);if(t.target instanceof Node&&this.#j.value?.contains(t.target))return void this.lastSelectedAndEnabledOption?.privateEdit();const e=this.filterable||this.isFilterable;if(this.#ht||!this.open||e&&!(t.target instanceof Element&&this.#u.value?.contains(t.target)))return 0!==t.detail?(this.open=!0,void this.focus()):void 0;this.open=!1}#S(t){const e=this.filterable||this.isFilterable,i=t.target instanceof Tag;e&&!i?t.target!==this.#n.value&&(t.preventDefault(),this.focus()):i||t.preventDefault()}#q(){this.open=!1}#F(){this.isCommunicateItemCountToScreenreaders=!1,this.isInputTooltipOpen=!1}#V(){this.#n.value?.select(),this.isInputTooltipOpen=!0}async#_(t){let e;if(t.stopPropagation(),this.open=!0,this.#n.value&&(this.inputValue=this.#n.value.value),this.multiple&&""!==this.#n.value?.value?this.isFiltering=!0:this.multiple?this.isFiltering=!1:""!==this.#n.value?.value&&this.#n.value?.value!==this.lastSelectedAndEnabledOption?.label?(this.isFiltering=!0,this.isShowSingleSelectIcon=!1):(this.isFiltering=!1,this.isShowSingleSelectIcon=!1),this.#n.value){this.isAddButtonVisible=this.addButton&&this.#n.value?.value.trim().length>0&&!this.#d.some((({label:t})=>this.#n.value&&t?.toLowerCase()===this.#n.value.value.toLowerCase().trim()));try{e=await this.filter(this.#n.value.value)}catch{}}if(e)for(const t of this.#d)t.hidden=!e.includes(t);if(this.isCommunicateItemCountToScreenreaders=!0,this.#mt&&(this.itemCount=this.isAddButtonVisible?this.#mt.length+1:this.#mt.length),this.hasNoMatchingOptions=0===this.#mt?.length,this.hasNoMatchingOptions)return void(this.addButton?(this.isAddButtonActive=!0,this.activeOption&&this.#lt.value&&(this.#ut=this.activeOption,this.activeOption.privateActive=!1,this.ariaActivedescendant=this.#lt.value.id)):(this.activeOption&&(this.#ut=this.activeOption,this.activeOption.privateActive=!1),this.ariaActivedescendant=""));const i=this.#mt?.find((({disabled:t})=>!t));if(this.isAddButtonActive&&!this.isAddButtonVisible&&this.#ut&&!this.#ut.hidden&&!this.#ut.disabled)return this.isAddButtonActive=!1,this.#ut.privateActive=!0,void(this.ariaActivedescendant=this.#ut.id);if(this.isAddButtonActive&&!this.isAddButtonVisible&&i)return i.privateActive=!0,this.isAddButtonActive=!1,void(this.ariaActivedescendant=i.id);if((!this.activeOption||this.activeOption?.hidden||this.activeOption?.disabled)&&this.#ut&&!this.#ut.hidden&&!this.#ut.disabled){const t=this.#ut;return this.activeOption&&(this.#ut=this.activeOption,this.#ut.privateActive=!1),t.privateActive=!0,void(this.ariaActivedescendant=t.id)}return this.activeOption?.hidden&&i?(this.#ut=this.activeOption,this.activeOption.privateActive=!1,this.ariaActivedescendant=i.id,void(i.privateActive=!0)):void 0}#C(t){const e=this.selectedAndEnabledOptions.findLast(((t,e)=>e<=this.tagOverflowLimit-1));if(e&&"Backspace"===t.key&&!t.metaKey&&this.multiple&&this.#n.value&&0===this.#n.value.selectionStart)return this.#at=!0,e.selected=!1,void(this.#at=!1);const i=this.selectedAndEnabledOptions.filter(((t,e)=>e<=this.tagOverflowLimit-1));if(e&&"Backspace"===t.key&&t.metaKey&&this.multiple&&this.#n.value&&0===this.#n.value.selectionStart){this.#at=!0;for(const t of i)t.selected=!1;this.#at=!1}else;}#T(){this.#n.value&&(this.isInputOverflowing=this.#n.value.scrollWidth>this.#n.value.clientWidth)}#M(){this.#N.value&&(this.isInternalLabelOverflowing=this.#N.value.scrollWidth>this.#N.value.clientWidth)}get#d(){return this.#et.value?.assignedElements().filter((t=>t instanceof DropdownOption))??[]}get#h(){const t=this.#d;return this.#Y.value&&t.unshift(this.#Y.value),t}get#mt(){return this.#et.value?.assignedElements().filter((t=>t instanceof DropdownOption&&!t.hidden))}get#vt(){const t=this.#mt;return this.#Y.value&&!this.#Y.value.hidden&&t?.unshift(this.#Y.value),t}#z(t){t.target instanceof DropdownOption&&(t.target.selected=!t.target.selected),t.target===this.#Y.value&&this.#ft(),this.#n.value&&(this.#n.value.value=""),this.inputValue="",this.#l()}#U(t){if(t.target instanceof Element){const e=t.target.closest("glide-core-dropdown-option");if(e instanceof DropdownOption&&e.disabled)return;if(e instanceof DropdownOption&&e.privateIsEditActive)return e.privateEdit(),void(this.open=!1);if(e&&!e.selected)return e.selected=!0,this.#l(),this.open=!1,this.isInputTooltipOpen=!1,this.#n.value&&void 0!==e.label&&(this.#n.value.value=e.label,this.inputValue=e.label,this.isInputOverflowing=this.#n.value.scrollWidth-1>this.#n.value.clientWidth),this.dispatchEvent(new Event("input",{bubbles:!0,composed:!0})),void this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}));if(e?.selected&&!this.multiple)return void(this.open=!1)}}#J(t){if(this.multiple&&t.target instanceof DropdownOption&&t.target.disabled){if(t.target.selected&&(this.#r=this.#r.filter(((e,i)=>i!==this.selectedAndEnabledOptions.indexOf(t.target))),this.selectedAndEnabledOptions=this.selectedAndEnabledOptions.filter((e=>e!==t.target)),this.#g()),t.target.privateActive){t.target.privateActive=!1;const e=this.#d.find((({disabled:t})=>!t));e&&(e.privateActive=!0,this.#ut=e,this.ariaActivedescendant=e.id)}}else if(t.target instanceof DropdownOption&&t.target.disabled){if(this.selectedAndEnabledOptions=this.selectedAndEnabledOptions.filter((e=>e!==t.target)),this.#r=this.lastSelectedAndEnabledOption?.value?[this.lastSelectedAndEnabledOption.value]:[],t.target.privateActive){t.target.privateActive=!1;const e=this.#d.find((({disabled:t})=>!t));e&&(e.privateActive=!0,this.#ut=e,this.ariaActivedescendant=e.id)}this.#n.value&&(this.#n.value.value=this.lastSelectedAndEnabledOption?.label??"",this.inputValue=this.lastSelectedAndEnabledOption?.label??"",this.isInputOverflowing=this.#n.value.scrollWidth-1>this.#n.value.clientWidth);for(const e of this.#d)e.selected&&e!==t.target&&e.requestUpdate()}else if(this.multiple&&t.target instanceof DropdownOption&&t.target.selected)t.target.value&&this.#r.push(t.target.value),this.selectedAndEnabledOptions=[...this.selectedAndEnabledOptions,t.target],this.#g();else if(t.target instanceof DropdownOption&&t.target.selected&&void 0!==t.target.label){this.selectedAndEnabledOptions=[...this.selectedAndEnabledOptions,t.target],this.#r=t.target===this.lastSelectedAndEnabledOption&&t.target.value?[t.target.value]:[];for(const e of this.#d)e.selected&&e!==t.target&&e.requestUpdate();this.#n.value&&(this.#n.value.value=t.target.label,this.inputValue=t.target.label,this.isInputOverflowing=this.#n.value.scrollWidth-1>this.#n.value.clientWidth)}}#G(){this.requestUpdate()}#W(t){t.target instanceof DropdownOption&&(this.activeOption&&(this.activeOption.privateActive=!1),t.target.privateActive=!0,this.#ut=t.target)}#Q(){this.selectedAndEnabledOptions.length>0&&(this.multiple?this.requestUpdate():(this.filterable||this.isFilterable)&&this.#n.value&&this.lastSelectedAndEnabledOption?.label?(this.#n.value.value=this.lastSelectedAndEnabledOption.label,this.inputValue=this.lastSelectedAndEnabledOption.label,this.isInputOverflowing=this.#n.value.scrollWidth>this.#n.value.clientWidth):this.requestUpdate())}#P(t){(this.filterable||this.isFilterable)&&t.preventDefault()}#K(t){if(t.target instanceof DropdownOption&&this.#vt){if(t.target.disabled)return;this.activeOption&&(this.#ut=this.activeOption,this.activeOption.privateIsTooltipOpen=!1,this.activeOption.privateActive=!1),this.ariaActivedescendant=t.target.id,this.isAddButtonActive=!1,t.target.privateActive=!0,t.target.privateIsEditActive=!1}}#Z(t){if(!this.#pt){if(this.#Y.value&&(this.#Y.value.selected=this.areAllOptionsSelected),t.target instanceof DropdownOption){const e=this.selectedAndEnabledOptions.every((e=>e!==t.target));if(this.multiple)t.target.selected?(t.target.disabled&&(t.target.disabled=!1),e&&(this.selectedAndEnabledOptions=[...this.selectedAndEnabledOptions,t.target],this.#p||(this.#r=[...this.value,t.target.value]))):(this.#p||(this.#r=this.#r.filter(((e,i)=>i!==this.selectedAndEnabledOptions.indexOf(t.target)))),this.selectedAndEnabledOptions=this.selectedAndEnabledOptions.filter((e=>e!==t.target)));else if(!this.multiple)if(t.target.selected){t.target.disabled&&(t.target.disabled=!1),e&&(this.selectedAndEnabledOptions=[...this.selectedAndEnabledOptions,t.target]);for(const e of this.#d)e!==t.target&&(e.selected=!1);this.#p||(this.#r=[t.target.value])}else this.selectedAndEnabledOptions=this.selectedAndEnabledOptions.filter((e=>e!==t.target)),this.#p||(this.#r=this.lastSelectedAndEnabledOption?.value?[this.lastSelectedAndEnabledOption.value]:[])}this.multiple&&this.#g()}}#X(t){t.target instanceof DropdownOption&&this.multiple&&t.target.selected&&t.detail.new?this.#r=this.value.map((e=>e===t.detail.old?t.detail.new:e)):t.target instanceof DropdownOption&&this.multiple?this.#r=this.value.filter((e=>e!==t.detail.old)):t.target instanceof DropdownOption&&(this.#r=t.detail.new?[t.detail.new]:[])}#H(){this.isInternalLabelTooltipOpen=!0}#x(){this.isInternalLabelTooltipOpen=!1}#D(){this.#at=!0,this.open=!1}async#B(t){this.#at=!0,t.selected=!1;const e=this.#I.value?.querySelectorAll("glide-core-tag");if(e&&this.selectedAndEnabledOptions.length>0){const i=[...e].findIndex((e=>e.dataset.id===t.id)),s=e[i<e.length-1?i+1:i-1];await this.updateComplete,setTimeout((()=>{s?.focus(),this.#at=!1}))}else setTimeout((()=>{this.focus(),this.#at=!1}));this.dispatchEvent(new Event("input",{bubbles:!0,composed:!0})),this.dispatchEvent(new Event("change",{bubbles:!0,composed:!0}))}#k(t){t.stopPropagation()}#st(){this.#n.value&&(this.dispatchEvent(new CustomEvent("add",{bubbles:!0,composed:!0,detail:this.#n.value.value})),this.#n.value.value="",this.inputValue="");const t=this.#d.at(0);t&&(t.privateActive=!0,t.scrollIntoView(),this.ariaActivedescendant=t.id),this.multiple||(this.open=!1,this.isInputTooltipOpen=!1),this.#l(),this.focus()}#ft(){this.#pt=!0;for(const t of this.#d)!this.#Y.value?.selected||t.selected||t.disabled?!this.#Y.value?.selected&&t.selected&&(t.selected=!1):t.selected=!0;this.#pt=!1,this.selectedAndEnabledOptions=this.#d.filter((t=>t.selected&&!t.disabled)),this.#r=this.selectedAndEnabledOptions.map((({value:t})=>t)),this.#g()}async#g(){if(await this.updateComplete,this.#O.value){const t=this.#O.value.scrollWidth>this.#O.value.clientWidth;t&&this.tagOverflowLimit>1?(this.tagOverflowLimit=this.tagOverflowLimit-1,await this.updateComplete,this.#g()):!t&&!this.#rt&&this.tagOverflowLimit<this.selectedAndEnabledOptions.length?(this.#rt=!0,this.tagOverflowLimit=this.tagOverflowLimit+1,this.#g()):this.#rt=!1}}#i(){this.#ct?.(),this.#mt&&(this.itemCount=this.#mt.length),this.hasNoAvailableOptions=0===this.#d.length;const t=this.#vt?.find((t=>!t.disabled));!this.#ut||this.#ut.hidden||this.#ut.disabled?t&&(t.privateActive=!0,this.ariaActivedescendant=t.id,this.#ut=t):(this.#ut.privateActive=!0,this.ariaActivedescendant=this.#ut.id),this.#$.value&&this.#b.value&&(this.#ct=autoUpdate(this.#$.value,this.#b.value,(()=>{(async()=>{if(this.#$.value&&this.#b.value){const{x:t,y:e,placement:i}=await computePosition(this.#$.value,this.#b.value,{placement:"bottom-start",middleware:[offset({mainAxis:Number.parseFloat(window.getComputedStyle(document.body).getPropertyValue("--glide-core-spacing-base-xxs"))*Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize)}),flip()]});this.#b.value.dataset.placement=i,Object.assign(this.#b.value.style,{left:`${t}px`,top:`${e}px`}),this.#b.value?.showPopover()}})()})))}#l(){for(const t of this.#d)t.hidden=!1;this.isFiltering=!1,this.isAddButtonActive=!1,this.isAddButtonVisible=!1,this.hasNoMatchingOptions=!1,this.isShowSingleSelectIcon=Boolean(this.lastSelectedAndEnabledOption?.value)}};__decorate([property({reflect:!0}),required],Dropdown.prototype,"label",void 0),__decorate([property({attribute:"add-button",reflect:!0,type:Boolean})],Dropdown.prototype,"addButton",void 0),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"disabled",null),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"filterable",null),__decorate([property({attribute:"hide-label",reflect:!0,type:Boolean})],Dropdown.prototype,"hideLabel",void 0),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"loading",void 0),__decorate([property({reflect:!0,useDefault:!0})],Dropdown.prototype,"name",void 0),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"open",null),__decorate([property({reflect:!0,useDefault:!0})],Dropdown.prototype,"orientation",void 0),__decorate([property({reflect:!0})],Dropdown.prototype,"placeholder",void 0),__decorate([property()],Dropdown.prototype,"privateSplit",void 0),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"readonly",void 0),__decorate([property({attribute:"select-all",reflect:!0,type:Boolean})],Dropdown.prototype,"selectAll",void 0),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"required",void 0),__decorate([property({reflect:!0,type:Boolean})],Dropdown.prototype,"multiple",null),__decorate([property({reflect:!0})],Dropdown.prototype,"tooltip",void 0),__decorate([property({type:Array})],Dropdown.prototype,"value",null),__decorate([property({reflect:!0})],Dropdown.prototype,"variant",void 0),__decorate([property({reflect:!0})],Dropdown.prototype,"version",void 0),__decorate([state()],Dropdown.prototype,"ariaActivedescendant",void 0),__decorate([state()],Dropdown.prototype,"hasNoAvailableOptions",void 0),__decorate([state()],Dropdown.prototype,"hasNoMatchingOptions",void 0),__decorate([state()],Dropdown.prototype,"inputValue",void 0),__decorate([state()],Dropdown.prototype,"isAddButtonActive",void 0),__decorate([state()],Dropdown.prototype,"isAddButtonVisible",void 0),__decorate([state()],Dropdown.prototype,"isBlurring",void 0),__decorate([state()],Dropdown.prototype,"isCheckingValidity",void 0),__decorate([state()],Dropdown.prototype,"isCommunicateItemCountToScreenreaders",void 0),__decorate([state()],Dropdown.prototype,"isFilterable",void 0),__decorate([state()],Dropdown.prototype,"isFiltering",void 0),__decorate([state()],Dropdown.prototype,"isInputOverflowing",void 0),__decorate([state()],Dropdown.prototype,"isInputTooltipOpen",void 0),__decorate([state()],Dropdown.prototype,"isInternalLabelOverflowing",void 0),__decorate([state()],Dropdown.prototype,"isInternalLabelTooltipOpen",void 0),__decorate([state()],Dropdown.prototype,"isReportValidityOrSubmit",void 0),__decorate([state()],Dropdown.prototype,"isShowSingleSelectIcon",void 0),__decorate([state()],Dropdown.prototype,"itemCount",void 0),__decorate([state()],Dropdown.prototype,"selectedAndEnabledOptions",void 0),__decorate([state()],Dropdown.prototype,"tagOverflowLimit",void 0),__decorate([state()],Dropdown.prototype,"validityMessage",void 0),Dropdown=__decorate([customElement("glide-core-dropdown"),final],Dropdown);export default Dropdown;
|
926
|
+
</div>`;
|
927
|
+
}
|
928
|
+
reportValidity() {
|
929
|
+
this.isReportValidityOrSubmit = true;
|
930
|
+
const isValid = this.#internals.reportValidity();
|
931
|
+
// Ensures that getters referencing `this.validity.valid` are updated.
|
932
|
+
this.requestUpdate();
|
933
|
+
return isValid;
|
934
|
+
}
|
935
|
+
resetValidityFeedback() {
|
936
|
+
this.isReportValidityOrSubmit = false;
|
937
|
+
}
|
938
|
+
setCustomValidity(message) {
|
939
|
+
this.validityMessage = message;
|
940
|
+
if (message === '') {
|
941
|
+
this.#internals.setValidity({ customError: false }, '', this.filterable || this.isFilterable
|
942
|
+
? this.#inputElementRef.value
|
943
|
+
: this.#primaryButtonElementRef.value);
|
944
|
+
}
|
945
|
+
else {
|
946
|
+
// A validation message is required but unused because we disable native
|
947
|
+
// validation feedback. And an empty string isn't allowed. Thus a single space.
|
948
|
+
this.#internals.setValidity({
|
949
|
+
customError: true,
|
950
|
+
valueMissing: this.#internals.validity.valueMissing,
|
951
|
+
}, ' ', this.filterable || this.isFilterable
|
952
|
+
? this.#inputElementRef.value
|
953
|
+
: this.#primaryButtonElementRef.value);
|
954
|
+
}
|
955
|
+
}
|
956
|
+
setValidity(flags, message) {
|
957
|
+
this.validityMessage = message;
|
958
|
+
// A validation message is required but unused because we disable native
|
959
|
+
// validation feedback. And an empty string isn't allowed. Thus a single space.
|
960
|
+
this.#internals.setValidity(flags, ' ', this.filterable || this.isFilterable
|
961
|
+
? this.#inputElementRef.value
|
962
|
+
: this.#primaryButtonElementRef.value);
|
963
|
+
}
|
964
|
+
constructor() {
|
965
|
+
super();
|
966
|
+
this.addButton = false;
|
967
|
+
this.hideLabel = false;
|
968
|
+
this.loading = false;
|
969
|
+
this.name = '';
|
970
|
+
this.orientation = 'horizontal';
|
971
|
+
this.readonly = false;
|
972
|
+
this.selectAll = false;
|
973
|
+
this.required = false;
|
974
|
+
this.version = packageJson.version;
|
975
|
+
this.ariaActivedescendant = '';
|
976
|
+
// Used to show feedback when there are no slotted options.
|
977
|
+
this.hasNoAvailableOptions = false;
|
978
|
+
// Used to show feedback when every option has been hidden by having been filtered
|
979
|
+
// out. The names for both this and `hasNoAvailableOptions` are not ideal. But both
|
980
|
+
// are downstream of the copy provided by Design.
|
981
|
+
this.hasNoMatchingOptions = false;
|
982
|
+
this.inputValue = '';
|
983
|
+
this.isAddButtonActive = false;
|
984
|
+
this.isAddButtonVisible = false;
|
985
|
+
this.isBlurring = false;
|
986
|
+
this.isCheckingValidity = false;
|
987
|
+
// `itemCount` isn't immediately communicated to screenreaders so the number of
|
988
|
+
// options isn't read twice when Dropdown receives focus. It's already read once
|
989
|
+
// without additional effort due to `role="combobox"` and
|
990
|
+
// `aria-controls="options"`.
|
991
|
+
//
|
992
|
+
// However, an updated count isn't read when the user filters, which is where
|
993
|
+
// `itemCount` comes in. `isCommunicateItemCountToScreenreaders` is used to toggle
|
994
|
+
// `itemCount` so it isn't read on focus.
|
995
|
+
this.isCommunicateItemCountToScreenreaders = false;
|
996
|
+
this.isFilterable = false;
|
997
|
+
this.isFiltering = false;
|
998
|
+
this.isInputOverflowing = false;
|
999
|
+
this.isInputTooltipOpen = false;
|
1000
|
+
this.isInternalLabelOverflowing = false;
|
1001
|
+
this.isInternalLabelTooltipOpen = false;
|
1002
|
+
this.isReportValidityOrSubmit = false;
|
1003
|
+
this.isShowSingleSelectIcon = false;
|
1004
|
+
// "optionCount" or similar is arguably more natural. But "item" is how VoiceOver
|
1005
|
+
// refers to options when Dropdown is initially focused. So that's what the text
|
1006
|
+
// is. And so the variable name follows the text.
|
1007
|
+
this.itemCount = 0;
|
1008
|
+
this.selectedAndEnabledOptions = [];
|
1009
|
+
this.tagOverflowLimit = 0;
|
1010
|
+
this.#addButtonElementRef = createRef();
|
1011
|
+
// Not dynamically generated in `render()` so it remains constant when referenced
|
1012
|
+
// by `this.ariaActiveDescendant`.
|
1013
|
+
this.#addButtonId = uniqueId();
|
1014
|
+
this.#componentElementRef = createRef();
|
1015
|
+
this.#defaultSlotElementRef = createRef();
|
1016
|
+
this.#dropdownElementRef = createRef();
|
1017
|
+
this.#editButtonElementRef = createRef();
|
1018
|
+
this.#inputElementRef = createRef();
|
1019
|
+
this.#internalLabelElementRef = createRef();
|
1020
|
+
// `#onDocumentClick()` listens for clicks in their capture phase. There's a
|
1021
|
+
// comment in that method explaining why. `#isComponentClick` is set in
|
1022
|
+
// `#onComponentMouseup()` before `#onDocumentClick()` is called so
|
1023
|
+
// `#onDocumentClick()` has the information it needs to decide if it should
|
1024
|
+
// close Dropdown.
|
1025
|
+
this.#isComponentClick = false;
|
1026
|
+
this.#isDisabled = false;
|
1027
|
+
// Used to prevent Dropdown from reopening on click immediately after it's
|
1028
|
+
// closed when a tag is edited. Also used to prevent form submissions when a
|
1029
|
+
// tag is edited via Enter.
|
1030
|
+
this.#isEditingOrRemovingTag = false;
|
1031
|
+
this.#isFilterable = false;
|
1032
|
+
// Used in `#onDefaultSlotChange()` to lock Dropdown into being automatically
|
1033
|
+
// filterable or not based on the number of options present on first render.
|
1034
|
+
this.#isFirstDefaultSlotChange = true;
|
1035
|
+
this.#isMultiple = false;
|
1036
|
+
this.#isOpen = false;
|
1037
|
+
// See `#setTagOverflowLimit()`.
|
1038
|
+
this.#isOverflowTest = false;
|
1039
|
+
// Used in `#onOptionsSelectedChange()` to guard against, among other things,
|
1040
|
+
// resetting Select All back to its previous value after Select All is selected
|
1041
|
+
// or deselected.
|
1042
|
+
this.#isSelectionFromSelectAllOrNone = false;
|
1043
|
+
// Used in `#onOptionsSelectedChange()` to guard against a loop of duplicate
|
1044
|
+
// additions to `this.value` when `#onOptionsSelectedChange()` is called as a
|
1045
|
+
// result of `this.value` being set.
|
1046
|
+
this.#isSelectionFromValueSetter = false;
|
1047
|
+
// Used in `#onDropdownClick()`, which is opens and closes Dropdown. Space and
|
1048
|
+
// Enter produce "click" events. This field gives `#onDropdownClick()` the
|
1049
|
+
// information it needs to guard against opening or closing when the click comes
|
1050
|
+
// from option selection or deselection.
|
1051
|
+
this.#isSelectionViaSpaceOrEnter = false;
|
1052
|
+
this.#localize = new LocalizeController(this);
|
1053
|
+
this.#optionsAndFeedbackElementRef = createRef();
|
1054
|
+
this.#primaryButtonElementRef = createRef();
|
1055
|
+
this.#selectAllElementRef = createRef();
|
1056
|
+
this.#tagsElementRef = createRef();
|
1057
|
+
this.#value = [];
|
1058
|
+
// An arrow function field instead of a method so `this` is closed over and set
|
1059
|
+
// to the component instead of `document`.
|
1060
|
+
this.#onDocumentClick = () => {
|
1061
|
+
if (this.#isComponentClick) {
|
1062
|
+
// If the click came from within Dropdown, Dropdown should stay open. But,
|
1063
|
+
// now that the click has happened, we need reset `#isComponentClick` so
|
1064
|
+
// a later click from outside of Dropdown results in Dropdown closing.
|
1065
|
+
//
|
1066
|
+
// Options with a Checkbox emit two "click" events for every "mouseup": one
|
1067
|
+
// from the `<label>`, another from input field. It's just how a `<label>`
|
1068
|
+
// with a form control works. A timeout is used to ensure both events have
|
1069
|
+
// been dispatched before `#isComponentClick` is reset.
|
1070
|
+
//
|
1071
|
+
// Checking that the click's `event.target` is an instance of `Dropdown`
|
1072
|
+
// or `DropdownOption` would be a lot simpler. But, when Dropdown is
|
1073
|
+
// inside of another web component, `event.target` will that component instead.
|
1074
|
+
setTimeout(() => {
|
1075
|
+
this.#isComponentClick = false;
|
1076
|
+
});
|
1077
|
+
}
|
1078
|
+
else {
|
1079
|
+
this.open = false;
|
1080
|
+
}
|
1081
|
+
};
|
1082
|
+
// An arrow function field instead of a method so `this` is closed over and
|
1083
|
+
// set to the component instead of the form.
|
1084
|
+
this.#onFormdata = ({ formData }) => {
|
1085
|
+
if (this.name && this.value.length > 0 && !this.disabled) {
|
1086
|
+
formData.append(this.name, JSON.stringify(this.value));
|
1087
|
+
}
|
1088
|
+
};
|
1089
|
+
this.#internals = this.attachInternals();
|
1090
|
+
// Event handlers on the host aren't great because consumers can remove them.
|
1091
|
+
// Unfortunately, the host is the only thing on which this event is dispatched
|
1092
|
+
// because it's the host that is form-associated.
|
1093
|
+
this.addEventListener('invalid', (event) => {
|
1094
|
+
event.preventDefault(); // Canceled so a native validation message isn't shown.
|
1095
|
+
// We only want to focus the input if the "invalid" event resulted from either:
|
1096
|
+
//
|
1097
|
+
// 1. A form submission.
|
1098
|
+
// 2. A call of `reportValidity()` that did not result from Checkbox's "blur"
|
1099
|
+
// event.
|
1100
|
+
if (this.isCheckingValidity || this.isBlurring) {
|
1101
|
+
return;
|
1102
|
+
}
|
1103
|
+
this.isReportValidityOrSubmit = true;
|
1104
|
+
const isFirstInvalidFormElement = this.form?.querySelector(':invalid') === this;
|
1105
|
+
if (isFirstInvalidFormElement) {
|
1106
|
+
// Canceling the event means Dropdown won't get focus, even if we were to use
|
1107
|
+
// `this.#internals.delegatesFocus`. So we have to focus manually.
|
1108
|
+
this.focus();
|
1109
|
+
}
|
1110
|
+
});
|
1111
|
+
}
|
1112
|
+
#addButtonElementRef;
|
1113
|
+
// Not dynamically generated in `render()` so it remains constant when referenced
|
1114
|
+
// by `this.ariaActiveDescendant`.
|
1115
|
+
#addButtonId;
|
1116
|
+
#cleanUpFloatingUi;
|
1117
|
+
#componentElementRef;
|
1118
|
+
#defaultSlotElementRef;
|
1119
|
+
#dropdownElementRef;
|
1120
|
+
#editButtonElementRef;
|
1121
|
+
#inputElementRef;
|
1122
|
+
#internalLabelElementRef;
|
1123
|
+
#internals;
|
1124
|
+
// `#onDocumentClick()` listens for clicks in their capture phase. There's a
|
1125
|
+
// comment in that method explaining why. `#isComponentClick` is set in
|
1126
|
+
// `#onComponentMouseup()` before `#onDocumentClick()` is called so
|
1127
|
+
// `#onDocumentClick()` has the information it needs to decide if it should
|
1128
|
+
// close Dropdown.
|
1129
|
+
#isComponentClick;
|
1130
|
+
#isDisabled;
|
1131
|
+
// Used to prevent Dropdown from reopening on click immediately after it's
|
1132
|
+
// closed when a tag is edited. Also used to prevent form submissions when a
|
1133
|
+
// tag is edited via Enter.
|
1134
|
+
#isEditingOrRemovingTag;
|
1135
|
+
#isFilterable;
|
1136
|
+
// Used in `#onDefaultSlotChange()` to lock Dropdown into being automatically
|
1137
|
+
// filterable or not based on the number of options present on first render.
|
1138
|
+
#isFirstDefaultSlotChange;
|
1139
|
+
#isMultiple;
|
1140
|
+
#isOpen;
|
1141
|
+
// See `#setTagOverflowLimit()`.
|
1142
|
+
#isOverflowTest;
|
1143
|
+
// Used in `#onOptionsSelectedChange()` to guard against, among other things,
|
1144
|
+
// resetting Select All back to its previous value after Select All is selected
|
1145
|
+
// or deselected.
|
1146
|
+
#isSelectionFromSelectAllOrNone;
|
1147
|
+
// Used in `#onOptionsSelectedChange()` to guard against a loop of duplicate
|
1148
|
+
// additions to `this.value` when `#onOptionsSelectedChange()` is called as a
|
1149
|
+
// result of `this.value` being set.
|
1150
|
+
#isSelectionFromValueSetter;
|
1151
|
+
// Used in `#onDropdownClick()`, which is opens and closes Dropdown. Space and
|
1152
|
+
// Enter produce "click" events. This field gives `#onDropdownClick()` the
|
1153
|
+
// information it needs to guard against opening or closing when the click comes
|
1154
|
+
// from option selection or deselection.
|
1155
|
+
#isSelectionViaSpaceOrEnter;
|
1156
|
+
#localize;
|
1157
|
+
#optionsAndFeedbackElementRef;
|
1158
|
+
// Used in various situations to reactivate the previously active option.
|
1159
|
+
#previouslyActiveOption;
|
1160
|
+
#primaryButtonElementRef;
|
1161
|
+
#selectAllElementRef;
|
1162
|
+
#shadowRoot;
|
1163
|
+
#tagsElementRef;
|
1164
|
+
#value;
|
1165
|
+
// An arrow function field instead of a method so `this` is closed over and set
|
1166
|
+
// to the component instead of `document`.
|
1167
|
+
#onDocumentClick;
|
1168
|
+
// An arrow function field instead of a method so `this` is closed over and
|
1169
|
+
// set to the component instead of the form.
|
1170
|
+
#onFormdata;
|
1171
|
+
#hide() {
|
1172
|
+
this.#cleanUpFloatingUi?.();
|
1173
|
+
this.#optionsAndFeedbackElementRef.value?.hidePopover();
|
1174
|
+
this.ariaActivedescendant = '';
|
1175
|
+
if (this.activeOption) {
|
1176
|
+
this.#previouslyActiveOption = this.activeOption;
|
1177
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1178
|
+
this.activeOption.privateActive = false;
|
1179
|
+
}
|
1180
|
+
}
|
1181
|
+
get #isShowValidationFeedback() {
|
1182
|
+
return (!this.disabled && !this.validity.valid && this.isReportValidityOrSubmit);
|
1183
|
+
}
|
1184
|
+
#onAddButtonMouseover() {
|
1185
|
+
this.isAddButtonActive = true;
|
1186
|
+
if (this.#addButtonElementRef.value) {
|
1187
|
+
this.ariaActivedescendant = this.#addButtonElementRef.value.id;
|
1188
|
+
}
|
1189
|
+
if (this.activeOption) {
|
1190
|
+
this.#previouslyActiveOption = this.activeOption;
|
1191
|
+
this.activeOption.privateActive = false;
|
1192
|
+
}
|
1193
|
+
}
|
1194
|
+
#onComponentMouseup() {
|
1195
|
+
this.#isComponentClick = true;
|
1196
|
+
}
|
1197
|
+
async #onDefaultSlotChange() {
|
1198
|
+
if (this.open) {
|
1199
|
+
const firstEnabledOption = this.#optionElementsNotHiddenIncludingSelectAll?.find((option) => !option.disabled);
|
1200
|
+
// Setting the active option happens here and elsewhere instead of once in the
|
1201
|
+
// `open` setter because Dropdown's options may change when Dropdown is open.
|
1202
|
+
// For example, a developer may for whatever reason, may remove the active option
|
1203
|
+
// while Dropdown is open, leaving the user without one.
|
1204
|
+
if ((!this.activeOption || this.activeOption?.disabled) &&
|
1205
|
+
firstEnabledOption) {
|
1206
|
+
this.#previouslyActiveOption = firstEnabledOption;
|
1207
|
+
this.ariaActivedescendant = firstEnabledOption.id;
|
1208
|
+
firstEnabledOption.privateActive = true;
|
1209
|
+
}
|
1210
|
+
}
|
1211
|
+
if (this.#selectAllElementRef.value) {
|
1212
|
+
this.#selectAllElementRef.value.selected = this.areAllOptionsSelected;
|
1213
|
+
}
|
1214
|
+
// Set here in addition to in `#show()` because, every option may be removed by the
|
1215
|
+
// consumer while Dropdown is open. Many consumers do this while the user is
|
1216
|
+
// filtering.
|
1217
|
+
this.hasNoAvailableOptions = this.#optionElements.length === 0;
|
1218
|
+
this.selectedAndEnabledOptions = this.#optionElements.filter((option) => {
|
1219
|
+
return option.selected && !option.disabled;
|
1220
|
+
});
|
1221
|
+
if (this.#isFirstDefaultSlotChange) {
|
1222
|
+
// It's a requirement of Design for Dropdown to automatically become filterable
|
1223
|
+
// when there are more than 10 options. But it's also a bad user experience for
|
1224
|
+
// Dropdown to suddenly become unfilterable when a developer using Dropdown reduces
|
1225
|
+
// the number of options in the slot in response to the user filtering.
|
1226
|
+
//
|
1227
|
+
// So we lock in Dropdown as either filterable or unfilterable with the first slot
|
1228
|
+
// change. Consumers can still force filterability using the `filterable`
|
1229
|
+
// attribute.
|
1230
|
+
this.isFilterable = this.#optionElements.length > 10;
|
1231
|
+
this.#isFirstDefaultSlotChange = false;
|
1232
|
+
}
|
1233
|
+
if (this.multiple) {
|
1234
|
+
this.#value = this.selectedAndEnabledOptions
|
1235
|
+
.filter(({ value }) => Boolean(value))
|
1236
|
+
.map(({ value }) => value);
|
1237
|
+
// We set the overflow limit to show every tag initially. `#setTagOverflowLimit()`
|
1238
|
+
// then pares the limit back if necessary so no tags are overflowing.
|
1239
|
+
this.tagOverflowLimit = this.selectedAndEnabledOptions.length;
|
1240
|
+
this.#setTagOverflowLimit();
|
1241
|
+
}
|
1242
|
+
else {
|
1243
|
+
// With single-select, there's nothing to stop developers from adding a `selected`
|
1244
|
+
// attribute to more than one option. How native handles this when setting `value`
|
1245
|
+
// is to choose the last selected option, which seems reasonable.
|
1246
|
+
if (this.lastSelectedAndEnabledOption?.value) {
|
1247
|
+
this.#value = [this.lastSelectedAndEnabledOption.value];
|
1248
|
+
}
|
1249
|
+
this.isShowSingleSelectIcon = Boolean(this.lastSelectedAndEnabledOption?.value);
|
1250
|
+
// Dropdown becomes filterable if there are more than 10 options. But input field
|
1251
|
+
// won't have rendered yet given we just set `this.isFilterable` above. So we wait
|
1252
|
+
// for it to render (or not) before setting the value of input field.
|
1253
|
+
await this.updateComplete;
|
1254
|
+
if (this.#inputElementRef.value &&
|
1255
|
+
this.lastSelectedAndEnabledOption?.label) {
|
1256
|
+
this.#inputElementRef.value.value =
|
1257
|
+
this.lastSelectedAndEnabledOption.label;
|
1258
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
1259
|
+
this.isInputOverflowing =
|
1260
|
+
this.#inputElementRef.value.scrollWidth >
|
1261
|
+
this.#inputElementRef.value.clientWidth;
|
1262
|
+
// For the case where the selected option is programmatically removed from the DOM.
|
1263
|
+
// Without this, the value of the input field would still be set to the selected
|
1264
|
+
// option's `label`. And an ellipsis would still be shown if the `label` was long
|
1265
|
+
// enough to be truncated.
|
1266
|
+
//
|
1267
|
+
// We guard against `this.isFiltering` so we don't clear the user's filter query
|
1268
|
+
// when the user is filtering and an option is added or removed. This is
|
1269
|
+
// particularly helpful when consumers fetch and render options from the server in
|
1270
|
+
// response to their override of `this.filter()` being called.
|
1271
|
+
}
|
1272
|
+
else if (this.#inputElementRef.value && !this.isFiltering) {
|
1273
|
+
this.#inputElementRef.value.value = '';
|
1274
|
+
this.inputValue = '';
|
1275
|
+
this.isAddButtonVisible = false;
|
1276
|
+
this.isInputOverflowing =
|
1277
|
+
this.#inputElementRef.value.scrollWidth >
|
1278
|
+
this.#inputElementRef.value.clientWidth;
|
1279
|
+
}
|
1280
|
+
}
|
1281
|
+
for (const option of this.#optionElements) {
|
1282
|
+
// Both here and in the `this.multiple` setter because the setter isn't called when
|
1283
|
+
// an option is added to Dropdown after initial render.
|
1284
|
+
option.privateMultiple = this.multiple;
|
1285
|
+
if (!this.multiple && option.selected) {
|
1286
|
+
// When Dropdown is single-select, a Dropdown Option only appears as selected when
|
1287
|
+
// it's the last selected option because only the `value` of the last selected
|
1288
|
+
// option will be included in Dropdown's `value`. And what the user sees as
|
1289
|
+
// selected should always be the same as what's submitted with the form.
|
1290
|
+
//
|
1291
|
+
// Dropdown Options determine whether they're the last selected option (and thus
|
1292
|
+
// whether to show themselves as selected using a checkmark) via their internal
|
1293
|
+
// `lastSelectedAndEnabledOption` getter.
|
1294
|
+
//
|
1295
|
+
// An additional selected option can be added to Dropdown's default slot at any
|
1296
|
+
// time. And it may now be the last selected option. But what was the last selected
|
1297
|
+
// option won't know it's no longer the last. So we force selected options to
|
1298
|
+
// rerender.
|
1299
|
+
option.requestUpdate();
|
1300
|
+
}
|
1301
|
+
}
|
1302
|
+
}
|
1303
|
+
#onDropdownAndOptionsFocusout(event) {
|
1304
|
+
// If `event.relatedTarget` is `null`, the user has clicked an element outside
|
1305
|
+
// Dropdown that cannot receive focus. Otherwise, the user has either clicked
|
1306
|
+
// an element outside Dropdown that can receive focus or else has tabbed away
|
1307
|
+
// from Dropdown.
|
1308
|
+
const isFocusLost = event.relatedTarget === null ||
|
1309
|
+
(event.relatedTarget instanceof Node &&
|
1310
|
+
!this.#shadowRoot?.contains(event.relatedTarget) &&
|
1311
|
+
!this.contains(event.relatedTarget));
|
1312
|
+
if (isFocusLost && !this.#isEditingOrRemovingTag) {
|
1313
|
+
this.open = false;
|
1314
|
+
this.isBlurring = true;
|
1315
|
+
this.reportValidity();
|
1316
|
+
this.isBlurring = false;
|
1317
|
+
}
|
1318
|
+
}
|
1319
|
+
// This handler is on `.dropdown-and-options` instead of `.dropdown` because
|
1320
|
+
// options can receive focus via VoiceOver, and `.dropdown` won't emit a "keydown"
|
1321
|
+
// if an option is focused.
|
1322
|
+
#onDropdownAndOptionsKeydown(event) {
|
1323
|
+
if (this.disabled || this.readonly) {
|
1324
|
+
return;
|
1325
|
+
}
|
1326
|
+
// On Enter and Space, an "edit" event should be dispatched and Dropdown should
|
1327
|
+
// close. Both Enter and Space result in a "click" event. So no need to dispatch
|
1328
|
+
// "edit" here. It's dispatched in `#onDropdownClick`. And Dropdown is closed via
|
1329
|
+
// `#onEditButtonClick`.
|
1330
|
+
if ((event.key === 'Enter' || event.key === ' ') &&
|
1331
|
+
event.target === this.#editButtonElementRef.value) {
|
1332
|
+
this.#isComponentClick = true;
|
1333
|
+
return;
|
1334
|
+
}
|
1335
|
+
if (!this.open && event.key === 'Enter' && !this.#isEditingOrRemovingTag) {
|
1336
|
+
this.form?.requestSubmit();
|
1337
|
+
return;
|
1338
|
+
}
|
1339
|
+
if (event.key === 'Escape') {
|
1340
|
+
// Prevent Safari from leaving full screen.
|
1341
|
+
event.preventDefault();
|
1342
|
+
this.open = false;
|
1343
|
+
return;
|
1344
|
+
}
|
1345
|
+
const isFromPrimaryButtonOrInputOrAnOption = event.target === this.#primaryButtonElementRef.value ||
|
1346
|
+
event.target === this.#inputElementRef.value ||
|
1347
|
+
event.target instanceof DropdownOption;
|
1348
|
+
// If multiselect, and `event.target` isn't one of the above, then the event came
|
1349
|
+
// from a tag and focus is on the tag. Arrowing up or down then pressing Enter
|
1350
|
+
// would both remove the tag and select or deselect the active option or it would
|
1351
|
+
// open Dropdown, and neither of which is probably what the user would expect.
|
1352
|
+
// Similar situation for when a key is pressed and focus is on single-select's Edit
|
1353
|
+
// button.
|
1354
|
+
if (this.multiple && !isFromPrimaryButtonOrInputOrAnOption) {
|
1355
|
+
return;
|
1356
|
+
}
|
1357
|
+
if (!this.open && [' ', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
|
1358
|
+
// Prevents page scroll. Also prevents the insertion point moving to beginning or
|
1359
|
+
// end of the field and a space from being entered in addition to making the
|
1360
|
+
// options visible when Dropdown is filterable.
|
1361
|
+
event.preventDefault();
|
1362
|
+
this.open = true;
|
1363
|
+
if (this.activeOption) {
|
1364
|
+
this.activeOption.privateIsTooltipOpen =
|
1365
|
+
!this.activeOption.privateIsEditActive;
|
1366
|
+
}
|
1367
|
+
// The user almost certainly wasn't intending to do both open Dropdown and change
|
1368
|
+
// the active option in the case of ArrowUp or ArrowDown. Thus return. The user
|
1369
|
+
// can press ArrowUp or ArrowDown again to change the active option.
|
1370
|
+
return;
|
1371
|
+
}
|
1372
|
+
// `event.key` is checked in the below conditions instead of this one to trap every
|
1373
|
+
// key press in this condition, which returns, so they're not handled elsewhere.
|
1374
|
+
// Any other key, like ArrowDown and PageDown, not handled below is meant to have
|
1375
|
+
// no effect when the Add button is active.
|
1376
|
+
if (this.isAddButtonActive && this.open) {
|
1377
|
+
if ((event.key === 'ArrowUp' && event.metaKey) ||
|
1378
|
+
['Home', 'PageUp'].includes(event.key)) {
|
1379
|
+
// Prevent page scroll. Also prevent the insertion point from moving to the
|
1380
|
+
// beginning of the field.
|
1381
|
+
event.preventDefault();
|
1382
|
+
const firstOption = this.#optionElementsNotHidden?.at(0);
|
1383
|
+
if (firstOption) {
|
1384
|
+
firstOption.privateActive = true;
|
1385
|
+
firstOption.privateIsTooltipOpen = !firstOption.editable;
|
1386
|
+
this.isAddButtonActive = false;
|
1387
|
+
this.ariaActivedescendant = firstOption.id;
|
1388
|
+
firstOption.scrollIntoView();
|
1389
|
+
}
|
1390
|
+
}
|
1391
|
+
if (event.key === 'ArrowUp') {
|
1392
|
+
// Prevent page scroll. Also prevent the insertion point from moving to the
|
1393
|
+
// beginning of the field.
|
1394
|
+
event.preventDefault();
|
1395
|
+
// The user can jump to the Add button from any other option is active by pressing
|
1396
|
+
// Meta + ArrowDown, PageDown, or End. So he may not have arrived on the Add button
|
1397
|
+
// via the last option. That's why, when `this.#previouslyActiveOption` is
|
1398
|
+
// available, we move the user back to it. That way he doesn't have arrow up
|
1399
|
+
// through what may be a long list of options to get back to where he was.
|
1400
|
+
const option = this.#previouslyActiveOption && !this.#previouslyActiveOption.hidden
|
1401
|
+
? this.#previouslyActiveOption
|
1402
|
+
: this.#optionElementsNotHidden?.at(-1);
|
1403
|
+
if (option) {
|
1404
|
+
option.privateActive = true;
|
1405
|
+
option.privateIsEditActive = option.editable;
|
1406
|
+
option.privateIsTooltipOpen = !option.editable;
|
1407
|
+
this.isAddButtonActive = false;
|
1408
|
+
this.ariaActivedescendant = option.id;
|
1409
|
+
}
|
1410
|
+
}
|
1411
|
+
if (event.key === 'Enter') {
|
1412
|
+
this.#selectAddButton();
|
1413
|
+
}
|
1414
|
+
return;
|
1415
|
+
}
|
1416
|
+
if (this.activeOption && this.open) {
|
1417
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
1418
|
+
if (this.activeOption.privateIsEditActive) {
|
1419
|
+
// Pressing Enter or Space when an option is active won't result in a
|
1420
|
+
// "click" event because options don't receive focus except with VoiceOver.
|
1421
|
+
// Otherwise, we'd dispatch this event from within Dropdown Option.
|
1422
|
+
//
|
1423
|
+
// A "click" event, on the other hand, is dispatched when an Edit button is
|
1424
|
+
// clicked. So Dropdown Option could dispatch "edit" in that case. But then
|
1425
|
+
// two components instead of one would be responsible for dispatching "edit".
|
1426
|
+
this.activeOption.privateEdit();
|
1427
|
+
this.open = false;
|
1428
|
+
return;
|
1429
|
+
}
|
1430
|
+
// Space is excluded when Dropdown is filterable because the user may want to
|
1431
|
+
// include a space in his filter and because he expects pressing Space to result
|
1432
|
+
// in a space. So we either cancel Space and let it select and deselect as when
|
1433
|
+
// Dropdown isn't filterable, or we let the user type it. Neither is ideal.
|
1434
|
+
if ((event.key === 'Enter' &&
|
1435
|
+
this.#optionElementsNotHidden &&
|
1436
|
+
this.#optionElementsNotHidden.length > 0) ||
|
1437
|
+
(event.key === ' ' && !this.filterable && !this.isFilterable)) {
|
1438
|
+
this.#isSelectionViaSpaceOrEnter = true;
|
1439
|
+
// Prevent the options from scrolling when a focused option is selected via Space
|
1440
|
+
// when using VoiceOver.
|
1441
|
+
event.preventDefault();
|
1442
|
+
this.activeOption.selected = this.multiple
|
1443
|
+
? !this.activeOption.selected
|
1444
|
+
: true;
|
1445
|
+
if (this.activeOption === this.#selectAllElementRef.value) {
|
1446
|
+
this.#selectAllOrNone();
|
1447
|
+
}
|
1448
|
+
this.#isSelectionViaSpaceOrEnter = false;
|
1449
|
+
this.#unfilter();
|
1450
|
+
if (this.multiple) {
|
1451
|
+
if (this.#inputElementRef.value) {
|
1452
|
+
this.#inputElementRef.value.value = '';
|
1453
|
+
}
|
1454
|
+
this.inputValue = '';
|
1455
|
+
}
|
1456
|
+
else {
|
1457
|
+
if (this.#inputElementRef.value &&
|
1458
|
+
// `undefined` is guarded against only to satisfy the type system.
|
1459
|
+
// `this.activeOption.label` is guaranteed to be defined because it's required.
|
1460
|
+
this.activeOption.label !== undefined) {
|
1461
|
+
this.#inputElementRef.value.value = this.activeOption.label;
|
1462
|
+
this.inputValue = this.activeOption.label;
|
1463
|
+
// One is subtracted to account for an apparent Chrome bug where `scrollWidth`
|
1464
|
+
// is off by one relative to `clientWidth` when they should be the same. It
|
1465
|
+
// happens whenever the input field was overflowing and now isn't.
|
1466
|
+
this.isInputOverflowing =
|
1467
|
+
this.#inputElementRef.value.scrollWidth - 1 >
|
1468
|
+
this.#inputElementRef.value.clientWidth;
|
1469
|
+
}
|
1470
|
+
this.open = false;
|
1471
|
+
// `this.isInputTooltipOpen` is set to `true` when the input field receives
|
1472
|
+
// focus and `false` when it loses focus. It's currently `true`. But the
|
1473
|
+
// user just selected an option and so knows what the option's label is. So
|
1474
|
+
// there's no need to show a tooltip.
|
1475
|
+
this.isInputTooltipOpen = false;
|
1476
|
+
}
|
1477
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
1478
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
1479
|
+
return;
|
1480
|
+
}
|
1481
|
+
}
|
1482
|
+
const activeOptionIndex = this.#optionElementsNotHiddenIncludingSelectAll?.indexOf(this.activeOption);
|
1483
|
+
// All the logic below could just as well go in a "keydown" handler on each
|
1484
|
+
// Dropdown Option. It's here to mirror the tests, which necessarily test against
|
1485
|
+
// Dropdown as a whole because more than one option is required to test these
|
1486
|
+
// interactions.
|
1487
|
+
if (event.key === 'ArrowUp' &&
|
1488
|
+
!event.metaKey &&
|
1489
|
+
this.#optionElementsNotHiddenIncludingSelectAll &&
|
1490
|
+
typeof activeOptionIndex === 'number') {
|
1491
|
+
// Prevent page scroll. When filterable, also prevent the insertion point from
|
1492
|
+
// moving to the beginning of the field.
|
1493
|
+
event.preventDefault();
|
1494
|
+
const previousOption = this.#optionElementsNotHiddenIncludingSelectAll.findLast((option, index) => {
|
1495
|
+
return !option.disabled && index < activeOptionIndex;
|
1496
|
+
});
|
1497
|
+
if (this.activeOption?.privateIsEditActive) {
|
1498
|
+
this.activeOption.privateIsEditActive = false;
|
1499
|
+
this.activeOption.privateIsTooltipOpen = true;
|
1500
|
+
// If `previousOption` isn't defined, we've reached the top.
|
1501
|
+
}
|
1502
|
+
else if (previousOption) {
|
1503
|
+
this.#previouslyActiveOption = this.activeOption;
|
1504
|
+
this.activeOption.privateIsEditActive = false;
|
1505
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1506
|
+
this.activeOption.privateActive = false;
|
1507
|
+
this.ariaActivedescendant = previousOption.id;
|
1508
|
+
previousOption.privateActive = true;
|
1509
|
+
previousOption.privateIsEditActive = previousOption.editable;
|
1510
|
+
previousOption.privateIsTooltipOpen = !previousOption.editable;
|
1511
|
+
// "center" so options before the current one are in view.
|
1512
|
+
previousOption.scrollIntoView({ block: 'center' });
|
1513
|
+
}
|
1514
|
+
return;
|
1515
|
+
}
|
1516
|
+
if (event.key === 'ArrowDown' &&
|
1517
|
+
!event.metaKey &&
|
1518
|
+
this.#optionElementsNotHiddenIncludingSelectAll &&
|
1519
|
+
typeof activeOptionIndex === 'number') {
|
1520
|
+
// Prevent page scroll. When filterable, also prevent the insertion point from
|
1521
|
+
// moving to the end of the field.
|
1522
|
+
event.preventDefault();
|
1523
|
+
const nextOption = this.#optionElementsNotHiddenIncludingSelectAll.find((option, index) => {
|
1524
|
+
return !option.disabled && index > activeOptionIndex;
|
1525
|
+
});
|
1526
|
+
if (this.activeOption.editable &&
|
1527
|
+
!this.activeOption.privateIsEditActive) {
|
1528
|
+
this.activeOption.privateIsEditActive = true;
|
1529
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1530
|
+
// If `nextOption` isn't defined, we've reached the bottom.
|
1531
|
+
}
|
1532
|
+
else if (nextOption) {
|
1533
|
+
this.#previouslyActiveOption = this.activeOption;
|
1534
|
+
this.activeOption.privateIsEditActive = false;
|
1535
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1536
|
+
this.activeOption.privateActive = false;
|
1537
|
+
this.ariaActivedescendant = nextOption.id;
|
1538
|
+
nextOption.privateActive = true;
|
1539
|
+
nextOption.privateIsTooltipOpen = true;
|
1540
|
+
// "center" so options after the current one are in view.
|
1541
|
+
nextOption.scrollIntoView({ block: 'center' });
|
1542
|
+
}
|
1543
|
+
else if (this.isAddButtonVisible && this.#addButtonElementRef.value) {
|
1544
|
+
this.#previouslyActiveOption = this.activeOption;
|
1545
|
+
this.activeOption.privateIsEditActive = false;
|
1546
|
+
this.activeOption.privateActive = false;
|
1547
|
+
this.isAddButtonActive = true;
|
1548
|
+
this.ariaActivedescendant = this.#addButtonElementRef.value.id;
|
1549
|
+
}
|
1550
|
+
return;
|
1551
|
+
}
|
1552
|
+
if (((event.key === 'ArrowUp' && event.metaKey) ||
|
1553
|
+
event.key === 'Home' ||
|
1554
|
+
event.key === 'PageUp') &&
|
1555
|
+
this.#optionElementsNotHiddenIncludingSelectAll) {
|
1556
|
+
// Prevent page scroll. When filterable, also prevent the insertion point from
|
1557
|
+
// moving to the beginning of the field.
|
1558
|
+
event.preventDefault();
|
1559
|
+
const firstOption = [...this.#optionElementsNotHiddenIncludingSelectAll]
|
1560
|
+
.reverse()
|
1561
|
+
.findLast((option) => !option.disabled);
|
1562
|
+
if (firstOption) {
|
1563
|
+
this.#previouslyActiveOption = this.activeOption;
|
1564
|
+
this.activeOption.privateIsEditActive = false;
|
1565
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1566
|
+
this.activeOption.privateActive = false;
|
1567
|
+
this.ariaActivedescendant = firstOption.id;
|
1568
|
+
firstOption.privateActive = true;
|
1569
|
+
firstOption.privateIsTooltipOpen = true;
|
1570
|
+
firstOption.scrollIntoView();
|
1571
|
+
}
|
1572
|
+
return;
|
1573
|
+
}
|
1574
|
+
if (((event.key === 'ArrowDown' && event.metaKey) ||
|
1575
|
+
event.key === 'End' ||
|
1576
|
+
event.key === 'PageDown') &&
|
1577
|
+
this.#optionElementsNotHiddenIncludingSelectAll) {
|
1578
|
+
// Prevent page scroll. When filterable, also prevent the insertion point from
|
1579
|
+
// moving to the end of the field.
|
1580
|
+
event.preventDefault();
|
1581
|
+
const lastOption = [
|
1582
|
+
...this.#optionElementsNotHiddenIncludingSelectAll,
|
1583
|
+
].findLast((option) => !option.disabled);
|
1584
|
+
if (this.isAddButtonVisible && this.#addButtonElementRef.value) {
|
1585
|
+
this.#previouslyActiveOption = this.activeOption;
|
1586
|
+
this.activeOption.privateIsEditActive = false;
|
1587
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1588
|
+
this.activeOption.privateActive = false;
|
1589
|
+
this.isAddButtonActive = true;
|
1590
|
+
this.ariaActivedescendant = this.#addButtonElementRef.value.id;
|
1591
|
+
// If `lastOption` isn't defined, we've reached the bottom.
|
1592
|
+
}
|
1593
|
+
else if (lastOption && this.activeOption) {
|
1594
|
+
this.#previouslyActiveOption = this.activeOption;
|
1595
|
+
this.activeOption.privateIsEditActive = false;
|
1596
|
+
this.activeOption.privateIsTooltipOpen = false;
|
1597
|
+
this.activeOption.privateActive = false;
|
1598
|
+
this.ariaActivedescendant = lastOption.id;
|
1599
|
+
lastOption.privateActive = true;
|
1600
|
+
lastOption.privateIsTooltipOpen = true;
|
1601
|
+
lastOption.scrollIntoView();
|
1602
|
+
}
|
1603
|
+
return;
|
1604
|
+
}
|
1605
|
+
}
|
1606
|
+
}
|
1607
|
+
#onDropdownClick(event) {
|
1608
|
+
if (this.disabled || this.readonly) {
|
1609
|
+
return;
|
1610
|
+
}
|
1611
|
+
if (this.#isEditingOrRemovingTag) {
|
1612
|
+
this.#isEditingOrRemovingTag = false;
|
1613
|
+
return;
|
1614
|
+
}
|
1615
|
+
if (event.target instanceof Node &&
|
1616
|
+
this.#editButtonElementRef.value?.contains(event.target)) {
|
1617
|
+
this.lastSelectedAndEnabledOption?.privateEdit();
|
1618
|
+
return;
|
1619
|
+
}
|
1620
|
+
const isFilterable = this.filterable || this.isFilterable;
|
1621
|
+
if (!this.#isSelectionViaSpaceOrEnter &&
|
1622
|
+
this.open &&
|
1623
|
+
(!isFilterable ||
|
1624
|
+
(event.target instanceof Element &&
|
1625
|
+
this.#primaryButtonElementRef.value?.contains(event.target)))) {
|
1626
|
+
this.open = false;
|
1627
|
+
return;
|
1628
|
+
}
|
1629
|
+
// `event.detail` is an integer set to the number of clicks. When it's zero,
|
1630
|
+
// the event most likely originated from an Enter press. And, if Dropdown is part
|
1631
|
+
// of a form, Enter should submit the form instead of opening Dropdown.
|
1632
|
+
if (event.detail !== 0) {
|
1633
|
+
this.open = true;
|
1634
|
+
// If Dropdown was opened because its primary button or `<input>` were clicked,
|
1635
|
+
// then Dropdown will already have focus. But if something else was clicked, like
|
1636
|
+
// the padding around Dropdown or a Tag, then it won't. So we focus it manually.
|
1637
|
+
this.focus();
|
1638
|
+
return;
|
1639
|
+
}
|
1640
|
+
}
|
1641
|
+
#onDropdownMousedown(event) {
|
1642
|
+
// Retain focus if anything inside Dropdown is about to be clicked, like the
|
1643
|
+
// padding around the component. Clicking the padding will move focus to
|
1644
|
+
// `document.body`, which is not what the user expects.
|
1645
|
+
//
|
1646
|
+
// Having to exclude tags is unfortunate because clicking on the tag's label or
|
1647
|
+
// padding shouldn't cause the input to lose focus. The trouble is we don't know
|
1648
|
+
// it if is the Tag's removal button that's being clicked. And if it is we have
|
1649
|
+
// to allow it to receive focus.
|
1650
|
+
const isFilterable = this.filterable || this.isFilterable;
|
1651
|
+
const isTag = event.target instanceof Tag;
|
1652
|
+
if (isFilterable && !isTag) {
|
1653
|
+
// If input field is about to be clicked, canceling the event would prevent the
|
1654
|
+
// insertion point from moving inside the input.
|
1655
|
+
if (event.target !== this.#inputElementRef.value) {
|
1656
|
+
event.preventDefault();
|
1657
|
+
this.focus();
|
1658
|
+
}
|
1659
|
+
}
|
1660
|
+
else if (!isTag) {
|
1661
|
+
event.preventDefault();
|
1662
|
+
}
|
1663
|
+
}
|
1664
|
+
#onEditButtonClick() {
|
1665
|
+
this.open = false;
|
1666
|
+
}
|
1667
|
+
#onInputBlur() {
|
1668
|
+
this.isCommunicateItemCountToScreenreaders = false;
|
1669
|
+
this.isInputTooltipOpen = false;
|
1670
|
+
}
|
1671
|
+
#onInputFocus() {
|
1672
|
+
this.#inputElementRef.value?.select();
|
1673
|
+
this.isInputTooltipOpen = true;
|
1674
|
+
}
|
1675
|
+
async #onInputInput(event) {
|
1676
|
+
// Allowing the event to propagate would break things for consumers or at least
|
1677
|
+
// confuse them. They expect "input" events only when an option is selected or
|
1678
|
+
// deselected.
|
1679
|
+
event.stopPropagation();
|
1680
|
+
this.open = true;
|
1681
|
+
if (this.#inputElementRef.value) {
|
1682
|
+
this.inputValue = this.#inputElementRef.value.value;
|
1683
|
+
}
|
1684
|
+
if (this.multiple && this.#inputElementRef.value?.value !== '') {
|
1685
|
+
this.isFiltering = true;
|
1686
|
+
}
|
1687
|
+
else if (this.multiple) {
|
1688
|
+
this.isFiltering = false;
|
1689
|
+
}
|
1690
|
+
else if (this.#inputElementRef.value?.value !== '' &&
|
1691
|
+
this.#inputElementRef.value?.value !==
|
1692
|
+
this.lastSelectedAndEnabledOption?.label) {
|
1693
|
+
this.isFiltering = true;
|
1694
|
+
this.isShowSingleSelectIcon = false;
|
1695
|
+
}
|
1696
|
+
else {
|
1697
|
+
this.isFiltering = false;
|
1698
|
+
this.isShowSingleSelectIcon = false;
|
1699
|
+
}
|
1700
|
+
let options;
|
1701
|
+
if (this.#inputElementRef.value) {
|
1702
|
+
this.isAddButtonVisible =
|
1703
|
+
this.addButton &&
|
1704
|
+
this.#inputElementRef.value?.value.trim().length > 0 &&
|
1705
|
+
!this.#optionElements.some(({ label }) => {
|
1706
|
+
// Design doesn't know of such a case. But it's possible that case sensitivity
|
1707
|
+
// matters to some consumers. Ignoring case sensitivity at the outset is the
|
1708
|
+
// best way to smoke that case out.
|
1709
|
+
return (this.#inputElementRef.value &&
|
1710
|
+
label?.toLowerCase() ===
|
1711
|
+
this.#inputElementRef.value.value.toLowerCase().trim());
|
1712
|
+
});
|
1713
|
+
try {
|
1714
|
+
// It would be convenient for consumers if we passed an array of options
|
1715
|
+
// as the second argument. The problem is consumers fetch and render new
|
1716
|
+
// options when filtering. So the array will become stale.
|
1717
|
+
options = await this.filter(this.#inputElementRef.value.value);
|
1718
|
+
// eslint-disable-next-line no-empty
|
1719
|
+
}
|
1720
|
+
catch { }
|
1721
|
+
}
|
1722
|
+
if (options) {
|
1723
|
+
for (const option of this.#optionElements) {
|
1724
|
+
option.hidden = !options.includes(option);
|
1725
|
+
}
|
1726
|
+
}
|
1727
|
+
this.isCommunicateItemCountToScreenreaders = true;
|
1728
|
+
if (this.#optionElementsNotHidden) {
|
1729
|
+
this.itemCount = this.isAddButtonVisible
|
1730
|
+
? this.#optionElementsNotHidden.length + 1
|
1731
|
+
: this.#optionElementsNotHidden.length;
|
1732
|
+
}
|
1733
|
+
this.hasNoMatchingOptions =
|
1734
|
+
this.#optionElementsNotHidden?.length === 0 ? true : false;
|
1735
|
+
if (this.hasNoMatchingOptions) {
|
1736
|
+
// 1. Options are "One", "Two".
|
1737
|
+
// 2. Filter query is "".
|
1738
|
+
// 3. "One" is active.
|
1739
|
+
// 4. Filter query is now "three".
|
1740
|
+
// 5. The Add button is activated.
|
1741
|
+
if (this.addButton) {
|
1742
|
+
this.isAddButtonActive = true;
|
1743
|
+
if (this.activeOption && this.#addButtonElementRef.value) {
|
1744
|
+
this.#previouslyActiveOption = this.activeOption;
|
1745
|
+
this.activeOption.privateActive = false;
|
1746
|
+
this.ariaActivedescendant = this.#addButtonElementRef.value.id;
|
1747
|
+
}
|
1748
|
+
// 1. Options are "One", "Two".
|
1749
|
+
// 2. Filter query is "o".
|
1750
|
+
// 3. "One" is active.
|
1751
|
+
// 4. Filter query is now "three".
|
1752
|
+
// 5. "One" is deactivated.
|
1753
|
+
}
|
1754
|
+
else {
|
1755
|
+
if (this.activeOption) {
|
1756
|
+
this.#previouslyActiveOption = this.activeOption;
|
1757
|
+
this.activeOption.privateActive = false;
|
1758
|
+
}
|
1759
|
+
this.ariaActivedescendant = '';
|
1760
|
+
}
|
1761
|
+
return;
|
1762
|
+
}
|
1763
|
+
const firstEnabledAndVisibleOption = this.#optionElementsNotHidden?.find(({ disabled }) => !disabled);
|
1764
|
+
// 1. Options are "One", "Two".
|
1765
|
+
// 2. Filter query is "o".
|
1766
|
+
// 3. Previous active option is "Two".
|
1767
|
+
// 4. Add button is active.
|
1768
|
+
// 5. Filter query is now "".
|
1769
|
+
// 6. Add button is filtered out.
|
1770
|
+
// 7. "Two" is reactivated.
|
1771
|
+
if (this.isAddButtonActive &&
|
1772
|
+
!this.isAddButtonVisible &&
|
1773
|
+
this.#previouslyActiveOption &&
|
1774
|
+
!this.#previouslyActiveOption.hidden &&
|
1775
|
+
!this.#previouslyActiveOption.disabled) {
|
1776
|
+
this.isAddButtonActive = false;
|
1777
|
+
this.#previouslyActiveOption.privateActive = true;
|
1778
|
+
this.ariaActivedescendant = this.#previouslyActiveOption.id;
|
1779
|
+
return;
|
1780
|
+
}
|
1781
|
+
// 1. Options are "One", "Two", "Three", "Four".
|
1782
|
+
// 2. Filter query is "o".
|
1783
|
+
// 2. Previous active option is "Four".
|
1784
|
+
// 4. Filter query is now "ne".
|
1785
|
+
// 5. The Add button is filtered out.
|
1786
|
+
// 6. "Four" is filtered out.
|
1787
|
+
// 7. "One" is activated.
|
1788
|
+
if (this.isAddButtonActive &&
|
1789
|
+
!this.isAddButtonVisible &&
|
1790
|
+
firstEnabledAndVisibleOption) {
|
1791
|
+
firstEnabledAndVisibleOption.privateActive = true;
|
1792
|
+
this.isAddButtonActive = false;
|
1793
|
+
this.ariaActivedescendant = firstEnabledAndVisibleOption.id;
|
1794
|
+
return;
|
1795
|
+
}
|
1796
|
+
// 1. Options are "ABC", "AB", "A".
|
1797
|
+
// 2. Previous active option is "AB".
|
1798
|
+
// 3. Filter query is "abd". No active option.
|
1799
|
+
// 4. Filter query is now "ab".
|
1800
|
+
// 5. "AB" is activated.
|
1801
|
+
//
|
1802
|
+
// Or:
|
1803
|
+
//
|
1804
|
+
// 1. Options are "ABC", "AB", "A".
|
1805
|
+
// 2. Previous active option is "AB".
|
1806
|
+
// 3. Filter query is "".
|
1807
|
+
// 4. Active option is "A"
|
1808
|
+
// 5. Filter query is now "ab".
|
1809
|
+
// 6. "A" is filtered out.
|
1810
|
+
// 7. "AB" is activated.
|
1811
|
+
if ((!this.activeOption ||
|
1812
|
+
this.activeOption?.hidden ||
|
1813
|
+
this.activeOption?.disabled) &&
|
1814
|
+
this.#previouslyActiveOption &&
|
1815
|
+
!this.#previouslyActiveOption.hidden &&
|
1816
|
+
!this.#previouslyActiveOption.disabled) {
|
1817
|
+
const previouslyActiveOption = this.#previouslyActiveOption;
|
1818
|
+
if (this.activeOption) {
|
1819
|
+
this.#previouslyActiveOption = this.activeOption;
|
1820
|
+
this.#previouslyActiveOption.privateActive = false;
|
1821
|
+
}
|
1822
|
+
previouslyActiveOption.privateActive = true;
|
1823
|
+
this.ariaActivedescendant = previouslyActiveOption.id;
|
1824
|
+
return;
|
1825
|
+
}
|
1826
|
+
// 1. Options are "ABCD", "ABC", "AB", "A".
|
1827
|
+
// 2. "ABCD" is disabled.
|
1828
|
+
// 3. Previous active option is "AB".
|
1829
|
+
// 4. Active option is "A".
|
1830
|
+
// 5. Filter query was "".
|
1831
|
+
// 6. Filter query is now "abc".
|
1832
|
+
// 7. "A" is now hidden.
|
1833
|
+
// 8. "AB" is now hidden.
|
1834
|
+
// 9. "ABC" is activated.
|
1835
|
+
if (this.activeOption?.hidden && firstEnabledAndVisibleOption) {
|
1836
|
+
this.#previouslyActiveOption = this.activeOption;
|
1837
|
+
this.activeOption.privateActive = false;
|
1838
|
+
this.ariaActivedescendant = firstEnabledAndVisibleOption.id;
|
1839
|
+
firstEnabledAndVisibleOption.privateActive = true;
|
1840
|
+
return;
|
1841
|
+
}
|
1842
|
+
}
|
1843
|
+
#onInputKeydown(event) {
|
1844
|
+
// Deselecting an option the user can't see ain't good. So they're filtered out.
|
1845
|
+
// As the user deselects options, ones previously overflowing will be become
|
1846
|
+
// visible and thus deselectable using Backspace.
|
1847
|
+
const lastSelectedAndNotOverflowingOption = this.selectedAndEnabledOptions.findLast((_, index) => index <= this.tagOverflowLimit - 1);
|
1848
|
+
if (lastSelectedAndNotOverflowingOption &&
|
1849
|
+
event.key === 'Backspace' &&
|
1850
|
+
!event.metaKey &&
|
1851
|
+
this.multiple &&
|
1852
|
+
this.#inputElementRef.value &&
|
1853
|
+
this.#inputElementRef.value.selectionStart === 0) {
|
1854
|
+
this.#isEditingOrRemovingTag = true;
|
1855
|
+
lastSelectedAndNotOverflowingOption.selected = false;
|
1856
|
+
this.#isEditingOrRemovingTag = false;
|
1857
|
+
return;
|
1858
|
+
}
|
1859
|
+
const selectedAndNotOverflowingOptions = this.selectedAndEnabledOptions.filter((_, index) => index <= this.tagOverflowLimit - 1);
|
1860
|
+
if (lastSelectedAndNotOverflowingOption &&
|
1861
|
+
event.key === 'Backspace' &&
|
1862
|
+
event.metaKey &&
|
1863
|
+
this.multiple &&
|
1864
|
+
this.#inputElementRef.value &&
|
1865
|
+
this.#inputElementRef.value.selectionStart === 0) {
|
1866
|
+
this.#isEditingOrRemovingTag = true;
|
1867
|
+
for (const option of selectedAndNotOverflowingOptions) {
|
1868
|
+
option.selected = false;
|
1869
|
+
}
|
1870
|
+
this.#isEditingOrRemovingTag = false;
|
1871
|
+
return;
|
1872
|
+
}
|
1873
|
+
}
|
1874
|
+
#onInputResize() {
|
1875
|
+
if (this.#inputElementRef.value) {
|
1876
|
+
this.isInputOverflowing =
|
1877
|
+
this.#inputElementRef.value.scrollWidth >
|
1878
|
+
this.#inputElementRef.value.clientWidth;
|
1879
|
+
}
|
1880
|
+
}
|
1881
|
+
#onInternalLabelResize() {
|
1882
|
+
if (this.#internalLabelElementRef.value) {
|
1883
|
+
this.isInternalLabelOverflowing =
|
1884
|
+
this.#internalLabelElementRef.value.scrollWidth >
|
1885
|
+
this.#internalLabelElementRef.value.clientWidth;
|
1886
|
+
}
|
1887
|
+
}
|
1888
|
+
get #optionElements() {
|
1889
|
+
return (this.#defaultSlotElementRef.value
|
1890
|
+
?.assignedElements()
|
1891
|
+
.filter((element) => element instanceof DropdownOption) ?? []);
|
1892
|
+
}
|
1893
|
+
get #optionElementsIncludingSelectAll() {
|
1894
|
+
const options = this.#optionElements;
|
1895
|
+
if (this.#selectAllElementRef.value) {
|
1896
|
+
options.unshift(this.#selectAllElementRef.value);
|
1897
|
+
}
|
1898
|
+
return options;
|
1899
|
+
}
|
1900
|
+
get #optionElementsNotHidden() {
|
1901
|
+
return this.#defaultSlotElementRef.value
|
1902
|
+
?.assignedElements()
|
1903
|
+
.filter((element) => element instanceof DropdownOption && !element.hidden);
|
1904
|
+
}
|
1905
|
+
get #optionElementsNotHiddenIncludingSelectAll() {
|
1906
|
+
const options = this.#optionElementsNotHidden;
|
1907
|
+
if (this.#selectAllElementRef.value &&
|
1908
|
+
!this.#selectAllElementRef.value.hidden) {
|
1909
|
+
options?.unshift(this.#selectAllElementRef.value);
|
1910
|
+
}
|
1911
|
+
return options;
|
1912
|
+
}
|
1913
|
+
#onOptionsChange(event) {
|
1914
|
+
if (event.target instanceof DropdownOption) {
|
1915
|
+
event.target.selected = !event.target.selected;
|
1916
|
+
}
|
1917
|
+
if (event.target === this.#selectAllElementRef.value) {
|
1918
|
+
this.#selectAllOrNone();
|
1919
|
+
}
|
1920
|
+
if (this.#inputElementRef.value) {
|
1921
|
+
this.#inputElementRef.value.value = '';
|
1922
|
+
}
|
1923
|
+
this.inputValue = '';
|
1924
|
+
this.#unfilter();
|
1925
|
+
}
|
1926
|
+
#onOptionsClick(event) {
|
1927
|
+
if (event.target instanceof Element) {
|
1928
|
+
const option = event.target.closest('glide-core-dropdown-option');
|
1929
|
+
if (option instanceof DropdownOption && option.disabled) {
|
1930
|
+
return;
|
1931
|
+
}
|
1932
|
+
if (option instanceof DropdownOption && option.privateIsEditActive) {
|
1933
|
+
option.privateEdit();
|
1934
|
+
this.open = false;
|
1935
|
+
return;
|
1936
|
+
}
|
1937
|
+
if (option && !option.selected) {
|
1938
|
+
option.selected = true;
|
1939
|
+
this.#unfilter();
|
1940
|
+
this.open = false;
|
1941
|
+
// `this.isInputTooltipOpen` is set to `true` when the input field receives
|
1942
|
+
// focus and `false` when it loses focus. It's currently `true`. But the
|
1943
|
+
// user just selected an option and so knows what the option's label is. So
|
1944
|
+
// there's no need to show a tooltip.
|
1945
|
+
this.isInputTooltipOpen = false;
|
1946
|
+
// `undefined` is guarded against only to satisfy the type system. `option.label`
|
1947
|
+
// is guaranteed to be defined because it's required.
|
1948
|
+
if (this.#inputElementRef.value && option.label !== undefined) {
|
1949
|
+
this.#inputElementRef.value.value = option.label;
|
1950
|
+
this.inputValue = option.label;
|
1951
|
+
// One is subtracted to account for an apparent Chrome bug where `scrollWidth`
|
1952
|
+
// is off by one relative to `clientWidth` when they should be the same. It
|
1953
|
+
// happens whenever the input field was overflowing and now isn't.
|
1954
|
+
this.isInputOverflowing =
|
1955
|
+
this.#inputElementRef.value.scrollWidth - 1 >
|
1956
|
+
this.#inputElementRef.value.clientWidth;
|
1957
|
+
}
|
1958
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
1959
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
1960
|
+
return;
|
1961
|
+
}
|
1962
|
+
if (option?.selected && !this.multiple) {
|
1963
|
+
this.open = false;
|
1964
|
+
return;
|
1965
|
+
}
|
1966
|
+
}
|
1967
|
+
}
|
1968
|
+
#onOptionsDisabledChange(event) {
|
1969
|
+
if (this.multiple &&
|
1970
|
+
event.target instanceof DropdownOption &&
|
1971
|
+
event.target.disabled) {
|
1972
|
+
if (event.target.selected) {
|
1973
|
+
this.#value = this.#value.filter((_, index) => {
|
1974
|
+
return (index !==
|
1975
|
+
this.selectedAndEnabledOptions.indexOf(event.target));
|
1976
|
+
});
|
1977
|
+
this.selectedAndEnabledOptions = this.selectedAndEnabledOptions.filter((option) => option !== event.target);
|
1978
|
+
this.#setTagOverflowLimit();
|
1979
|
+
}
|
1980
|
+
if (event.target.privateActive) {
|
1981
|
+
event.target.privateActive = false;
|
1982
|
+
const firstEnabledOption = this.#optionElements.find(({ disabled }) => !disabled);
|
1983
|
+
if (firstEnabledOption) {
|
1984
|
+
firstEnabledOption.privateActive = true;
|
1985
|
+
this.#previouslyActiveOption = firstEnabledOption;
|
1986
|
+
this.ariaActivedescendant = firstEnabledOption.id;
|
1987
|
+
}
|
1988
|
+
}
|
1989
|
+
}
|
1990
|
+
else if (event.target instanceof DropdownOption &&
|
1991
|
+
event.target.disabled) {
|
1992
|
+
this.selectedAndEnabledOptions = this.selectedAndEnabledOptions.filter((option) => option !== event.target);
|
1993
|
+
this.#value = this.lastSelectedAndEnabledOption?.value
|
1994
|
+
? [this.lastSelectedAndEnabledOption.value]
|
1995
|
+
: [];
|
1996
|
+
if (event.target.privateActive) {
|
1997
|
+
event.target.privateActive = false;
|
1998
|
+
const firstEnabledOption = this.#optionElements.find(({ disabled }) => !disabled);
|
1999
|
+
if (firstEnabledOption) {
|
2000
|
+
firstEnabledOption.privateActive = true;
|
2001
|
+
this.#previouslyActiveOption = firstEnabledOption;
|
2002
|
+
this.ariaActivedescendant = firstEnabledOption.id;
|
2003
|
+
}
|
2004
|
+
}
|
2005
|
+
if (this.#inputElementRef.value) {
|
2006
|
+
this.#inputElementRef.value.value =
|
2007
|
+
this.lastSelectedAndEnabledOption?.label ?? '';
|
2008
|
+
this.inputValue = this.lastSelectedAndEnabledOption?.label ?? '';
|
2009
|
+
// One is subtracted to account for an apparent Chrome bug where `scrollWidth`
|
2010
|
+
// is off by one relative to `clientWidth` when they should be the same. It
|
2011
|
+
// happens whenever the input field was overflowing and now isn't.
|
2012
|
+
this.isInputOverflowing =
|
2013
|
+
this.#inputElementRef.value.scrollWidth - 1 >
|
2014
|
+
this.#inputElementRef.value.clientWidth;
|
2015
|
+
}
|
2016
|
+
for (const option of this.#optionElements) {
|
2017
|
+
if (option.selected && option !== event.target) {
|
2018
|
+
// When Dropdown is single-select, a Dropdown Option should only appear selected
|
2019
|
+
// when it's the last selected option because only the `value` of the last selected
|
2020
|
+
// option will be included in Dropdown's `value`. And what the user sees as
|
2021
|
+
// selected should always be the same as what's submitted with the form.
|
2022
|
+
//
|
2023
|
+
// Dropdown Options determine whether they're the last selected option (and thus
|
2024
|
+
// whether to show themselves as selected using a checkmark) via their internal
|
2025
|
+
// `lastSelectedAndEnabledOption` getter.
|
2026
|
+
//
|
2027
|
+
// The previously last selected option may have been the one that was disabled
|
2028
|
+
// and another selected option may be enabled. But the enabled option won't know
|
2029
|
+
// it's now the last selected option. So we force selected options to rerender.
|
2030
|
+
option.requestUpdate();
|
2031
|
+
}
|
2032
|
+
}
|
2033
|
+
}
|
2034
|
+
else if (this.multiple &&
|
2035
|
+
event.target instanceof DropdownOption &&
|
2036
|
+
event.target.selected) {
|
2037
|
+
if (event.target.value) {
|
2038
|
+
this.#value.push(event.target.value);
|
2039
|
+
}
|
2040
|
+
this.selectedAndEnabledOptions = [
|
2041
|
+
...this.selectedAndEnabledOptions,
|
2042
|
+
event.target,
|
2043
|
+
];
|
2044
|
+
this.#setTagOverflowLimit();
|
2045
|
+
}
|
2046
|
+
else if (event.target instanceof DropdownOption &&
|
2047
|
+
event.target.selected &&
|
2048
|
+
// `undefined` is guarded against only to satisfy the type system.
|
2049
|
+
// `event.target.label` is guaranteed to be defined because it's required.
|
2050
|
+
event.target.label !== undefined) {
|
2051
|
+
this.selectedAndEnabledOptions = [
|
2052
|
+
...this.selectedAndEnabledOptions,
|
2053
|
+
event.target,
|
2054
|
+
];
|
2055
|
+
this.#value =
|
2056
|
+
event.target === this.lastSelectedAndEnabledOption && event.target.value
|
2057
|
+
? [event.target.value]
|
2058
|
+
: [];
|
2059
|
+
for (const option of this.#optionElements) {
|
2060
|
+
if (option.selected && option !== event.target) {
|
2061
|
+
// When Dropdown is single-select, an option should only appear selected when it's
|
2062
|
+
// the last selected one because only the `value` of the last selected option will
|
2063
|
+
// be included in Dropdown's `value`. And what the user sees as selected should
|
2064
|
+
// always be the same as what's submitted with the form.
|
2065
|
+
//
|
2066
|
+
// Dropdown Options determine whether they're the last selected option (and thus
|
2067
|
+
// whether to show themselves as selected using a checkmark) via their internal
|
2068
|
+
// `lastSelectedAndEnabledOption` getter.
|
2069
|
+
//
|
2070
|
+
// What was previously the last selected and enabled option may no longer be the
|
2071
|
+
// last. But it won't know that because it doesn't know another option was enabled.
|
2072
|
+
// So we force selected options to rerender.
|
2073
|
+
option.requestUpdate();
|
2074
|
+
}
|
2075
|
+
}
|
2076
|
+
if (this.#inputElementRef.value) {
|
2077
|
+
this.#inputElementRef.value.value = event.target.label;
|
2078
|
+
this.inputValue = event.target.label;
|
2079
|
+
// One is subtracted to account for an apparent Chrome bug where `scrollWidth`
|
2080
|
+
// is off by one relative to `clientWidth` when they should be the same. It
|
2081
|
+
// happens whenever the input field was overflowing and now isn't.
|
2082
|
+
this.isInputOverflowing =
|
2083
|
+
this.#inputElementRef.value.scrollWidth - 1 >
|
2084
|
+
this.#inputElementRef.value.clientWidth;
|
2085
|
+
}
|
2086
|
+
}
|
2087
|
+
}
|
2088
|
+
#onOptionsEditableChange() {
|
2089
|
+
// Dropdown doesn't know to rerender when an option's `editable` property
|
2090
|
+
// has changed. But it needs to rerender to show or hide its edit button
|
2091
|
+
// or else to set or unset its Tags as editable in the case of multiselect.
|
2092
|
+
// So a rerender is forced.
|
2093
|
+
this.requestUpdate();
|
2094
|
+
}
|
2095
|
+
// Options don't receive focus normally but can receive programmatic focus from
|
2096
|
+
// screen readers.
|
2097
|
+
#onOptionsFocusin(event) {
|
2098
|
+
if (event.target instanceof DropdownOption) {
|
2099
|
+
if (this.activeOption) {
|
2100
|
+
this.activeOption.privateActive = false;
|
2101
|
+
}
|
2102
|
+
event.target.privateActive = true;
|
2103
|
+
this.#previouslyActiveOption = event.target;
|
2104
|
+
}
|
2105
|
+
}
|
2106
|
+
#onOptionsLabelChange() {
|
2107
|
+
if (this.selectedAndEnabledOptions.length > 0) {
|
2108
|
+
if (this.multiple) {
|
2109
|
+
// The option's label has changed and is reactive. But it's a separate component.
|
2110
|
+
// So Dropdown won't know to rerender its tags unless we tell it to.
|
2111
|
+
this.requestUpdate();
|
2112
|
+
}
|
2113
|
+
else if ((this.filterable || this.isFilterable) &&
|
2114
|
+
this.#inputElementRef.value &&
|
2115
|
+
this.lastSelectedAndEnabledOption?.label) {
|
2116
|
+
this.#inputElementRef.value.value =
|
2117
|
+
this.lastSelectedAndEnabledOption.label;
|
2118
|
+
this.inputValue = this.lastSelectedAndEnabledOption.label;
|
2119
|
+
this.isInputOverflowing =
|
2120
|
+
this.#inputElementRef.value.scrollWidth >
|
2121
|
+
this.#inputElementRef.value.clientWidth;
|
2122
|
+
}
|
2123
|
+
else {
|
2124
|
+
// The option's label has changed and is reactive. But it's a separate component.
|
2125
|
+
// So Dropdown won't know to rerun its `internalLabel` getter and rerender unless
|
2126
|
+
// we tell it to.
|
2127
|
+
this.requestUpdate();
|
2128
|
+
}
|
2129
|
+
}
|
2130
|
+
}
|
2131
|
+
#onOptionsMousedown(event) {
|
2132
|
+
// Keep focus on the input so the user can continue filtering while selecting
|
2133
|
+
// options.
|
2134
|
+
if (this.filterable || this.isFilterable) {
|
2135
|
+
event.preventDefault();
|
2136
|
+
}
|
2137
|
+
}
|
2138
|
+
#onOptionsMouseover(event) {
|
2139
|
+
if (event.target instanceof DropdownOption &&
|
2140
|
+
this.#optionElementsNotHiddenIncludingSelectAll) {
|
2141
|
+
if (event.target.disabled) {
|
2142
|
+
return;
|
2143
|
+
}
|
2144
|
+
if (this.activeOption) {
|
2145
|
+
this.#previouslyActiveOption = this.activeOption;
|
2146
|
+
// The currently active option may have been arrowed to and its tooltip forced
|
2147
|
+
// open. Normally, its tooltip would be closed when the user arrows again. But
|
2148
|
+
// now the user is using a mouse. So we force it closed.
|
2149
|
+
this.activeOption.privateIsTooltipOpen = false;
|
2150
|
+
this.activeOption.privateActive = false;
|
2151
|
+
}
|
2152
|
+
this.ariaActivedescendant = event.target.id;
|
2153
|
+
this.isAddButtonActive = false;
|
2154
|
+
event.target.privateActive = true;
|
2155
|
+
event.target.privateIsEditActive = false;
|
2156
|
+
}
|
2157
|
+
}
|
2158
|
+
#onOptionsSelectedChange(event) {
|
2159
|
+
if (this.#isSelectionFromSelectAllOrNone) {
|
2160
|
+
return;
|
2161
|
+
}
|
2162
|
+
if (this.#selectAllElementRef.value) {
|
2163
|
+
this.#selectAllElementRef.value.selected = this.areAllOptionsSelected;
|
2164
|
+
}
|
2165
|
+
if (event.target instanceof DropdownOption) {
|
2166
|
+
// It's possible the option was already selected but, for whatever reason, set as
|
2167
|
+
// selected again. So we use this to guard against adding its value twice to
|
2168
|
+
// `this.value`.
|
2169
|
+
const isOptionNewlySelected = this.selectedAndEnabledOptions.every((option) => option !== event.target);
|
2170
|
+
if (this.multiple) {
|
2171
|
+
if (event.target.selected) {
|
2172
|
+
if (event.target.disabled) {
|
2173
|
+
// We enable programmatically selected options for a couple reasons:
|
2174
|
+
//
|
2175
|
+
// One is because programmatic selection is a signal of developer intent that an
|
2176
|
+
// option is meant to appear to the user as selected. We only show to the user
|
2177
|
+
// selected options as selected when they are enabled. We do that because we think
|
2178
|
+
// users don't exactly know what it means for an option that appears as disabled to
|
2179
|
+
// also appear selected. Because disabled states often appear visually the same or
|
2180
|
+
// similar to read-only ones, it's not obvious that a disabled option won't be
|
2181
|
+
// submitted with the form.
|
2182
|
+
//
|
2183
|
+
//
|
2184
|
+
// Another is Dropdown's `value` setter, which calls this method indirectly when
|
2185
|
+
// it selects options programmatically. We have a few options if `value` is set
|
2186
|
+
// programmatically to include the value of a disabled option: we can throw, remove
|
2187
|
+
// the value from `this.value`, or enable the option. Throwing can be disruptive if
|
2188
|
+
// errors aren't handled downstream, which they often aren't. So we avoid throwing
|
2189
|
+
// unless we have to. And removing the value would, in a small way, be a breach of
|
2190
|
+
// Dropdown's contract with consumers. So we enable it.
|
2191
|
+
event.target.disabled = false;
|
2192
|
+
}
|
2193
|
+
if (isOptionNewlySelected) {
|
2194
|
+
this.selectedAndEnabledOptions = [
|
2195
|
+
...this.selectedAndEnabledOptions,
|
2196
|
+
event.target,
|
2197
|
+
];
|
2198
|
+
if (!this.#isSelectionFromValueSetter) {
|
2199
|
+
this.#value = [...this.value, event.target.value];
|
2200
|
+
}
|
2201
|
+
}
|
2202
|
+
}
|
2203
|
+
else {
|
2204
|
+
if (!this.#isSelectionFromValueSetter) {
|
2205
|
+
this.#value = this.#value.filter((_, index) => {
|
2206
|
+
return (index !==
|
2207
|
+
this.selectedAndEnabledOptions.indexOf(event.target));
|
2208
|
+
});
|
2209
|
+
}
|
2210
|
+
this.selectedAndEnabledOptions =
|
2211
|
+
this.selectedAndEnabledOptions.filter((option) => option !== event.target);
|
2212
|
+
}
|
2213
|
+
}
|
2214
|
+
else if (!this.multiple) {
|
2215
|
+
if (event.target.selected) {
|
2216
|
+
if (event.target.disabled) {
|
2217
|
+
event.target.disabled = false;
|
2218
|
+
}
|
2219
|
+
if (isOptionNewlySelected) {
|
2220
|
+
this.selectedAndEnabledOptions = [
|
2221
|
+
...this.selectedAndEnabledOptions,
|
2222
|
+
event.target,
|
2223
|
+
];
|
2224
|
+
}
|
2225
|
+
for (const option of this.#optionElements) {
|
2226
|
+
if (option !== event.target) {
|
2227
|
+
option.selected = false;
|
2228
|
+
}
|
2229
|
+
}
|
2230
|
+
if (!this.#isSelectionFromValueSetter) {
|
2231
|
+
this.#value = [event.target.value];
|
2232
|
+
}
|
2233
|
+
}
|
2234
|
+
else {
|
2235
|
+
this.selectedAndEnabledOptions =
|
2236
|
+
this.selectedAndEnabledOptions.filter((option) => option !== event.target);
|
2237
|
+
if (!this.#isSelectionFromValueSetter) {
|
2238
|
+
this.#value = this.lastSelectedAndEnabledOption?.value
|
2239
|
+
? [this.lastSelectedAndEnabledOption.value]
|
2240
|
+
: [];
|
2241
|
+
}
|
2242
|
+
}
|
2243
|
+
}
|
2244
|
+
}
|
2245
|
+
if (this.multiple) {
|
2246
|
+
this.#setTagOverflowLimit();
|
2247
|
+
}
|
2248
|
+
}
|
2249
|
+
#onOptionsValueChange(event) {
|
2250
|
+
// A cleaner approach would be to return early if `event.target` isn't an instance
|
2251
|
+
// of DropdownOption. But doing so would create an untestable branch and thus
|
2252
|
+
// force less than full code coverage because `event.target` will always be an
|
2253
|
+
// instance of `DropdownOption`.
|
2254
|
+
//
|
2255
|
+
// This is also why `#optionElementsIncludingSelectAll` and
|
2256
|
+
// `#optionElementsNotHiddenIncludingSelectAll` return `undefined` instead of
|
2257
|
+
// always returning an empty array. The empty array branch would only exist to
|
2258
|
+
// appease the typesystem and so would never actually get hit, making full test
|
2259
|
+
// coverage impossible. Maybe there's a better way?
|
2260
|
+
if (event.target instanceof DropdownOption &&
|
2261
|
+
this.multiple &&
|
2262
|
+
event.target.selected &&
|
2263
|
+
event.detail.new) {
|
2264
|
+
// There shouldn't be duplicate values. But this will fall short if there are.
|
2265
|
+
// Both instances of the value will be removed from `this.#value` when, strictly
|
2266
|
+
// speaking, only one of them should. Knowing which to remove would require storing
|
2267
|
+
// some state in a map and probably isn't worth the trouble.
|
2268
|
+
this.#value = this.value.map((value) => {
|
2269
|
+
return value === event.detail.old ? event.detail.new : value;
|
2270
|
+
});
|
2271
|
+
}
|
2272
|
+
else if (event.target instanceof DropdownOption && this.multiple) {
|
2273
|
+
this.#value = this.value.filter((value) => {
|
2274
|
+
return value !== event.detail.old;
|
2275
|
+
});
|
2276
|
+
}
|
2277
|
+
else if (event.target instanceof DropdownOption) {
|
2278
|
+
this.#value = event.detail.new ? [event.detail.new] : [];
|
2279
|
+
}
|
2280
|
+
}
|
2281
|
+
#onPrimaryButtonFocusin() {
|
2282
|
+
this.isInternalLabelTooltipOpen = true;
|
2283
|
+
}
|
2284
|
+
#onPrimaryButtonFocusout() {
|
2285
|
+
this.isInternalLabelTooltipOpen = false;
|
2286
|
+
}
|
2287
|
+
#onTagEdit() {
|
2288
|
+
this.#isEditingOrRemovingTag = true;
|
2289
|
+
this.open = false;
|
2290
|
+
}
|
2291
|
+
async #onTagRemove(option) {
|
2292
|
+
this.#isEditingOrRemovingTag = true;
|
2293
|
+
option.selected = false;
|
2294
|
+
const tags = this.#tagsElementRef.value?.querySelectorAll('glide-core-tag');
|
2295
|
+
if (tags && this.selectedAndEnabledOptions.length > 0) {
|
2296
|
+
const removedTagIndex = [...tags].findIndex((tag) => tag.dataset.id === option.id);
|
2297
|
+
// The tag to the right of the one removed unless it was the rightmost tag.
|
2298
|
+
// Otherwise the tag to the left.
|
2299
|
+
const tagToFocus = tags[removedTagIndex < tags.length - 1
|
2300
|
+
? removedTagIndex + 1
|
2301
|
+
: removedTagIndex - 1];
|
2302
|
+
// The tag to be focused may be one that's currently hidden. So we wait for it
|
2303
|
+
// to become visible before trying to focus it.
|
2304
|
+
await this.updateComplete;
|
2305
|
+
// Browsers have a quirk where, if focus is moved during a "keydown" event from
|
2306
|
+
// an Enter press, the "click" event that follows will be transferred to the now-
|
2307
|
+
// focused element. Thus we wait a tick, until the "click" event has passed,
|
2308
|
+
// before moving focus.
|
2309
|
+
//
|
2310
|
+
// Without a timeout, two tags will be removed by a single Enter press: the
|
2311
|
+
// previously focused Tag and the now-focused one. The previous because of the
|
2312
|
+
// "edit" event dispatched by Tag on "keydown". And the now-focused one because
|
2313
|
+
// of the "edit" event dispatched by Tag on "click".
|
2314
|
+
setTimeout(() => {
|
2315
|
+
tagToFocus?.focus();
|
2316
|
+
// Because the tag has been removed via `option.selected = false` above and focus
|
2317
|
+
// isn't moved until after the click, a "click" event will never be dispatched.
|
2318
|
+
// Otherwise, we could reset `#isEditingOrRemovingTag` in `#onDropdownClick()`.
|
2319
|
+
this.#isEditingOrRemovingTag = false;
|
2320
|
+
});
|
2321
|
+
}
|
2322
|
+
else {
|
2323
|
+
setTimeout(() => {
|
2324
|
+
this.focus();
|
2325
|
+
this.#isEditingOrRemovingTag = false;
|
2326
|
+
});
|
2327
|
+
}
|
2328
|
+
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
2329
|
+
this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
|
2330
|
+
}
|
2331
|
+
#onTooltipToggle(event) {
|
2332
|
+
// Dropdown dispatches its own "toggle" event. Letting Tooltip's "toggle"
|
2333
|
+
// propagate would mean that consumers listening for the event on Dropdown
|
2334
|
+
// would have to filter it out when it comes from Tooltip. But first they'll
|
2335
|
+
// probably file a bug. It's likely no consumer will be interested in knowing
|
2336
|
+
// when this component's Tooltips are open. So it's probably best to stop the
|
2337
|
+
// events from propagating.
|
2338
|
+
event.stopPropagation();
|
2339
|
+
}
|
2340
|
+
#selectAddButton() {
|
2341
|
+
if (this.#inputElementRef.value) {
|
2342
|
+
this.dispatchEvent(new CustomEvent('add', {
|
2343
|
+
bubbles: true,
|
2344
|
+
composed: true,
|
2345
|
+
detail: this.#inputElementRef.value.value,
|
2346
|
+
}));
|
2347
|
+
this.#inputElementRef.value.value = '';
|
2348
|
+
this.inputValue = '';
|
2349
|
+
}
|
2350
|
+
const firstOption = this.#optionElements.at(0);
|
2351
|
+
if (firstOption) {
|
2352
|
+
firstOption.privateActive = true;
|
2353
|
+
firstOption.scrollIntoView();
|
2354
|
+
this.ariaActivedescendant = firstOption.id;
|
2355
|
+
}
|
2356
|
+
if (!this.multiple) {
|
2357
|
+
this.open = false;
|
2358
|
+
// `this.isInputTooltipOpen` is set to `true` when the input field receives
|
2359
|
+
// focus and `false` when it loses focus. It's currently `true`. But the
|
2360
|
+
// user just selected an option and so knows what the option's label is. So
|
2361
|
+
// there's no need to show a tooltip.
|
2362
|
+
this.isInputTooltipOpen = false;
|
2363
|
+
}
|
2364
|
+
this.#unfilter();
|
2365
|
+
this.focus();
|
2366
|
+
}
|
2367
|
+
#selectAllOrNone() {
|
2368
|
+
this.#isSelectionFromSelectAllOrNone = true;
|
2369
|
+
for (const option of this.#optionElements) {
|
2370
|
+
if (this.#selectAllElementRef.value?.selected &&
|
2371
|
+
!option.selected &&
|
2372
|
+
!option.disabled) {
|
2373
|
+
option.selected = true;
|
2374
|
+
}
|
2375
|
+
else if (!this.#selectAllElementRef.value?.selected &&
|
2376
|
+
option.selected) {
|
2377
|
+
option.selected = false;
|
2378
|
+
}
|
2379
|
+
}
|
2380
|
+
this.#isSelectionFromSelectAllOrNone = false;
|
2381
|
+
this.selectedAndEnabledOptions = this.#optionElements.filter((option) => {
|
2382
|
+
return option.selected && !option.disabled;
|
2383
|
+
});
|
2384
|
+
this.#value = this.selectedAndEnabledOptions.map(({ value }) => value);
|
2385
|
+
this.#setTagOverflowLimit();
|
2386
|
+
}
|
2387
|
+
async #setTagOverflowLimit() {
|
2388
|
+
// Flush pending state changes before checking for overflow.
|
2389
|
+
await this.updateComplete;
|
2390
|
+
if (this.#componentElementRef.value) {
|
2391
|
+
const isOverflowing = this.#componentElementRef.value.scrollWidth >
|
2392
|
+
this.#componentElementRef.value.clientWidth;
|
2393
|
+
if (isOverflowing && this.tagOverflowLimit > 1) {
|
2394
|
+
this.tagOverflowLimit = this.tagOverflowLimit - 1;
|
2395
|
+
// Wait for the update to complete. Then run through this logic again to see
|
2396
|
+
// if Dropdown is still overflowing. Rinse and repeat until there's no overflow.
|
2397
|
+
await this.updateComplete;
|
2398
|
+
this.#setTagOverflowLimit();
|
2399
|
+
}
|
2400
|
+
else if (!isOverflowing &&
|
2401
|
+
!this.#isOverflowTest &&
|
2402
|
+
this.tagOverflowLimit < this.selectedAndEnabledOptions.length) {
|
2403
|
+
// The limit increase may cause an overflow. But we won't know until we rerun this
|
2404
|
+
// method. If it does cause an overflow, the branch above will correct the overflow
|
2405
|
+
// when it calls this function again.
|
2406
|
+
//
|
2407
|
+
// `#isOverflowTest` is set so we don't wind up back in this branch after returning
|
2408
|
+
// to the branch above, creating a loop.
|
2409
|
+
this.#isOverflowTest = true;
|
2410
|
+
this.tagOverflowLimit = this.tagOverflowLimit + 1;
|
2411
|
+
this.#setTagOverflowLimit();
|
2412
|
+
}
|
2413
|
+
else {
|
2414
|
+
this.#isOverflowTest = false;
|
2415
|
+
}
|
2416
|
+
}
|
2417
|
+
}
|
2418
|
+
#show() {
|
2419
|
+
// `#show()` is called whenever `open` is set. And `open` can be set arbitrarily
|
2420
|
+
// by the consumer. Rather than guarding against calling `#show()` everywhere,
|
2421
|
+
// Floating UI is simply cleaned up every time `#show()` is called.
|
2422
|
+
this.#cleanUpFloatingUi?.();
|
2423
|
+
if (this.#optionElementsNotHidden) {
|
2424
|
+
this.itemCount = this.#optionElementsNotHidden.length;
|
2425
|
+
}
|
2426
|
+
// Set here, in addition to in `#onDefaultSlotChange()` because
|
2427
|
+
// `#onDefaultSlotChange()` isn't called if there are no options.
|
2428
|
+
this.hasNoAvailableOptions = this.#optionElements.length === 0;
|
2429
|
+
const firstEnabledOption = this.#optionElementsNotHiddenIncludingSelectAll?.find((option) => !option.disabled);
|
2430
|
+
// Done now instead of after Floating UI does its thing because the user may begin
|
2431
|
+
// arrowing immediately or may have arrowed to open Dropdown and accidentally
|
2432
|
+
// arrowed again.
|
2433
|
+
if (this.#previouslyActiveOption &&
|
2434
|
+
!this.#previouslyActiveOption.hidden &&
|
2435
|
+
!this.#previouslyActiveOption.disabled) {
|
2436
|
+
this.#previouslyActiveOption.privateActive = true;
|
2437
|
+
this.ariaActivedescendant = this.#previouslyActiveOption.id;
|
2438
|
+
// It's possible the consumer disabled the previously active option since Dropdown
|
2439
|
+
// was last open. If so, we land in this condition.
|
2440
|
+
}
|
2441
|
+
else if (firstEnabledOption) {
|
2442
|
+
firstEnabledOption.privateActive = true;
|
2443
|
+
this.ariaActivedescendant = firstEnabledOption.id;
|
2444
|
+
this.#previouslyActiveOption = firstEnabledOption;
|
2445
|
+
}
|
2446
|
+
if (this.#dropdownElementRef.value &&
|
2447
|
+
this.#optionsAndFeedbackElementRef.value) {
|
2448
|
+
this.#cleanUpFloatingUi = autoUpdate(this.#dropdownElementRef.value, this.#optionsAndFeedbackElementRef.value, () => {
|
2449
|
+
(async () => {
|
2450
|
+
if (this.#dropdownElementRef.value &&
|
2451
|
+
this.#optionsAndFeedbackElementRef.value) {
|
2452
|
+
const { x, y, placement } = await computePosition(this.#dropdownElementRef.value, this.#optionsAndFeedbackElementRef.value, {
|
2453
|
+
placement: 'bottom-start',
|
2454
|
+
middleware: [
|
2455
|
+
offset({
|
2456
|
+
mainAxis: Number.parseFloat(window
|
2457
|
+
.getComputedStyle(document.body)
|
2458
|
+
.getPropertyValue('--glide-core-spacing-base-xxs')) *
|
2459
|
+
Number.parseFloat(window.getComputedStyle(document.documentElement)
|
2460
|
+
.fontSize),
|
2461
|
+
}),
|
2462
|
+
flip(),
|
2463
|
+
],
|
2464
|
+
});
|
2465
|
+
this.#optionsAndFeedbackElementRef.value.dataset.placement =
|
2466
|
+
placement;
|
2467
|
+
Object.assign(this.#optionsAndFeedbackElementRef.value.style, {
|
2468
|
+
left: `${x}px`,
|
2469
|
+
top: `${y}px`,
|
2470
|
+
});
|
2471
|
+
this.#optionsAndFeedbackElementRef.value?.showPopover();
|
2472
|
+
}
|
2473
|
+
})();
|
2474
|
+
});
|
2475
|
+
}
|
2476
|
+
}
|
2477
|
+
#unfilter() {
|
2478
|
+
// It would be nice to clear the input field and `this.inputValue` in here
|
2479
|
+
// too. It would work for multiselect. But single-select calls this method
|
2480
|
+
// after a selection is made and sets the input field and `this.inputValue`
|
2481
|
+
// to the label of the selected option instead of clearing them.
|
2482
|
+
for (const option of this.#optionElements) {
|
2483
|
+
option.hidden = false;
|
2484
|
+
}
|
2485
|
+
this.isFiltering = false;
|
2486
|
+
this.isAddButtonActive = false;
|
2487
|
+
this.isAddButtonVisible = false;
|
2488
|
+
this.hasNoMatchingOptions = false;
|
2489
|
+
this.isShowSingleSelectIcon = Boolean(this.lastSelectedAndEnabledOption?.value);
|
2490
|
+
}
|
2491
|
+
};
|
2492
|
+
__decorate([
|
2493
|
+
property({ reflect: true }),
|
2494
|
+
required
|
2495
|
+
], Dropdown.prototype, "label", void 0);
|
2496
|
+
__decorate([
|
2497
|
+
property({ attribute: 'add-button', reflect: true, type: Boolean })
|
2498
|
+
], Dropdown.prototype, "addButton", void 0);
|
2499
|
+
__decorate([
|
2500
|
+
property({ reflect: true, type: Boolean })
|
2501
|
+
], Dropdown.prototype, "disabled", null);
|
2502
|
+
__decorate([
|
2503
|
+
property({ reflect: true, type: Boolean })
|
2504
|
+
], Dropdown.prototype, "filterable", null);
|
2505
|
+
__decorate([
|
2506
|
+
property({ attribute: 'hide-label', reflect: true, type: Boolean })
|
2507
|
+
], Dropdown.prototype, "hideLabel", void 0);
|
2508
|
+
__decorate([
|
2509
|
+
property({ reflect: true, type: Boolean })
|
2510
|
+
], Dropdown.prototype, "loading", void 0);
|
2511
|
+
__decorate([
|
2512
|
+
property({ reflect: true, useDefault: true })
|
2513
|
+
], Dropdown.prototype, "name", void 0);
|
2514
|
+
__decorate([
|
2515
|
+
property({ reflect: true, type: Boolean })
|
2516
|
+
], Dropdown.prototype, "open", null);
|
2517
|
+
__decorate([
|
2518
|
+
property({ reflect: true, useDefault: true })
|
2519
|
+
], Dropdown.prototype, "orientation", void 0);
|
2520
|
+
__decorate([
|
2521
|
+
property({ reflect: true })
|
2522
|
+
], Dropdown.prototype, "placeholder", void 0);
|
2523
|
+
__decorate([
|
2524
|
+
property()
|
2525
|
+
], Dropdown.prototype, "privateSplit", void 0);
|
2526
|
+
__decorate([
|
2527
|
+
property({ reflect: true, type: Boolean })
|
2528
|
+
], Dropdown.prototype, "readonly", void 0);
|
2529
|
+
__decorate([
|
2530
|
+
property({ attribute: 'select-all', reflect: true, type: Boolean })
|
2531
|
+
], Dropdown.prototype, "selectAll", void 0);
|
2532
|
+
__decorate([
|
2533
|
+
property({ reflect: true, type: Boolean })
|
2534
|
+
], Dropdown.prototype, "required", void 0);
|
2535
|
+
__decorate([
|
2536
|
+
property({ reflect: true, type: Boolean })
|
2537
|
+
], Dropdown.prototype, "multiple", null);
|
2538
|
+
__decorate([
|
2539
|
+
property({ reflect: true })
|
2540
|
+
], Dropdown.prototype, "tooltip", void 0);
|
2541
|
+
__decorate([
|
2542
|
+
property({ type: Array })
|
2543
|
+
], Dropdown.prototype, "value", null);
|
2544
|
+
__decorate([
|
2545
|
+
property({ reflect: true })
|
2546
|
+
], Dropdown.prototype, "variant", void 0);
|
2547
|
+
__decorate([
|
2548
|
+
property({ reflect: true })
|
2549
|
+
], Dropdown.prototype, "version", void 0);
|
2550
|
+
__decorate([
|
2551
|
+
state()
|
2552
|
+
], Dropdown.prototype, "ariaActivedescendant", void 0);
|
2553
|
+
__decorate([
|
2554
|
+
state()
|
2555
|
+
], Dropdown.prototype, "hasNoAvailableOptions", void 0);
|
2556
|
+
__decorate([
|
2557
|
+
state()
|
2558
|
+
], Dropdown.prototype, "hasNoMatchingOptions", void 0);
|
2559
|
+
__decorate([
|
2560
|
+
state()
|
2561
|
+
], Dropdown.prototype, "inputValue", void 0);
|
2562
|
+
__decorate([
|
2563
|
+
state()
|
2564
|
+
], Dropdown.prototype, "isAddButtonActive", void 0);
|
2565
|
+
__decorate([
|
2566
|
+
state()
|
2567
|
+
], Dropdown.prototype, "isAddButtonVisible", void 0);
|
2568
|
+
__decorate([
|
2569
|
+
state()
|
2570
|
+
], Dropdown.prototype, "isBlurring", void 0);
|
2571
|
+
__decorate([
|
2572
|
+
state()
|
2573
|
+
], Dropdown.prototype, "isCheckingValidity", void 0);
|
2574
|
+
__decorate([
|
2575
|
+
state()
|
2576
|
+
], Dropdown.prototype, "isCommunicateItemCountToScreenreaders", void 0);
|
2577
|
+
__decorate([
|
2578
|
+
state()
|
2579
|
+
], Dropdown.prototype, "isFilterable", void 0);
|
2580
|
+
__decorate([
|
2581
|
+
state()
|
2582
|
+
], Dropdown.prototype, "isFiltering", void 0);
|
2583
|
+
__decorate([
|
2584
|
+
state()
|
2585
|
+
], Dropdown.prototype, "isInputOverflowing", void 0);
|
2586
|
+
__decorate([
|
2587
|
+
state()
|
2588
|
+
], Dropdown.prototype, "isInputTooltipOpen", void 0);
|
2589
|
+
__decorate([
|
2590
|
+
state()
|
2591
|
+
], Dropdown.prototype, "isInternalLabelOverflowing", void 0);
|
2592
|
+
__decorate([
|
2593
|
+
state()
|
2594
|
+
], Dropdown.prototype, "isInternalLabelTooltipOpen", void 0);
|
2595
|
+
__decorate([
|
2596
|
+
state()
|
2597
|
+
], Dropdown.prototype, "isReportValidityOrSubmit", void 0);
|
2598
|
+
__decorate([
|
2599
|
+
state()
|
2600
|
+
], Dropdown.prototype, "isShowSingleSelectIcon", void 0);
|
2601
|
+
__decorate([
|
2602
|
+
state()
|
2603
|
+
], Dropdown.prototype, "itemCount", void 0);
|
2604
|
+
__decorate([
|
2605
|
+
state()
|
2606
|
+
], Dropdown.prototype, "selectedAndEnabledOptions", void 0);
|
2607
|
+
__decorate([
|
2608
|
+
state()
|
2609
|
+
], Dropdown.prototype, "tagOverflowLimit", void 0);
|
2610
|
+
__decorate([
|
2611
|
+
state()
|
2612
|
+
], Dropdown.prototype, "validityMessage", void 0);
|
2613
|
+
Dropdown = __decorate([
|
2614
|
+
customElement('glide-core-dropdown'),
|
2615
|
+
final
|
2616
|
+
], Dropdown);
|
2617
|
+
export default Dropdown;
|