@spfx-fast/sharepoint-ui 0.4.3 → 0.5.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/QUICKSTART.md +13 -128
- package/README.md +19 -26
- package/components.json +240 -5834
- package/dist/accordion.d.ts +11 -0
- package/dist/accordion.js +4 -0
- package/dist/alert.d.ts +3 -0
- package/dist/alert.js +4 -0
- package/dist/attachment-list.d.ts +15 -0
- package/dist/attachment-list.js +14 -0
- package/dist/avatar-group.d.ts +12 -0
- package/dist/avatar-group.js +7 -0
- package/dist/avatar.d.ts +6 -0
- package/dist/avatar.js +5 -0
- package/dist/badge.d.ts +3 -0
- package/dist/badge.js +4 -0
- package/dist/breadcrumb.d.ts +10 -0
- package/dist/breadcrumb.js +9 -0
- package/dist/button-group.d.ts +6 -0
- package/dist/button-group.js +4 -0
- package/dist/button.d.ts +3 -0
- package/dist/button.js +4 -0
- package/dist/card.d.ts +3 -0
- package/dist/card.js +4 -0
- package/dist/carousel.d.ts +11 -0
- package/dist/carousel.js +6 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.js +4 -0
- package/dist/combobox.d.ts +9 -0
- package/dist/combobox.js +6 -0
- package/dist/command-bar.d.ts +6 -0
- package/dist/command-bar.js +4 -0
- package/dist/copy-button.d.ts +7 -0
- package/dist/copy-button.js +19 -0
- package/dist/data-grid.d.ts +25 -0
- package/dist/data-grid.js +39 -0
- package/dist/date-range-picker.d.ts +12 -0
- package/dist/date-range-picker.js +4 -0
- package/dist/dialog.d.ts +9 -0
- package/dist/dialog.js +21 -0
- package/dist/divider.d.ts +3 -0
- package/dist/divider.js +4 -0
- package/dist/drawer.d.ts +6 -0
- package/dist/drawer.js +5 -0
- package/dist/empty-state.d.ts +7 -0
- package/dist/empty-state.js +5 -0
- package/dist/field.d.ts +15 -0
- package/dist/field.js +11 -0
- package/dist/file-dropzone.d.ts +15 -0
- package/dist/file-dropzone.js +21 -0
- package/dist/filter-bar.d.ts +9 -0
- package/dist/filter-bar.js +9 -0
- package/dist/image.d.ts +5 -0
- package/dist/image.js +4 -0
- package/dist/index.d.ts +68 -78
- package/dist/index.js +60 -68
- package/dist/input.d.ts +3 -0
- package/dist/input.js +4 -0
- package/dist/label.d.ts +3 -0
- package/dist/label.js +4 -0
- package/dist/link.d.ts +3 -0
- package/dist/link.js +9 -0
- package/dist/list-view.d.ts +29 -0
- package/dist/list-view.js +14 -0
- package/dist/list.d.ts +6 -0
- package/dist/list.js +5 -0
- package/dist/lookup-picker.d.ts +12 -0
- package/dist/lookup-picker.js +4 -0
- package/dist/menu.d.ts +6 -0
- package/dist/menu.js +4 -0
- package/dist/nav.d.ts +12 -0
- package/dist/nav.js +4 -0
- package/dist/pagination.d.ts +11 -0
- package/dist/pagination.js +5 -0
- package/dist/person-picker.d.ts +13 -0
- package/dist/person-picker.js +4 -0
- package/dist/persona.d.ts +7 -0
- package/dist/persona.js +5 -0
- package/dist/popover.d.ts +6 -0
- package/dist/popover.js +4 -0
- package/dist/progress-bar.d.ts +5 -0
- package/dist/progress-bar.js +5 -0
- package/dist/radio-group.d.ts +6 -0
- package/dist/radio-group.js +4 -0
- package/dist/radio.d.ts +3 -0
- package/dist/radio.js +4 -0
- package/dist/rating.d.ts +8 -0
- package/dist/rating.js +5 -0
- package/dist/react.d.ts +1 -4715
- package/dist/react.js +1 -7097
- package/dist/select.d.ts +3 -0
- package/dist/select.js +4 -0
- package/dist/sharepoint-field.d.ts +18 -0
- package/dist/sharepoint-field.js +54 -0
- package/dist/skeleton.d.ts +6 -0
- package/dist/skeleton.js +9 -0
- package/dist/spinner.d.ts +5 -0
- package/dist/spinner.js +4 -0
- package/dist/swatch-picker.d.ts +17 -0
- package/dist/swatch-picker.js +11 -0
- package/dist/switch.d.ts +3 -0
- package/dist/switch.js +4 -0
- package/dist/table.d.ts +6 -0
- package/dist/table.js +4 -0
- package/dist/tabs.d.ts +14 -0
- package/dist/tabs.js +30 -0
- package/dist/tag-group.d.ts +6 -0
- package/dist/tag-group.js +4 -0
- package/dist/tag-picker.d.ts +12 -0
- package/dist/tag-picker.js +4 -0
- package/dist/tag.d.ts +7 -0
- package/dist/tag.js +5 -0
- package/dist/text.d.ts +5 -0
- package/dist/text.js +4 -0
- package/dist/textarea.d.ts +3 -0
- package/dist/textarea.js +4 -0
- package/dist/theme.d.ts +15 -175
- package/dist/theme.js +36 -239
- package/dist/toast.d.ts +9 -0
- package/dist/toast.js +12 -0
- package/dist/tree.d.ts +13 -0
- package/dist/tree.js +9 -0
- package/dist/view-card.d.ts +8 -0
- package/dist/view-card.js +4 -0
- package/dist/view-switcher.d.ts +13 -0
- package/dist/view-switcher.js +7 -0
- package/dist/virtual-list.d.ts +11 -0
- package/dist/virtual-list.js +25 -0
- package/measurements.json +365 -0
- package/package.json +14 -46
- package/src/theme.css +125 -0
- package/AGENTS.md +0 -231
- package/dist/appearance.d.ts +0 -6
- package/dist/appearance.js +0 -217
- package/dist/elements/accordion.d.ts +0 -9
- package/dist/elements/accordion.js +0 -338
- package/dist/elements/alert.d.ts +0 -2
- package/dist/elements/alert.js +0 -86
- package/dist/elements/avatar-group-primitives.d.ts +0 -3
- package/dist/elements/avatar-group-primitives.js +0 -189
- package/dist/elements/avatar-group.d.ts +0 -2
- package/dist/elements/avatar-group.js +0 -110
- package/dist/elements/avatar.d.ts +0 -4
- package/dist/elements/avatar.js +0 -251
- package/dist/elements/badge.d.ts +0 -8
- package/dist/elements/badge.js +0 -182
- package/dist/elements/breadcrumb.d.ts +0 -4
- package/dist/elements/breadcrumb.js +0 -233
- package/dist/elements/button-group.d.ts +0 -1
- package/dist/elements/button-group.js +0 -20
- package/dist/elements/button-variants.d.ts +0 -6
- package/dist/elements/button-variants.js +0 -339
- package/dist/elements/button.d.ts +0 -5
- package/dist/elements/button.js +0 -233
- package/dist/elements/card-primitives.d.ts +0 -3
- package/dist/elements/card-primitives.js +0 -83
- package/dist/elements/card.d.ts +0 -5
- package/dist/elements/card.js +0 -213
- package/dist/elements/carousel.d.ts +0 -92
- package/dist/elements/carousel.js +0 -1115
- package/dist/elements/color-picker.d.ts +0 -24
- package/dist/elements/color-picker.js +0 -1004
- package/dist/elements/combobox.d.ts +0 -1
- package/dist/elements/combobox.js +0 -1012
- package/dist/elements/data-grid-model.d.ts +0 -120
- package/dist/elements/data-grid-model.js +0 -294
- package/dist/elements/data-grid.d.ts +0 -30
- package/dist/elements/data-grid.js +0 -1576
- package/dist/elements/date-picker.d.ts +0 -1
- package/dist/elements/date-picker.js +0 -486
- package/dist/elements/dialog.d.ts +0 -23
- package/dist/elements/dialog.js +0 -409
- package/dist/elements/divider.d.ts +0 -2
- package/dist/elements/divider.js +0 -66
- package/dist/elements/drawer.d.ts +0 -3
- package/dist/elements/drawer.js +0 -308
- package/dist/elements/field-composition.d.ts +0 -26
- package/dist/elements/field-composition.js +0 -133
- package/dist/elements/field.d.ts +0 -1
- package/dist/elements/field.js +0 -67
- package/dist/elements/file-upload.d.ts +0 -6
- package/dist/elements/file-upload.js +0 -211
- package/dist/elements/floating-position.d.ts +0 -36
- package/dist/elements/floating-position.js +0 -92
- package/dist/elements/image.d.ts +0 -28
- package/dist/elements/image.js +0 -38
- package/dist/elements/info.d.ts +0 -9
- package/dist/elements/info.js +0 -102
- package/dist/elements/input.d.ts +0 -5
- package/dist/elements/input.js +0 -255
- package/dist/elements/label.d.ts +0 -15
- package/dist/elements/label.js +0 -56
- package/dist/elements/link.d.ts +0 -1
- package/dist/elements/link.js +0 -92
- package/dist/elements/list.d.ts +0 -19
- package/dist/elements/list.js +0 -297
- package/dist/elements/menu.d.ts +0 -33
- package/dist/elements/menu.js +0 -1103
- package/dist/elements/message-bar.d.ts +0 -10
- package/dist/elements/message-bar.js +0 -327
- package/dist/elements/nav.d.ts +0 -32
- package/dist/elements/nav.js +0 -794
- package/dist/elements/overflow.d.ts +0 -10
- package/dist/elements/overflow.js +0 -218
- package/dist/elements/persona.d.ts +0 -3
- package/dist/elements/persona.js +0 -125
- package/dist/elements/popover.d.ts +0 -77
- package/dist/elements/popover.js +0 -840
- package/dist/elements/progress-bar.d.ts +0 -4
- package/dist/elements/progress-bar.js +0 -82
- package/dist/elements/radio-group.d.ts +0 -1
- package/dist/elements/radio-group.js +0 -303
- package/dist/elements/rating-display.d.ts +0 -2
- package/dist/elements/rating-display.js +0 -125
- package/dist/elements/rating.d.ts +0 -2
- package/dist/elements/rating.js +0 -206
- package/dist/elements/rich-text-editor.d.ts +0 -32
- package/dist/elements/rich-text-editor.js +0 -1367
- package/dist/elements/search.d.ts +0 -3
- package/dist/elements/search.js +0 -200
- package/dist/elements/select.d.ts +0 -6
- package/dist/elements/select.js +0 -291
- package/dist/elements/shared.d.ts +0 -22
- package/dist/elements/shared.js +0 -409
- package/dist/elements/sharepoint-field-basic-controls.d.ts +0 -1
- package/dist/elements/sharepoint-field-basic-controls.js +0 -14
- package/dist/elements/sharepoint-fields.d.ts +0 -29
- package/dist/elements/sharepoint-fields.js +0 -460
- package/dist/elements/skeleton.d.ts +0 -6
- package/dist/elements/skeleton.js +0 -91
- package/dist/elements/slider.d.ts +0 -1
- package/dist/elements/slider.js +0 -248
- package/dist/elements/spin-button.d.ts +0 -7
- package/dist/elements/spin-button.js +0 -499
- package/dist/elements/spinner.d.ts +0 -4
- package/dist/elements/spinner.js +0 -124
- package/dist/elements/swatch-picker.d.ts +0 -5
- package/dist/elements/swatch-picker.js +0 -363
- package/dist/elements/table.d.ts +0 -21
- package/dist/elements/table.js +0 -261
- package/dist/elements/tabs.d.ts +0 -1
- package/dist/elements/tabs.js +0 -276
- package/dist/elements/tag-picker.d.ts +0 -29
- package/dist/elements/tag-picker.js +0 -715
- package/dist/elements/tags.d.ts +0 -21
- package/dist/elements/tags.js +0 -421
- package/dist/elements/teaching-popover.d.ts +0 -32
- package/dist/elements/teaching-popover.js +0 -412
- package/dist/elements/text.d.ts +0 -9
- package/dist/elements/text.js +0 -75
- package/dist/elements/textarea.d.ts +0 -1
- package/dist/elements/textarea.js +0 -182
- package/dist/elements/toast.d.ts +0 -25
- package/dist/elements/toast.js +0 -337
- package/dist/elements/toaster.d.ts +0 -6
- package/dist/elements/toaster.js +0 -357
- package/dist/elements/toggle.d.ts +0 -2
- package/dist/elements/toggle.js +0 -246
- package/dist/elements/toolbar.d.ts +0 -12
- package/dist/elements/toolbar.js +0 -381
- package/dist/elements/tooltip.d.ts +0 -76
- package/dist/elements/tooltip.js +0 -527
- package/dist/elements/tree-model.d.ts +0 -19
- package/dist/elements/tree-model.js +0 -34
- package/dist/elements/tree.d.ts +0 -5
- package/dist/elements/tree.js +0 -273
- package/dist/elements/view-card.d.ts +0 -2
- package/dist/elements/view-card.js +0 -78
- package/dist/elements/virtual-list.d.ts +0 -41
- package/dist/elements/virtual-list.js +0 -369
- package/dist/elements.d.ts +0 -73
- package/dist/elements.js +0 -210
- package/dist/sharepoint-fields.d.ts +0 -135
- package/dist/sharepoint-fields.js +0 -402
- package/dist/virtualizer-hooks.d.ts +0 -80
- package/dist/virtualizer-hooks.js +0 -253
- package/scripts/generate-token-index.mjs +0 -43
- package/scripts/sync-component-catalog-signatures.mjs +0 -55
- package/scripts/validate-component-catalog.mjs +0 -44
- package/skills/sharepoint-ui/SKILL.md +0 -12
- package/skills/sharepoint-ui/references/choosing-components.md +0 -20
- package/skills/sharepoint-ui/references/sharepoint-patterns.md +0 -17
- package/tokens.json +0 -3777
|
@@ -1,1012 +0,0 @@
|
|
|
1
|
-
import { base, rememberNativeFormControlDefaults, stopHostAccessibleNameSync, syncHostAccessibleName } from "./shared.js";
|
|
2
|
-
export function defineCombobox() {
|
|
3
|
-
if (customElements.get("spui-combobox"))
|
|
4
|
-
return;
|
|
5
|
-
class SpuiCombobox extends HTMLElement {
|
|
6
|
-
static formAssociated = true;
|
|
7
|
-
static observedAttributes = ["label", "value", "selected-value", "selected-options", "default-selected-options", "multiselect", "disabled", "required", "required-message", "name", "placeholder", "allow-freeform", "read-only", "button-trigger", "open", "default-open", "clearable", "clear-label", "disable-auto-focus", "appearance", "size", "positioning", "inline-popup", "no-results", "remove-selected-label", "selected-label", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"];
|
|
8
|
-
internals;
|
|
9
|
-
observer;
|
|
10
|
-
positionEvents;
|
|
11
|
-
positionObserver;
|
|
12
|
-
observedPositionAnchor;
|
|
13
|
-
observedPositionListbox;
|
|
14
|
-
positionFrame;
|
|
15
|
-
instanceId = crypto.randomUUID();
|
|
16
|
-
currentValue = "";
|
|
17
|
-
selectedValueState = "";
|
|
18
|
-
selectedOptionsState = [];
|
|
19
|
-
buttonTriggerSlot;
|
|
20
|
-
fallbackButtonTrigger;
|
|
21
|
-
dirty = false;
|
|
22
|
-
selectionsDirty = false;
|
|
23
|
-
typing = false;
|
|
24
|
-
activeIndex = -1;
|
|
25
|
-
open = false;
|
|
26
|
-
inputSlot;
|
|
27
|
-
fallbackInput;
|
|
28
|
-
boundInput;
|
|
29
|
-
listboxElementValue;
|
|
30
|
-
formDisabled = false;
|
|
31
|
-
slotInputDefaults;
|
|
32
|
-
handleInput = (event) => {
|
|
33
|
-
const input = this.getInput();
|
|
34
|
-
if (!input)
|
|
35
|
-
return;
|
|
36
|
-
const changed = !this.open;
|
|
37
|
-
this.currentValue = input.value;
|
|
38
|
-
this.dirty = true;
|
|
39
|
-
const previousOption = this.activeOptionValue();
|
|
40
|
-
const query = input.value.trim().toLocaleLowerCase(this.lang || undefined);
|
|
41
|
-
const matchingOption = query ? this.optionElements().find(option => !this.isOptionDisabled(option) && (option.getAttribute("label") ?? option.textContent ?? "").trim().toLocaleLowerCase(this.lang || undefined).startsWith(query)) : undefined;
|
|
42
|
-
const options = this.filteredOptions();
|
|
43
|
-
this.activeIndex = matchingOption ? options.indexOf(matchingOption) : -1;
|
|
44
|
-
this.announceActiveOption(previousOption, event);
|
|
45
|
-
if (!this.hasAttribute("multiselect") && (this.selectedValueState || this.selectedOptionsState.length === 1) && (!query || !matchingOption)) {
|
|
46
|
-
this.selectedValueState = "";
|
|
47
|
-
this.selectedOptionsState = [];
|
|
48
|
-
this.selectionsDirty = true;
|
|
49
|
-
this.dispatchEvent(new CustomEvent("spui-change", {
|
|
50
|
-
detail: { event, value: input.value, selectedValue: undefined, optionText: undefined, selectedOptions: [] },
|
|
51
|
-
bubbles: true,
|
|
52
|
-
composed: true
|
|
53
|
-
}));
|
|
54
|
-
}
|
|
55
|
-
this.open = true;
|
|
56
|
-
this.updateOptions();
|
|
57
|
-
this.syncFormState();
|
|
58
|
-
this.notifyOpenChange(true, event, changed);
|
|
59
|
-
this.dispatchEvent(new CustomEvent("spui-input", { detail: { value: input.value }, bubbles: true, composed: true }));
|
|
60
|
-
};
|
|
61
|
-
handleInputClick = (event) => { if (!this.open)
|
|
62
|
-
this.openList(0, event); };
|
|
63
|
-
handleInputKeyDown = (event) => this.onKeyDown(event);
|
|
64
|
-
handleInputBlur = (event) => { window.setTimeout(() => { if (!this.matches(":focus-within"))
|
|
65
|
-
this.closeList(event); }, 0); };
|
|
66
|
-
onSlottedControlClick = (event) => {
|
|
67
|
-
const slotted = event.composedPath().find(node => node instanceof HTMLElement && node.parentElement === this);
|
|
68
|
-
if (slotted?.slot === "clear-button")
|
|
69
|
-
this.clearSelection(event);
|
|
70
|
-
else if (this.hasAttribute("button-trigger") && slotted?.slot === "button")
|
|
71
|
-
this.toggleOpen(event);
|
|
72
|
-
};
|
|
73
|
-
onSlottedTriggerKeyDown = (event) => {
|
|
74
|
-
if (this.hasAttribute("button-trigger") && event.composedPath().some(node => node instanceof HTMLElement && node.parentElement === this && node.slot === "button"))
|
|
75
|
-
this.onKeyDown(event);
|
|
76
|
-
};
|
|
77
|
-
get form() { return this.internals?.form ?? null; }
|
|
78
|
-
constructor() {
|
|
79
|
-
super();
|
|
80
|
-
try {
|
|
81
|
-
this.internals = this.attachInternals();
|
|
82
|
-
}
|
|
83
|
-
catch { /* Keep listbox interaction available without custom form association. */ }
|
|
84
|
-
}
|
|
85
|
-
connectedCallback() {
|
|
86
|
-
this.currentValue = this.getAttribute("value") ?? this.querySelector('input[slot="input"]')?.value ?? "";
|
|
87
|
-
this.selectedValueState = this.getAttribute("selected-value") ?? "";
|
|
88
|
-
this.selectedOptionsState = this.readSelectedOptions(this.getAttribute("selected-options") ?? this.getAttribute("default-selected-options"));
|
|
89
|
-
this.render();
|
|
90
|
-
this.syncClearButton();
|
|
91
|
-
this.addEventListener("click", this.onSlottedControlClick);
|
|
92
|
-
this.addEventListener("keydown", this.onSlottedTriggerKeyDown);
|
|
93
|
-
if (this.hasAttribute("open") || this.hasAttribute("default-open"))
|
|
94
|
-
this.openList();
|
|
95
|
-
this.observer = new MutationObserver(records => {
|
|
96
|
-
const listbox = this.getListbox();
|
|
97
|
-
const trigger = this.getTrigger();
|
|
98
|
-
if (records.some(record => !listbox?.contains(record.target) && !trigger?.contains(record.target)))
|
|
99
|
-
this.updateOptions();
|
|
100
|
-
});
|
|
101
|
-
this.observer.observe(this, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["value", "label", "disabled"] });
|
|
102
|
-
this.syncFormState();
|
|
103
|
-
}
|
|
104
|
-
disconnectedCallback() { this.observer?.disconnect(); this.positionEvents?.abort(); this.positionEvents = undefined; this.stopPositionObserver(); stopHostAccessibleNameSync(this); this.removeEventListener("click", this.onSlottedControlClick); this.removeEventListener("keydown", this.onSlottedTriggerKeyDown); this.boundInput?.removeEventListener("input", this.handleInput); this.boundInput?.removeEventListener("click", this.handleInputClick); this.boundInput?.removeEventListener("keydown", this.handleInputKeyDown); this.boundInput?.removeEventListener("blur", this.handleInputBlur); }
|
|
105
|
-
get listboxElement() { return this.listboxElementValue; }
|
|
106
|
-
set listboxElement(value) {
|
|
107
|
-
const next = value ?? undefined;
|
|
108
|
-
if (this.listboxElementValue === next)
|
|
109
|
-
return;
|
|
110
|
-
this.listboxElementValue = next;
|
|
111
|
-
if (this.isConnected)
|
|
112
|
-
this.updateOptions();
|
|
113
|
-
}
|
|
114
|
-
getInput() {
|
|
115
|
-
const assigned = this.inputSlot?.assignedElements({ flatten: true }).find((element) => element instanceof HTMLInputElement);
|
|
116
|
-
return assigned ?? this.fallbackInput ?? null;
|
|
117
|
-
}
|
|
118
|
-
bindInput() {
|
|
119
|
-
const input = this.getInput();
|
|
120
|
-
if (!input)
|
|
121
|
-
return;
|
|
122
|
-
if (input !== this.fallbackInput)
|
|
123
|
-
this.slotInputDefaults = rememberNativeFormControlDefaults(this.slotInputDefaults, input);
|
|
124
|
-
const defaults = input === this.fallbackInput ? undefined : this.slotInputDefaults;
|
|
125
|
-
if (this.boundInput !== input) {
|
|
126
|
-
this.boundInput?.removeEventListener("input", this.handleInput);
|
|
127
|
-
this.boundInput?.removeEventListener("click", this.handleInputClick);
|
|
128
|
-
this.boundInput?.removeEventListener("keydown", this.handleInputKeyDown);
|
|
129
|
-
this.boundInput?.removeEventListener("blur", this.handleInputBlur);
|
|
130
|
-
input.addEventListener("input", this.handleInput);
|
|
131
|
-
input.addEventListener("click", this.handleInputClick);
|
|
132
|
-
input.addEventListener("keydown", this.handleInputKeyDown);
|
|
133
|
-
input.addEventListener("blur", this.handleInputBlur);
|
|
134
|
-
this.boundInput = input;
|
|
135
|
-
}
|
|
136
|
-
if (this.fallbackInput)
|
|
137
|
-
this.fallbackInput.hidden = input !== this.fallbackInput;
|
|
138
|
-
if (!input.id)
|
|
139
|
-
input.id = `combo-${this.instanceId}-input`;
|
|
140
|
-
if (!input.type)
|
|
141
|
-
input.type = "text";
|
|
142
|
-
input.role = "combobox";
|
|
143
|
-
input.autocomplete ||= "off";
|
|
144
|
-
input.setAttribute("aria-autocomplete", "list");
|
|
145
|
-
input.setAttribute("aria-haspopup", "listbox");
|
|
146
|
-
input.value = this.currentValue;
|
|
147
|
-
input.placeholder = this.getAttribute("placeholder") ?? "";
|
|
148
|
-
input.readOnly = this.hasAttribute("read-only") || Boolean(defaults?.readOnly);
|
|
149
|
-
input.disabled = this.formDisabled || this.hasAttribute("disabled") || Boolean(defaults?.disabled);
|
|
150
|
-
input.required = this.hasAttribute("required") || Boolean(defaults?.required);
|
|
151
|
-
input.hidden = this.hasAttribute("button-trigger");
|
|
152
|
-
input.part.add("control");
|
|
153
|
-
for (const name of ["aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"]) {
|
|
154
|
-
const value = this.getAttribute(name);
|
|
155
|
-
if (value === null)
|
|
156
|
-
input.removeAttribute(name);
|
|
157
|
-
else
|
|
158
|
-
input.setAttribute(name, value);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
attributeChangedCallback(name, _oldValue, value) {
|
|
162
|
-
if (!this.isConnected)
|
|
163
|
-
return;
|
|
164
|
-
const input = this.getInput();
|
|
165
|
-
if (!input)
|
|
166
|
-
return;
|
|
167
|
-
if (name === "label") {
|
|
168
|
-
const label = this.shadowRoot?.querySelector("label");
|
|
169
|
-
if (label)
|
|
170
|
-
label.textContent = value ?? "";
|
|
171
|
-
}
|
|
172
|
-
else if (name === "value") {
|
|
173
|
-
this.currentValue = value ?? "";
|
|
174
|
-
input.value = this.currentValue;
|
|
175
|
-
this.activeIndex = -1;
|
|
176
|
-
this.updateOptions();
|
|
177
|
-
}
|
|
178
|
-
else if (name === "selected-value") {
|
|
179
|
-
this.selectedValueState = value ?? "";
|
|
180
|
-
this.dirty = false;
|
|
181
|
-
this.updateOptions();
|
|
182
|
-
}
|
|
183
|
-
else if (name === "selected-options") {
|
|
184
|
-
this.selectedOptionsState = this.readSelectedOptions(value);
|
|
185
|
-
this.selectionsDirty = false;
|
|
186
|
-
this.updateOptions();
|
|
187
|
-
}
|
|
188
|
-
else if (name === "default-selected-options" && !this.selectionsDirty && !this.hasAttribute("selected-options")) {
|
|
189
|
-
this.selectedOptionsState = this.readSelectedOptions(value);
|
|
190
|
-
this.updateOptions();
|
|
191
|
-
}
|
|
192
|
-
else if (name === "disabled")
|
|
193
|
-
input.disabled = this.formDisabled || this.hasAttribute("disabled") || Boolean(this.slotInputDefaults?.disabled);
|
|
194
|
-
else if (name === "read-only") {
|
|
195
|
-
input.readOnly = this.hasAttribute("read-only") || Boolean(this.slotInputDefaults?.readOnly);
|
|
196
|
-
this.syncExpandIcon();
|
|
197
|
-
}
|
|
198
|
-
else if (name === "button-trigger") {
|
|
199
|
-
const active = this.hasAttribute("button-trigger");
|
|
200
|
-
input.hidden = active;
|
|
201
|
-
if (this.buttonTriggerSlot) {
|
|
202
|
-
this.buttonTriggerSlot.hidden = !active;
|
|
203
|
-
if (active && this.fallbackButtonTrigger?.parentElement !== this.buttonTriggerSlot)
|
|
204
|
-
this.buttonTriggerSlot.append(this.fallbackButtonTrigger);
|
|
205
|
-
else if (!active)
|
|
206
|
-
this.fallbackButtonTrigger?.remove();
|
|
207
|
-
}
|
|
208
|
-
const label = this.shadowRoot?.querySelector("label");
|
|
209
|
-
if (label)
|
|
210
|
-
label.htmlFor = active ? "combo-" + this.instanceId : "combo-" + this.instanceId + "-input";
|
|
211
|
-
}
|
|
212
|
-
else if (name === "clearable" || name === "clear-label")
|
|
213
|
-
this.syncClearButton();
|
|
214
|
-
else if (name === "appearance" || name === "size" || name === "positioning") { /* Host selectors update the shadow theme without rebuilding focused controls. */ }
|
|
215
|
-
else if (name === "disable-auto-focus")
|
|
216
|
-
this.updateOptions();
|
|
217
|
-
else if (name === "expand-icon")
|
|
218
|
-
this.syncExpandIcon();
|
|
219
|
-
else if (name === "selected-label")
|
|
220
|
-
this.shadowRoot?.querySelector(".selected")?.setAttribute("aria-label", value ?? "Selected options");
|
|
221
|
-
else if (name === "open" || name === "default-open") {
|
|
222
|
-
if (this.hasAttribute("open") || this.hasAttribute("default-open"))
|
|
223
|
-
this.openList();
|
|
224
|
-
else
|
|
225
|
-
this.closeList();
|
|
226
|
-
}
|
|
227
|
-
else if (name === "required")
|
|
228
|
-
input.required = this.hasAttribute("required") || Boolean(this.slotInputDefaults?.required);
|
|
229
|
-
else if (name === "placeholder")
|
|
230
|
-
input.placeholder = value ?? "";
|
|
231
|
-
else if (name === "aria-label" || name === "aria-labelledby" || name === "aria-description" || name === "aria-describedby" || name === "aria-invalid")
|
|
232
|
-
value === null ? input.removeAttribute(name) : input.setAttribute(name, value);
|
|
233
|
-
else if (name === "allow-freeform" || name === "no-results" || name === "remove-selected-label" || name === "multiselect")
|
|
234
|
-
this.updateOptions();
|
|
235
|
-
this.syncAria();
|
|
236
|
-
this.syncFormState();
|
|
237
|
-
}
|
|
238
|
-
get value() { return this.currentValue; }
|
|
239
|
-
set value(value) {
|
|
240
|
-
this.currentValue = value;
|
|
241
|
-
const input = this.getInput();
|
|
242
|
-
if (input) {
|
|
243
|
-
input.value = value;
|
|
244
|
-
this.activeIndex = -1;
|
|
245
|
-
this.updateOptions();
|
|
246
|
-
this.syncAria();
|
|
247
|
-
this.syncFormState();
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
get selectedValue() { return this.selectedValueState; }
|
|
251
|
-
set selectedValue(value) {
|
|
252
|
-
this.selectedValueState = value;
|
|
253
|
-
this.dirty = false;
|
|
254
|
-
this.updateOptions();
|
|
255
|
-
this.syncFormState();
|
|
256
|
-
}
|
|
257
|
-
get selectedOptions() { return [...this.selectedOptionsState]; }
|
|
258
|
-
set selectedOptions(values) {
|
|
259
|
-
this.selectedOptionsState = [...new Set(values.map(String))];
|
|
260
|
-
this.selectionsDirty = false;
|
|
261
|
-
this.updateOptions();
|
|
262
|
-
this.syncFormState();
|
|
263
|
-
}
|
|
264
|
-
setOpen(open) { if (open)
|
|
265
|
-
this.openList();
|
|
266
|
-
else
|
|
267
|
-
this.closeList(); }
|
|
268
|
-
readSelectedOptions(value) {
|
|
269
|
-
if (!value)
|
|
270
|
-
return [];
|
|
271
|
-
try {
|
|
272
|
-
const parsed = JSON.parse(value);
|
|
273
|
-
return Array.isArray(parsed) ? [...new Set(parsed.map(String))] : [];
|
|
274
|
-
}
|
|
275
|
-
catch {
|
|
276
|
-
return [];
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
formDisabledCallback(disabled) { this.formDisabled = disabled; this.bindInput(); this.syncFormState(); }
|
|
280
|
-
formResetCallback() {
|
|
281
|
-
this.selectedOptionsState = this.readSelectedOptions(this.getAttribute("default-selected-options"));
|
|
282
|
-
this.selectionsDirty = false;
|
|
283
|
-
this.selectedValueState = this.getAttribute("selected-value") ?? "";
|
|
284
|
-
this.value = this.getAttribute("value") ?? this.slotInputDefaults?.defaultValue ?? "";
|
|
285
|
-
this.closeList();
|
|
286
|
-
}
|
|
287
|
-
formStateRestoreCallback(state) {
|
|
288
|
-
if (typeof state !== "string")
|
|
289
|
-
return;
|
|
290
|
-
if (this.hasAttribute("multiselect"))
|
|
291
|
-
this.selectedOptions = this.readSelectedOptions(state);
|
|
292
|
-
else
|
|
293
|
-
this.selectOption(state);
|
|
294
|
-
}
|
|
295
|
-
optionElements() { return [...this.querySelectorAll(":scope > spui-combobox-option, :scope > spui-combobox-option-group > spui-combobox-option")]; }
|
|
296
|
-
filteredOptions() {
|
|
297
|
-
const query = this.hasAttribute("read-only") ? "" : this.currentValue.trim().toLocaleLowerCase(this.lang || undefined);
|
|
298
|
-
return this.optionElements().filter(option => !query || (option.getAttribute("label") ?? option.textContent ?? "").trim().toLocaleLowerCase(this.lang || undefined).includes(query));
|
|
299
|
-
}
|
|
300
|
-
syncFormState() {
|
|
301
|
-
const formValue = this.hasAttribute("multiselect")
|
|
302
|
-
? this.selectedOptionsState.length ? this.selectedOptionsFormData() : null
|
|
303
|
-
: this.dirty ? (this.hasAttribute("allow-freeform") ? this.currentValue : "") : (this.selectedValue || (this.hasAttribute("allow-freeform") ? this.currentValue : ""));
|
|
304
|
-
this.internals?.setFormValue(formValue || null, this.hasAttribute("multiselect") ? JSON.stringify(this.selectedOptionsState) : typeof formValue === "string" ? formValue : null);
|
|
305
|
-
const missing = this.hasAttribute("required") && (this.hasAttribute("multiselect") ? this.selectedOptionsState.length === 0 : !formValue);
|
|
306
|
-
try {
|
|
307
|
-
this.internals?.setValidity(missing ? { valueMissing: true } : {}, missing ? this.getAttribute("required-message") || "Choose an option." : "", this.hasAttribute("button-trigger") ? undefined : this.getInput() ?? undefined);
|
|
308
|
-
}
|
|
309
|
-
catch { /* Keep native input semantics if an engine rejects the validity anchor. */ }
|
|
310
|
-
}
|
|
311
|
-
selectedOptionsFormData() {
|
|
312
|
-
const data = new FormData();
|
|
313
|
-
const name = this.getAttribute("name") ?? "";
|
|
314
|
-
if (name)
|
|
315
|
-
for (const value of this.selectedOptionsState)
|
|
316
|
-
data.append(name, value);
|
|
317
|
-
return data;
|
|
318
|
-
}
|
|
319
|
-
syncClearButton() {
|
|
320
|
-
const label = this.getAttribute("clear-label") ?? "Clear selection";
|
|
321
|
-
const visible = this.hasAttribute("clearable");
|
|
322
|
-
const fallback = this.shadowRoot?.querySelector(".clear");
|
|
323
|
-
if (fallback) {
|
|
324
|
-
fallback.hidden = !visible;
|
|
325
|
-
fallback.setAttribute("aria-label", label);
|
|
326
|
-
}
|
|
327
|
-
for (const element of this.querySelectorAll(":scope > [slot=clear-button]")) {
|
|
328
|
-
element.hidden = !visible;
|
|
329
|
-
if (!element.hasAttribute("aria-label"))
|
|
330
|
-
element.setAttribute("aria-label", label);
|
|
331
|
-
if (element instanceof HTMLButtonElement)
|
|
332
|
-
element.type = "button";
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
syncExpandIcon() {
|
|
336
|
-
const expand = this.shadowRoot?.querySelector(".expand");
|
|
337
|
-
const custom = (this.shadowRoot?.querySelector('slot[name="expand-icon"]')?.assignedElements({ flatten: true }).length ?? 0) > 0;
|
|
338
|
-
if (expand)
|
|
339
|
-
expand.hidden = !(this.hasAttribute("read-only") || this.hasAttribute("button-trigger") || custom);
|
|
340
|
-
}
|
|
341
|
-
clearSelection(event) {
|
|
342
|
-
this.currentValue = "";
|
|
343
|
-
this.selectedValueState = "";
|
|
344
|
-
this.selectedOptionsState = [];
|
|
345
|
-
this.dirty = false;
|
|
346
|
-
this.selectionsDirty = true;
|
|
347
|
-
const input = this.getInput();
|
|
348
|
-
if (input)
|
|
349
|
-
input.value = "";
|
|
350
|
-
this.closeList(event);
|
|
351
|
-
this.updateOptions();
|
|
352
|
-
this.syncFormState();
|
|
353
|
-
this.dispatchEvent(new CustomEvent("spui-change", { detail: { event, value: "", selectedValue: undefined, optionText: undefined, selectedOptions: [] }, bubbles: true, composed: true }));
|
|
354
|
-
(this.hasAttribute("button-trigger") ? this.getTrigger() : input)?.focus();
|
|
355
|
-
}
|
|
356
|
-
getTrigger() {
|
|
357
|
-
const slot = this.shadowRoot?.querySelector('slot[name="button"]');
|
|
358
|
-
return slot?.assignedElements({ flatten: true })[0] ?? slot?.querySelector("button") ?? null;
|
|
359
|
-
}
|
|
360
|
-
getListbox() {
|
|
361
|
-
if (this.listboxElementValue)
|
|
362
|
-
return this.listboxElementValue;
|
|
363
|
-
const slot = this.shadowRoot?.querySelector('slot[name="listbox"]');
|
|
364
|
-
const assigned = slot?.assignedElements({ flatten: true }).find(element => element instanceof HTMLElement);
|
|
365
|
-
return assigned ?? slot?.querySelector("[role=listbox]") ?? null;
|
|
366
|
-
}
|
|
367
|
-
toggleOpen(event) { if (this.open)
|
|
368
|
-
this.closeList(event);
|
|
369
|
-
else
|
|
370
|
-
this.openList(0, event); this.getTrigger()?.focus(); }
|
|
371
|
-
syncAria() {
|
|
372
|
-
const input = this.getInput();
|
|
373
|
-
const list = this.getListbox();
|
|
374
|
-
if (!input || !list)
|
|
375
|
-
return;
|
|
376
|
-
const trigger = this.hasAttribute("button-trigger") ? this.getTrigger() : input;
|
|
377
|
-
const triggerValue = this.getTrigger()?.querySelector(".trigger-value") ?? this.shadowRoot?.querySelector(".trigger-value");
|
|
378
|
-
if (triggerValue)
|
|
379
|
-
triggerValue.textContent = this.currentValue || this.getAttribute("placeholder") || "";
|
|
380
|
-
if (trigger) {
|
|
381
|
-
trigger.setAttribute("role", "combobox");
|
|
382
|
-
trigger.setAttribute("aria-haspopup", "listbox");
|
|
383
|
-
trigger.setAttribute("aria-expanded", String(this.open));
|
|
384
|
-
const label = this.shadowRoot?.querySelector("label");
|
|
385
|
-
if (!trigger.id)
|
|
386
|
-
trigger.id = "combo-" + this.instanceId;
|
|
387
|
-
if (label)
|
|
388
|
-
label.htmlFor = trigger.id;
|
|
389
|
-
const explicitLabel = this.getAttribute("aria-label");
|
|
390
|
-
const explicitLabelledBy = this.getAttribute("aria-labelledby");
|
|
391
|
-
if (explicitLabel !== null) {
|
|
392
|
-
trigger.setAttribute("aria-label", explicitLabel);
|
|
393
|
-
trigger.removeAttribute("aria-labelledby");
|
|
394
|
-
}
|
|
395
|
-
else if (explicitLabelledBy !== null) {
|
|
396
|
-
trigger.removeAttribute("aria-label");
|
|
397
|
-
trigger.setAttribute("aria-labelledby", explicitLabelledBy);
|
|
398
|
-
}
|
|
399
|
-
else if (label && trigger.getRootNode() !== this.shadowRoot) {
|
|
400
|
-
trigger.removeAttribute("aria-labelledby");
|
|
401
|
-
trigger.setAttribute("aria-label", label.textContent?.trim() ?? "");
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
trigger.removeAttribute("aria-label");
|
|
405
|
-
if (label)
|
|
406
|
-
trigger.setAttribute("aria-labelledby", label.id);
|
|
407
|
-
else
|
|
408
|
-
trigger.removeAttribute("aria-labelledby");
|
|
409
|
-
}
|
|
410
|
-
if (trigger !== input)
|
|
411
|
-
syncHostAccessibleName(this, input);
|
|
412
|
-
syncHostAccessibleName(this, trigger);
|
|
413
|
-
const invalid = this.getAttribute("aria-invalid");
|
|
414
|
-
if (invalid === null)
|
|
415
|
-
trigger.removeAttribute("aria-invalid");
|
|
416
|
-
else
|
|
417
|
-
trigger.setAttribute("aria-invalid", invalid);
|
|
418
|
-
if (trigger.getRootNode() === list.getRootNode())
|
|
419
|
-
trigger.setAttribute("aria-controls", list.id);
|
|
420
|
-
else
|
|
421
|
-
trigger.removeAttribute("aria-controls");
|
|
422
|
-
if (this.hasAttribute("disabled")) {
|
|
423
|
-
trigger.setAttribute("aria-disabled", "true");
|
|
424
|
-
if (trigger instanceof HTMLButtonElement)
|
|
425
|
-
trigger.disabled = true;
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
trigger.removeAttribute("aria-disabled");
|
|
429
|
-
if (trigger instanceof HTMLButtonElement)
|
|
430
|
-
trigger.disabled = false;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (!list.id)
|
|
434
|
-
list.id = `listbox-${this.instanceId}`;
|
|
435
|
-
list.setAttribute("role", this.hasAttribute("multiselect") ? "menu" : "listbox");
|
|
436
|
-
if (list.slot === "listbox") {
|
|
437
|
-
const defaults = {
|
|
438
|
-
position: "absolute", "z-index": "var(--spui-layer-dropdown,20)", top: "calc(100% + 4px)", "inset-inline": "0",
|
|
439
|
-
"box-sizing": "border-box", "max-height": "240px", overflow: "auto", margin: "0", padding: "4px",
|
|
440
|
-
"list-style": "none", border: "1px solid var(--spui-ui-stroke,#d1d1d1)", color: "var(--spui-ui-text,inherit)", font: "inherit",
|
|
441
|
-
"border-radius": "var(--spui-card-radius,8px)", background: "var(--spui-ui-surface,#fff)",
|
|
442
|
-
"box-shadow": "var(--spui-card-shadow,0 4px 14px #0002)"
|
|
443
|
-
};
|
|
444
|
-
for (const [property, value] of Object.entries(defaults))
|
|
445
|
-
if (!list.style.getPropertyValue(property))
|
|
446
|
-
list.style.setProperty(property, value);
|
|
447
|
-
if (this.getAttribute("positioning")?.startsWith("above")) {
|
|
448
|
-
list.style.setProperty("top", "auto");
|
|
449
|
-
list.style.setProperty("bottom", "calc(100% + 4px)");
|
|
450
|
-
}
|
|
451
|
-
else
|
|
452
|
-
list.style.removeProperty("bottom");
|
|
453
|
-
}
|
|
454
|
-
if (!this.hasAttribute("button-trigger")) {
|
|
455
|
-
input.setAttribute("aria-expanded", String(this.open));
|
|
456
|
-
if (input.getRootNode() === list.getRootNode())
|
|
457
|
-
input.setAttribute("aria-controls", list.id);
|
|
458
|
-
else
|
|
459
|
-
input.removeAttribute("aria-controls");
|
|
460
|
-
}
|
|
461
|
-
list.removeAttribute("aria-multiselectable");
|
|
462
|
-
const active = list.querySelector(`[data-option-index="${this.activeIndex}"]`);
|
|
463
|
-
if (this.open && active)
|
|
464
|
-
trigger?.setAttribute("aria-activedescendant", active.id);
|
|
465
|
-
else
|
|
466
|
-
trigger?.removeAttribute("aria-activedescendant");
|
|
467
|
-
for (const option of list.querySelectorAll("[role=option], [role=menuitemcheckbox]")) {
|
|
468
|
-
const activeOption = this.open && Number(option.dataset.optionIndex) === this.activeIndex;
|
|
469
|
-
const selectedOption = option.getAttribute("aria-selected") === "true" || option.getAttribute("aria-checked") === "true";
|
|
470
|
-
option.dataset.active = String(activeOption);
|
|
471
|
-
if (list.slot === "listbox")
|
|
472
|
-
option.style.background = activeOption || selectedOption ? "var(--spui-ui-surface-hover,var(--spui-ui-canvas,#f5f5f5))" : "";
|
|
473
|
-
}
|
|
474
|
-
list.hidden = !this.open;
|
|
475
|
-
this.syncPositionListeners();
|
|
476
|
-
this.positionListbox();
|
|
477
|
-
}
|
|
478
|
-
syncPositionListeners() {
|
|
479
|
-
const shouldListen = this.open && Boolean(this.listboxElementValue) && !this.hasAttribute("inline-popup");
|
|
480
|
-
if (!shouldListen) {
|
|
481
|
-
this.positionEvents?.abort();
|
|
482
|
-
this.positionEvents = undefined;
|
|
483
|
-
this.stopPositionObserver();
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
if (!this.positionEvents) {
|
|
487
|
-
this.positionEvents = new AbortController();
|
|
488
|
-
const options = { signal: this.positionEvents.signal };
|
|
489
|
-
window.addEventListener("resize", this.schedulePositionListbox, options);
|
|
490
|
-
window.addEventListener("scroll", this.schedulePositionListbox, { ...options, capture: true });
|
|
491
|
-
}
|
|
492
|
-
if (typeof ResizeObserver === "undefined")
|
|
493
|
-
return;
|
|
494
|
-
this.positionObserver ??= new ResizeObserver(this.schedulePositionListbox);
|
|
495
|
-
const anchor = this.hasAttribute("button-trigger") ? this.getTrigger() : this.getInput();
|
|
496
|
-
const list = this.listboxElementValue;
|
|
497
|
-
if (anchor !== this.observedPositionAnchor) {
|
|
498
|
-
if (this.observedPositionAnchor)
|
|
499
|
-
this.positionObserver.unobserve(this.observedPositionAnchor);
|
|
500
|
-
this.observedPositionAnchor = anchor ?? undefined;
|
|
501
|
-
if (anchor)
|
|
502
|
-
this.positionObserver.observe(anchor);
|
|
503
|
-
}
|
|
504
|
-
if (list !== this.observedPositionListbox) {
|
|
505
|
-
if (this.observedPositionListbox)
|
|
506
|
-
this.positionObserver.unobserve(this.observedPositionListbox);
|
|
507
|
-
this.observedPositionListbox = list;
|
|
508
|
-
if (list)
|
|
509
|
-
this.positionObserver.observe(list);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
stopPositionObserver() {
|
|
513
|
-
this.positionObserver?.disconnect();
|
|
514
|
-
this.observedPositionAnchor = undefined;
|
|
515
|
-
this.observedPositionListbox = undefined;
|
|
516
|
-
if (this.positionFrame !== undefined)
|
|
517
|
-
cancelAnimationFrame(this.positionFrame);
|
|
518
|
-
this.positionFrame = undefined;
|
|
519
|
-
}
|
|
520
|
-
schedulePositionListbox = () => {
|
|
521
|
-
if (this.positionFrame !== undefined)
|
|
522
|
-
return;
|
|
523
|
-
this.positionFrame = requestAnimationFrame(() => {
|
|
524
|
-
this.positionFrame = undefined;
|
|
525
|
-
this.positionListbox();
|
|
526
|
-
});
|
|
527
|
-
};
|
|
528
|
-
positionListbox = () => {
|
|
529
|
-
const list = this.listboxElementValue;
|
|
530
|
-
if (!list || this.hasAttribute("inline-popup"))
|
|
531
|
-
return;
|
|
532
|
-
const anchor = this.hasAttribute("button-trigger") ? this.getTrigger() : this.getInput();
|
|
533
|
-
if (!anchor)
|
|
534
|
-
return;
|
|
535
|
-
const rect = anchor.getBoundingClientRect();
|
|
536
|
-
const viewportWidth = this.ownerDocument.documentElement.clientWidth || window.innerWidth;
|
|
537
|
-
const viewportHeight = this.ownerDocument.documentElement.clientHeight || window.innerHeight;
|
|
538
|
-
const margin = 8;
|
|
539
|
-
const gap = 4;
|
|
540
|
-
const maxWidth = Math.max(0, viewportWidth - margin * 2);
|
|
541
|
-
const width = Math.min(Math.max(rect.width, 0), maxWidth);
|
|
542
|
-
const left = Math.max(margin, Math.min(rect.left, viewportWidth - width - margin));
|
|
543
|
-
const below = Math.max(0, viewportHeight - rect.bottom - margin - gap);
|
|
544
|
-
const above = Math.max(0, rect.top - margin - gap);
|
|
545
|
-
const requested = this.getAttribute("positioning")?.startsWith("above") ? "above" : this.getAttribute("positioning")?.startsWith("below") ? "below" : undefined;
|
|
546
|
-
const naturalHeight = Math.min(240, Math.max(list.scrollHeight, 0));
|
|
547
|
-
const side = requested ?? (below < naturalHeight && above > below ? "above" : "below");
|
|
548
|
-
const available = side === "above" ? above : below;
|
|
549
|
-
const maxHeight = Math.min(240, available);
|
|
550
|
-
list.style.position = "fixed";
|
|
551
|
-
list.style.insetInline = "auto";
|
|
552
|
-
list.style.left = `${left}px`;
|
|
553
|
-
list.style.right = "auto";
|
|
554
|
-
list.style.width = `${width}px`;
|
|
555
|
-
list.style.maxWidth = `calc(100vw - ${margin * 2}px)`;
|
|
556
|
-
list.style.maxHeight = `${maxHeight}px`;
|
|
557
|
-
list.style.top = side === "above" ? "auto" : `${rect.bottom + gap}px`;
|
|
558
|
-
list.style.bottom = side === "above" ? `${Math.max(margin, viewportHeight - rect.top + gap)}px` : "auto";
|
|
559
|
-
list.style.zIndex = "var(--spui-layer-dropdown,20)";
|
|
560
|
-
};
|
|
561
|
-
updateOptions() {
|
|
562
|
-
const list = this.getListbox();
|
|
563
|
-
if (!list)
|
|
564
|
-
return;
|
|
565
|
-
const options = this.filteredOptions();
|
|
566
|
-
list.replaceChildren();
|
|
567
|
-
let currentGroup = null;
|
|
568
|
-
let groupContainer = null;
|
|
569
|
-
options.forEach((option, index) => {
|
|
570
|
-
const group = option.parentElement?.matches("spui-combobox-option-group") ? option.parentElement : null;
|
|
571
|
-
if (group && group !== currentGroup) {
|
|
572
|
-
currentGroup = group;
|
|
573
|
-
groupContainer = document.createElement("div");
|
|
574
|
-
groupContainer.setAttribute("role", "group");
|
|
575
|
-
groupContainer.className = "option-group";
|
|
576
|
-
if (list.slot === "listbox") {
|
|
577
|
-
groupContainer.style.display = "grid";
|
|
578
|
-
groupContainer.style.gap = "2px";
|
|
579
|
-
groupContainer.style.paddingBlock = "2px";
|
|
580
|
-
}
|
|
581
|
-
const label = group.getAttribute("label") ?? group.getAttribute("aria-label") ?? "";
|
|
582
|
-
if (label)
|
|
583
|
-
groupContainer.setAttribute("aria-label", label);
|
|
584
|
-
const heading = document.createElement("span");
|
|
585
|
-
heading.className = "group-label";
|
|
586
|
-
heading.textContent = group.getAttribute("label") ?? group.getAttribute("aria-label") ?? "";
|
|
587
|
-
if (list.slot === "listbox") {
|
|
588
|
-
heading.style.padding = "6px 10px";
|
|
589
|
-
heading.style.color = "var(--spui-ui-muted,inherit)";
|
|
590
|
-
heading.style.fontSize = "12px";
|
|
591
|
-
heading.style.fontWeight = "600";
|
|
592
|
-
}
|
|
593
|
-
groupContainer.append(heading);
|
|
594
|
-
list.append(groupContainer);
|
|
595
|
-
}
|
|
596
|
-
else if (!group) {
|
|
597
|
-
currentGroup = null;
|
|
598
|
-
groupContainer = null;
|
|
599
|
-
}
|
|
600
|
-
const item = document.createElement("div");
|
|
601
|
-
item.id = `option-${this.instanceId}-${index}`;
|
|
602
|
-
item.dataset.optionIndex = String(index);
|
|
603
|
-
const value = option.getAttribute("value") ?? "";
|
|
604
|
-
const selected = this.hasAttribute("multiselect") ? this.selectedOptionsState.includes(value) : value === this.selectedValue;
|
|
605
|
-
if (this.hasAttribute("multiselect")) {
|
|
606
|
-
item.setAttribute("role", "menuitemcheckbox");
|
|
607
|
-
item.setAttribute("aria-checked", String(selected));
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
item.setAttribute("role", "option");
|
|
611
|
-
item.setAttribute("aria-selected", String(selected));
|
|
612
|
-
}
|
|
613
|
-
const disabled = option.hasAttribute("disabled") || Boolean(group?.hasAttribute("disabled"));
|
|
614
|
-
if (disabled)
|
|
615
|
-
item.setAttribute("aria-disabled", "true");
|
|
616
|
-
if (this.hasAttribute("multiselect")) {
|
|
617
|
-
const check = document.createElement("span");
|
|
618
|
-
check.className = "check";
|
|
619
|
-
check.setAttribute("aria-hidden", "true");
|
|
620
|
-
check.textContent = selected ? "✓" : "";
|
|
621
|
-
if (list.slot === "listbox") {
|
|
622
|
-
check.style.cssText = "display:inline-grid;place-items:center;width:16px;height:16px;flex:none;border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:3px;color:var(--spui-ui-accent,#0f6cbd);font-size:12px";
|
|
623
|
-
if (selected)
|
|
624
|
-
check.style.background = "var(--spui-ui-canvas,#f5f5f5)";
|
|
625
|
-
}
|
|
626
|
-
item.append(check);
|
|
627
|
-
}
|
|
628
|
-
if (list.slot === "listbox") {
|
|
629
|
-
item.style.cssText = "display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--spui-control-radius,4px);color:var(--spui-ui-text,inherit);cursor:pointer";
|
|
630
|
-
if (selected)
|
|
631
|
-
item.style.background = "var(--spui-ui-surface-hover,var(--spui-ui-canvas,#f5f5f5))";
|
|
632
|
-
if (disabled)
|
|
633
|
-
item.style.opacity = ".5";
|
|
634
|
-
}
|
|
635
|
-
const text = document.createElement("span");
|
|
636
|
-
text.textContent = (option.getAttribute("label") ?? option.textContent ?? "").trim();
|
|
637
|
-
item.append(text);
|
|
638
|
-
item.addEventListener("pointerdown", event => event.preventDefault());
|
|
639
|
-
item.addEventListener("pointerenter", () => { if (list.slot === "listbox" && !disabled)
|
|
640
|
-
item.style.background = "var(--spui-ui-surface-hover,var(--spui-ui-canvas,#f5f5f5))"; });
|
|
641
|
-
item.addEventListener("pointerleave", () => { if (list.slot === "listbox" && item.dataset.active !== "true" && item.getAttribute("aria-selected") !== "true" && item.getAttribute("aria-checked") !== "true")
|
|
642
|
-
item.style.background = ""; });
|
|
643
|
-
item.addEventListener("click", event => { if (!disabled)
|
|
644
|
-
this.selectOption(option.getAttribute("value") ?? "", event); });
|
|
645
|
-
(groupContainer ?? list).append(item);
|
|
646
|
-
});
|
|
647
|
-
const empty = this.shadowRoot?.querySelector(".empty");
|
|
648
|
-
if (empty) {
|
|
649
|
-
empty.textContent = this.getAttribute("no-results") ?? "No results";
|
|
650
|
-
empty.hidden = options.length !== 0;
|
|
651
|
-
}
|
|
652
|
-
this.syncSelectedTags();
|
|
653
|
-
this.syncAria();
|
|
654
|
-
}
|
|
655
|
-
syncSelectedTags() {
|
|
656
|
-
const container = this.shadowRoot?.querySelector(".selected");
|
|
657
|
-
if (!container)
|
|
658
|
-
return;
|
|
659
|
-
container.replaceChildren();
|
|
660
|
-
if (!this.hasAttribute("multiselect")) {
|
|
661
|
-
container.hidden = true;
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const options = this.optionElements();
|
|
665
|
-
for (const value of this.selectedOptionsState) {
|
|
666
|
-
const option = options.find(item => item.getAttribute("value") === value);
|
|
667
|
-
const label = (option?.getAttribute("label") ?? option?.textContent ?? value).trim();
|
|
668
|
-
const chip = document.createElement("span");
|
|
669
|
-
chip.className = "tag";
|
|
670
|
-
const text = document.createElement("span");
|
|
671
|
-
text.textContent = label;
|
|
672
|
-
const remove = document.createElement("button");
|
|
673
|
-
remove.type = "button";
|
|
674
|
-
remove.className = "remove";
|
|
675
|
-
remove.textContent = "×";
|
|
676
|
-
remove.setAttribute("aria-label", `${this.getAttribute("remove-selected-label") ?? "Remove"} ${label}`);
|
|
677
|
-
remove.addEventListener("click", event => { this.setOptionSelected(value, false, event); this.getInput()?.focus(); });
|
|
678
|
-
chip.append(text, remove);
|
|
679
|
-
container.append(chip);
|
|
680
|
-
}
|
|
681
|
-
container.hidden = this.selectedOptionsState.length === 0;
|
|
682
|
-
}
|
|
683
|
-
openList(index = 0, sourceEvent) {
|
|
684
|
-
const changed = !this.open;
|
|
685
|
-
const previousOption = this.activeOptionValue();
|
|
686
|
-
const options = this.filteredOptions();
|
|
687
|
-
this.open = true;
|
|
688
|
-
let activeIndex = options.length ? Math.max(0, Math.min(index, options.length - 1)) : -1;
|
|
689
|
-
while (activeIndex >= 0 && activeIndex < options.length && this.isOptionDisabled(options[activeIndex]))
|
|
690
|
-
activeIndex++;
|
|
691
|
-
this.activeIndex = !this.hasAttribute("disable-auto-focus") && activeIndex < options.length ? activeIndex : -1;
|
|
692
|
-
this.announceActiveOption(previousOption, sourceEvent);
|
|
693
|
-
this.syncAria();
|
|
694
|
-
this.scrollActiveIntoView();
|
|
695
|
-
this.notifyOpenChange(true, sourceEvent, changed);
|
|
696
|
-
}
|
|
697
|
-
closeList(sourceEvent) {
|
|
698
|
-
const changed = this.open;
|
|
699
|
-
const previousOption = this.activeOptionValue();
|
|
700
|
-
this.open = false;
|
|
701
|
-
this.activeIndex = -1;
|
|
702
|
-
this.announceActiveOption(previousOption, sourceEvent);
|
|
703
|
-
this.syncAria();
|
|
704
|
-
this.notifyOpenChange(false, sourceEvent, changed);
|
|
705
|
-
}
|
|
706
|
-
notifyOpenChange(open, sourceEvent, changed = true) {
|
|
707
|
-
if (changed)
|
|
708
|
-
this.dispatchEvent(new CustomEvent("spui-open-change", { detail: { event: sourceEvent, open }, bubbles: true, composed: true }));
|
|
709
|
-
}
|
|
710
|
-
moveActive(delta, sourceEvent, steps = 1) {
|
|
711
|
-
const previousOption = this.activeOptionValue();
|
|
712
|
-
const options = this.filteredOptions();
|
|
713
|
-
const changed = !this.open;
|
|
714
|
-
if (!options.length) {
|
|
715
|
-
this.open = true;
|
|
716
|
-
this.activeIndex = -1;
|
|
717
|
-
this.announceActiveOption(previousOption, sourceEvent);
|
|
718
|
-
this.syncAria();
|
|
719
|
-
this.notifyOpenChange(true, sourceEvent, changed);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const navigable = options.map((option, index) => this.isOptionDisabled(option) ? -1 : index).filter(index => index >= 0);
|
|
723
|
-
const current = navigable.indexOf(this.open ? this.activeIndex : -1);
|
|
724
|
-
const next = Math.max(0, Math.min(navigable.length - 1, (current < 0 ? 0 : current) + delta * steps));
|
|
725
|
-
const index = navigable[next] ?? -1;
|
|
726
|
-
this.open = true;
|
|
727
|
-
this.activeIndex = index;
|
|
728
|
-
this.announceActiveOption(previousOption, sourceEvent);
|
|
729
|
-
this.syncAria();
|
|
730
|
-
this.scrollActiveIntoView();
|
|
731
|
-
this.notifyOpenChange(true, sourceEvent, changed);
|
|
732
|
-
}
|
|
733
|
-
moveActiveToEdge(edge, sourceEvent) {
|
|
734
|
-
const changed = !this.open;
|
|
735
|
-
const previousOption = this.activeOptionValue();
|
|
736
|
-
const options = this.filteredOptions();
|
|
737
|
-
const direction = edge === "first" ? 1 : -1;
|
|
738
|
-
let index = edge === "first" ? 0 : options.length - 1;
|
|
739
|
-
while (index >= 0 && index < options.length && this.isOptionDisabled(options[index]))
|
|
740
|
-
index += direction;
|
|
741
|
-
this.activeIndex = index >= 0 && index < options.length ? index : -1;
|
|
742
|
-
this.open = true;
|
|
743
|
-
this.typing = false;
|
|
744
|
-
this.announceActiveOption(previousOption, sourceEvent);
|
|
745
|
-
this.syncAria();
|
|
746
|
-
this.scrollActiveIntoView();
|
|
747
|
-
this.notifyOpenChange(true, sourceEvent, changed);
|
|
748
|
-
}
|
|
749
|
-
isOptionDisabled(option) {
|
|
750
|
-
return option.hasAttribute("disabled") || Boolean(option.parentElement?.matches("spui-combobox-option-group[disabled]"));
|
|
751
|
-
}
|
|
752
|
-
activeOptionValue() {
|
|
753
|
-
const option = this.filteredOptions()[this.activeIndex];
|
|
754
|
-
return option?.getAttribute("value") ?? null;
|
|
755
|
-
}
|
|
756
|
-
announceActiveOption(previousOption, sourceEvent) {
|
|
757
|
-
const nextOption = this.activeOptionValue();
|
|
758
|
-
if (previousOption !== nextOption)
|
|
759
|
-
this.dispatchEvent(new CustomEvent("spui-active-option-change", { detail: { event: sourceEvent, type: sourceEvent ? "change" : undefined, previousOption, nextOption, id: nextOption ?? "", previousId: previousOption }, bubbles: true, composed: true }));
|
|
760
|
-
}
|
|
761
|
-
scrollActiveIntoView() { this.getListbox()?.querySelector(`[data-option-index="${this.activeIndex}"]`)?.scrollIntoView({ block: "nearest" }); }
|
|
762
|
-
selectOption(value, sourceEvent) {
|
|
763
|
-
const option = this.optionElements().find(item => item.getAttribute("value") === value);
|
|
764
|
-
if (!option || option.hasAttribute("disabled"))
|
|
765
|
-
return;
|
|
766
|
-
if (this.hasAttribute("multiselect")) {
|
|
767
|
-
this.setOptionSelected(value, !this.selectedOptionsState.includes(value), sourceEvent);
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
const label = (option.getAttribute("label") ?? option.textContent ?? "").trim();
|
|
771
|
-
this.selectedValueState = value;
|
|
772
|
-
this.dirty = false;
|
|
773
|
-
this.currentValue = label;
|
|
774
|
-
const input = this.getInput();
|
|
775
|
-
if (input)
|
|
776
|
-
input.value = label;
|
|
777
|
-
this.closeList(sourceEvent);
|
|
778
|
-
this.updateOptions();
|
|
779
|
-
this.syncFormState();
|
|
780
|
-
this.dispatchEvent(new CustomEvent("spui-change", { detail: { event: sourceEvent, value: label, selectedValue: value, selectedOptions: [value] }, bubbles: true, composed: true }));
|
|
781
|
-
}
|
|
782
|
-
setOptionSelected(value, selected, sourceEvent) {
|
|
783
|
-
const wasOpen = this.open;
|
|
784
|
-
this.selectedOptionsState = selected
|
|
785
|
-
? [...new Set([...this.selectedOptionsState, value])]
|
|
786
|
-
: this.selectedOptionsState.filter(item => item !== value);
|
|
787
|
-
this.selectionsDirty = true;
|
|
788
|
-
this.dirty = false;
|
|
789
|
-
this.currentValue = "";
|
|
790
|
-
const input = this.getInput();
|
|
791
|
-
if (input)
|
|
792
|
-
input.value = "";
|
|
793
|
-
this.open = wasOpen;
|
|
794
|
-
this.activeIndex = wasOpen ? 0 : -1;
|
|
795
|
-
this.updateOptions();
|
|
796
|
-
this.syncFormState();
|
|
797
|
-
const option = this.optionElements().find(item => item.getAttribute("value") === value);
|
|
798
|
-
const label = option ? (option.getAttribute("label") ?? option.textContent ?? "").trim() : undefined;
|
|
799
|
-
this.dispatchEvent(new CustomEvent("spui-change", { detail: { event: sourceEvent, value: "", selectedValue: value, optionText: label, selectedOptions: [...this.selectedOptionsState] }, bubbles: true, composed: true }));
|
|
800
|
-
}
|
|
801
|
-
onKeyDown(event) {
|
|
802
|
-
if (this.hasAttribute("button-trigger") && (event.key === "Enter" || event.key === " ")) {
|
|
803
|
-
event.preventDefault();
|
|
804
|
-
this.typing = false;
|
|
805
|
-
if (this.open && this.activeIndex >= 0) {
|
|
806
|
-
const option = this.filteredOptions()[this.activeIndex];
|
|
807
|
-
this.selectOption(option.getAttribute("value") ?? "", event);
|
|
808
|
-
}
|
|
809
|
-
else
|
|
810
|
-
this.toggleOpen(event);
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
if (event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
814
|
-
this.typing = true;
|
|
815
|
-
if (!this.open)
|
|
816
|
-
this.openList(0, event);
|
|
817
|
-
}
|
|
818
|
-
else if (["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"].includes(event.key))
|
|
819
|
-
this.typing = false;
|
|
820
|
-
if (event.key === "ArrowDown") {
|
|
821
|
-
event.preventDefault();
|
|
822
|
-
this.moveActive(1, event);
|
|
823
|
-
}
|
|
824
|
-
else if (event.key === "ArrowUp" && event.altKey && this.open) {
|
|
825
|
-
event.preventDefault();
|
|
826
|
-
if (this.activeIndex >= 0) {
|
|
827
|
-
const option = this.filteredOptions()[this.activeIndex];
|
|
828
|
-
this.selectOption(option.getAttribute("value") ?? "", event);
|
|
829
|
-
}
|
|
830
|
-
else
|
|
831
|
-
this.closeList(event);
|
|
832
|
-
}
|
|
833
|
-
else if (event.key === "ArrowUp") {
|
|
834
|
-
event.preventDefault();
|
|
835
|
-
this.moveActive(-1, event);
|
|
836
|
-
}
|
|
837
|
-
else if (event.key === "Home" && this.open) {
|
|
838
|
-
event.preventDefault();
|
|
839
|
-
this.moveActiveToEdge("first", event);
|
|
840
|
-
}
|
|
841
|
-
else if (event.key === "End" && this.open) {
|
|
842
|
-
event.preventDefault();
|
|
843
|
-
this.moveActiveToEdge("last", event);
|
|
844
|
-
}
|
|
845
|
-
else if (event.key === "PageUp" && this.open) {
|
|
846
|
-
event.preventDefault();
|
|
847
|
-
this.moveActive(-1, event, 10);
|
|
848
|
-
}
|
|
849
|
-
else if (event.key === "PageDown" && this.open) {
|
|
850
|
-
event.preventDefault();
|
|
851
|
-
this.moveActive(1, event, 10);
|
|
852
|
-
}
|
|
853
|
-
else if (event.key === "Enter" && !this.open) {
|
|
854
|
-
event.preventDefault();
|
|
855
|
-
this.openList(0, event);
|
|
856
|
-
}
|
|
857
|
-
else if (event.key === "Enter" && this.open && this.activeIndex >= 0) {
|
|
858
|
-
event.preventDefault();
|
|
859
|
-
const option = this.filteredOptions()[this.activeIndex];
|
|
860
|
-
this.selectOption(option.getAttribute("value") ?? "", event);
|
|
861
|
-
}
|
|
862
|
-
else if (event.key === " " && this.open && (!this.typing || this.hasAttribute("button-trigger")) && this.activeIndex >= 0) {
|
|
863
|
-
event.preventDefault();
|
|
864
|
-
const option = this.filteredOptions()[this.activeIndex];
|
|
865
|
-
this.selectOption(option.getAttribute("value") ?? "", event);
|
|
866
|
-
}
|
|
867
|
-
else if (event.key === "Tab" && this.open && !this.hasAttribute("multiselect") && this.activeIndex >= 0) {
|
|
868
|
-
const option = this.filteredOptions()[this.activeIndex];
|
|
869
|
-
this.selectOption(option.getAttribute("value") ?? "", event);
|
|
870
|
-
}
|
|
871
|
-
else if (event.key === "Backspace" && this.hasAttribute("multiselect") && !this.currentValue && this.selectedOptionsState.length) {
|
|
872
|
-
event.preventDefault();
|
|
873
|
-
this.setOptionSelected(this.selectedOptionsState.at(-1), false, event);
|
|
874
|
-
}
|
|
875
|
-
else if (event.key === "Escape" && this.open) {
|
|
876
|
-
event.preventDefault();
|
|
877
|
-
event.stopPropagation();
|
|
878
|
-
this.typing = false;
|
|
879
|
-
const selected = this.optionElements().find(option => option.getAttribute("value") === this.selectedValue);
|
|
880
|
-
if (selected) {
|
|
881
|
-
this.currentValue = (selected.getAttribute("label") ?? selected.textContent ?? "").trim();
|
|
882
|
-
const input = this.getInput();
|
|
883
|
-
if (input)
|
|
884
|
-
input.value = this.currentValue;
|
|
885
|
-
}
|
|
886
|
-
else if (!this.hasAttribute("allow-freeform")) {
|
|
887
|
-
this.currentValue = "";
|
|
888
|
-
const input = this.getInput();
|
|
889
|
-
if (input)
|
|
890
|
-
input.value = "";
|
|
891
|
-
}
|
|
892
|
-
this.dirty = false;
|
|
893
|
-
this.closeList(event);
|
|
894
|
-
this.syncFormState();
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
render() {
|
|
898
|
-
const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
899
|
-
const id = `combo-${this.instanceId}`;
|
|
900
|
-
const label = document.createElement("label");
|
|
901
|
-
label.htmlFor = this.hasAttribute("button-trigger") ? id : id + "-input";
|
|
902
|
-
label.id = id + "-label";
|
|
903
|
-
label.textContent = this.getAttribute("label") ?? "";
|
|
904
|
-
label.part.add("label");
|
|
905
|
-
const wrapper = document.createElement("div");
|
|
906
|
-
wrapper.className = "control";
|
|
907
|
-
const selected = document.createElement("div");
|
|
908
|
-
selected.className = "selected";
|
|
909
|
-
selected.hidden = true;
|
|
910
|
-
selected.setAttribute("aria-label", this.getAttribute("selected-label") ?? "Selected options");
|
|
911
|
-
const input = document.createElement("input");
|
|
912
|
-
input.id = id + "-input";
|
|
913
|
-
input.type = "text";
|
|
914
|
-
input.role = "combobox";
|
|
915
|
-
input.autocomplete = "off";
|
|
916
|
-
input.setAttribute("aria-autocomplete", "list");
|
|
917
|
-
input.setAttribute("aria-haspopup", "listbox");
|
|
918
|
-
input.value = this.currentValue;
|
|
919
|
-
input.placeholder = this.getAttribute("placeholder") ?? "";
|
|
920
|
-
input.readOnly = this.hasAttribute("read-only");
|
|
921
|
-
input.disabled = this.hasAttribute("disabled");
|
|
922
|
-
input.required = this.hasAttribute("required");
|
|
923
|
-
for (const name of ["aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"]) {
|
|
924
|
-
const value = this.getAttribute(name);
|
|
925
|
-
if (value !== null)
|
|
926
|
-
input.setAttribute(name, value);
|
|
927
|
-
}
|
|
928
|
-
input.part.add("control");
|
|
929
|
-
this.fallbackInput = input;
|
|
930
|
-
const inputSlot = document.createElement("slot");
|
|
931
|
-
inputSlot.name = "input";
|
|
932
|
-
inputSlot.append(input);
|
|
933
|
-
this.inputSlot = inputSlot;
|
|
934
|
-
inputSlot.addEventListener("slotchange", () => { this.bindInput(); this.syncAria(); this.syncFormState(); });
|
|
935
|
-
const triggerSlot = document.createElement("slot");
|
|
936
|
-
triggerSlot.name = "button";
|
|
937
|
-
triggerSlot.hidden = !this.hasAttribute("button-trigger");
|
|
938
|
-
this.buttonTriggerSlot = triggerSlot;
|
|
939
|
-
const trigger = document.createElement("button");
|
|
940
|
-
trigger.type = "button";
|
|
941
|
-
trigger.id = id;
|
|
942
|
-
trigger.className = "trigger";
|
|
943
|
-
trigger.setAttribute("role", "combobox");
|
|
944
|
-
trigger.setAttribute("aria-haspopup", "listbox");
|
|
945
|
-
this.fallbackButtonTrigger = trigger;
|
|
946
|
-
const triggerValue = document.createElement("span");
|
|
947
|
-
triggerValue.className = "trigger-value";
|
|
948
|
-
trigger.append(triggerValue);
|
|
949
|
-
trigger.addEventListener("click", event => this.toggleOpen(event));
|
|
950
|
-
trigger.addEventListener("keydown", event => this.onKeyDown(event));
|
|
951
|
-
if (this.hasAttribute("button-trigger"))
|
|
952
|
-
triggerSlot.append(trigger);
|
|
953
|
-
triggerSlot.addEventListener("slotchange", () => {
|
|
954
|
-
if (triggerSlot.assignedElements({ flatten: true }).length)
|
|
955
|
-
trigger.remove();
|
|
956
|
-
else if (this.hasAttribute("button-trigger") && trigger.parentElement !== triggerSlot)
|
|
957
|
-
triggerSlot.append(trigger);
|
|
958
|
-
this.syncAria();
|
|
959
|
-
});
|
|
960
|
-
const clear = document.createElement("button");
|
|
961
|
-
clear.type = "button";
|
|
962
|
-
clear.className = "clear";
|
|
963
|
-
const clearIcon = document.createElement("slot");
|
|
964
|
-
clearIcon.name = "clear-icon";
|
|
965
|
-
const fallbackClearIcon = document.createElement("span");
|
|
966
|
-
fallbackClearIcon.textContent = "×";
|
|
967
|
-
fallbackClearIcon.setAttribute("aria-hidden", "true");
|
|
968
|
-
clearIcon.append(fallbackClearIcon);
|
|
969
|
-
clear.append(clearIcon);
|
|
970
|
-
clear.setAttribute("aria-label", this.getAttribute("clear-label") ?? "Clear selection");
|
|
971
|
-
clear.hidden = !this.hasAttribute("clearable");
|
|
972
|
-
clear.addEventListener("pointerdown", event => event.preventDefault());
|
|
973
|
-
clear.addEventListener("click", event => this.clearSelection(event));
|
|
974
|
-
const clearSlot = document.createElement("slot");
|
|
975
|
-
clearSlot.name = "clear-button";
|
|
976
|
-
clearSlot.append(clear);
|
|
977
|
-
clearSlot.addEventListener("slotchange", () => this.syncClearButton());
|
|
978
|
-
const expand = document.createElement("span");
|
|
979
|
-
expand.className = "expand";
|
|
980
|
-
expand.setAttribute("aria-hidden", "true");
|
|
981
|
-
const expandSlot = document.createElement("slot");
|
|
982
|
-
expandSlot.name = "expand-icon";
|
|
983
|
-
expandSlot.addEventListener("slotchange", () => this.syncExpandIcon());
|
|
984
|
-
const fallbackExpand = document.createElement("span");
|
|
985
|
-
fallbackExpand.textContent = "⌄";
|
|
986
|
-
expandSlot.append(fallbackExpand);
|
|
987
|
-
expand.append(expandSlot);
|
|
988
|
-
const list = document.createElement("ul");
|
|
989
|
-
list.setAttribute("role", "listbox");
|
|
990
|
-
list.id = `listbox-${this.instanceId}`;
|
|
991
|
-
const listboxSlot = document.createElement("slot");
|
|
992
|
-
listboxSlot.name = "listbox";
|
|
993
|
-
listboxSlot.append(list);
|
|
994
|
-
listboxSlot.addEventListener("slotchange", () => this.updateOptions());
|
|
995
|
-
const empty = document.createElement("div");
|
|
996
|
-
empty.className = "empty";
|
|
997
|
-
empty.setAttribute("role", "status");
|
|
998
|
-
empty.hidden = true;
|
|
999
|
-
wrapper.append(selected, triggerSlot, inputSlot, expand, clearSlot, listboxSlot, empty);
|
|
1000
|
-
const style = document.createElement("style");
|
|
1001
|
-
style.textContent = `${base}:host{display:grid;gap:var(--spui-control-gap,6px)}label{font-size:14px;font-weight:600;color:var(--spui-ui-muted,var(--spui-semantic-body-text,inherit))}.control{position:relative;display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:4px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,var(--colorNeutralStroke1,#8a8886))));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,var(--colorNeutralBackground1,#fff))));box-shadow:var(--spui-control-shadow,none)}.selected{display:flex;flex-wrap:wrap;gap:4px;min-width:0}.selected[hidden]{display:none}.tag{display:inline-flex;align-items:center;gap:4px;padding:2px 4px 2px 8px;border:1px solid var(--spui-tag-border,var(--spui-ui-stroke,#d1d1d1));border-radius:var(--spui-tag-radius,4px);background:var(--spui-tag-background,var(--spui-ui-surface-hover,#f5f5f5));color:var(--spui-ui-text,inherit);font-size:.9em}.remove,.clear{width:24px;height:24px;border:0;border-radius:3px;background:transparent;color:inherit;cursor:pointer}.remove:hover,.clear:hover{background:var(--spui-ui-surface-hover,#f5f5f5)}input{box-sizing:border-box;flex:1 1 120px;min-width:80px;min-height:calc(var(--spui-control-height,36px) - 10px);padding:4px 8px;border:0;outline:0;background:transparent;color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font:inherit}input:focus-visible{outline:2px solid var(--spui-focus-ring,#005a9e);outline-offset:1px}input:disabled{opacity:.55}ul{position:absolute;z-index:var(--spui-layer-dropdown,20);top:calc(100% + 4px);inset-inline:0;box-sizing:border-box;max-height:240px;overflow:auto;margin:0;padding:4px;list-style:none;border:1px solid var(--spui-ui-stroke,#d1d1d1);border-radius:var(--spui-card-radius,8px);background:var(--spui-ui-surface,#fff);box-shadow:var(--spui-card-shadow,0 4px 14px #0002)}:host([positioning^=above]) ul{top:auto;bottom:calc(100% + 4px)}ul[hidden]{display:none}.option-group{display:grid;gap:2px;padding-block:2px}.group-label{padding:6px 10px;color:var(--spui-ui-muted,inherit);font-size:12px;font-weight:600}[role=option],li.empty{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:var(--spui-control-radius,4px);color:var(--spui-ui-text,inherit);cursor:pointer}[role=option][data-active=true],[role=option][aria-selected=true]{background:var(--spui-ui-surface-hover,var(--spui-ui-canvas,#f5f5f5))}.check{display:inline-grid;place-items:center;width:16px;height:16px;flex:none;border:1px solid var(--spui-ui-stroke,#8a8886);border-radius:3px;color:var(--spui-ui-accent,#0f6cbd);font-size:12px}[role=option][aria-selected=true] .check{border-color:var(--spui-ui-accent,#0f6cbd);background:var(--spui-ui-canvas,#f5f5f5)}[role=option][aria-disabled=true]{opacity:.5;cursor:not-allowed}li.empty{color:var(--spui-ui-muted,inherit);cursor:default}:host([size=small]) input{min-height:30px}:host([size=large]) input{min-height:44px}:host([appearance=filled-darker]) input{background:var(--spui-ui-surface-sunken,#f0f0f0)}:host([appearance=filled-lighter]) input{background:var(--spui-ui-surface,#fff)}:host([appearance=underline]) input{border-radius:0;border-width:0 0 1px}@media(forced-colors:active){.control,ul{border-color:CanvasText;box-shadow:none}.tag{border-color:CanvasText}.check{border-color:CanvasText}li[role=option][data-active=true]{outline:1px solid Highlight}}:host(:not([multiselect])) .control{display:block;gap:0;padding:0;border:0;background:transparent;box-shadow:none}:host(:not([multiselect])) input{box-sizing:border-box;width:100%;min-height:var(--spui-control-height,36px);padding:8px 12px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,#8a8886)));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,#fff)));box-shadow:var(--spui-control-shadow,none);color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font:inherit}:host(:not([multiselect])) input:focus-visible{border-width:2px;border-color:var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline:0}:host([appearance=underline]:not([multiselect])) input:focus-visible{border-width:0 0 2px}:host([multiselect]) .control:focus-within{border-width:2px;border-color:var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));box-shadow:none}:host([multiselect]) .control input:focus-visible{outline:0}:host([aria-invalid]:not([aria-invalid=false])) .control,:host([aria-invalid]:not([aria-invalid=false])) input,:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=input]),:host([aria-invalid]:not([aria-invalid=false])) .trigger,:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=button]){border-color:var(--spui-input-invalid-border,var(--spui-ui-danger,var(--colorPaletteRedBorder2,#c50f1f)))}:host([aria-invalid]:not([aria-invalid=false])) input:focus-visible,:host([aria-invalid]:not([aria-invalid=false])) .trigger:focus-visible,:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=input]:focus-visible),:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=button]:focus-visible){border-color:var(--spui-input-invalid-border,var(--spui-ui-danger,var(--colorPaletteRedBorder2,#c50f1f)))}@media(forced-colors:active){:host(:not([multiselect])) input{border-color:FieldText;background:Field;color:FieldText}:host(:not([multiselect])) input:focus-visible{border-color:Highlight}:host([multiselect]) .control:focus-within{border-color:Highlight}:host([aria-invalid]:not([aria-invalid=false])) .control,:host([aria-invalid]:not([aria-invalid=false])) input,:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=input]),:host([aria-invalid]:not([aria-invalid=false])) .trigger,:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=button]){border-color:CanvasText}:host([aria-invalid]:not([aria-invalid=false])) input:focus-visible,:host([aria-invalid]:not([aria-invalid=false])) .trigger:focus-visible,:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=input]:focus-visible),:host([aria-invalid]:not([aria-invalid=false])) ::slotted([slot=button]:focus-visible){border-color:Highlight!important}:host(:not([multiselect])) ::slotted([slot=input]:focus-visible){border-color:Highlight!important}:host([button-trigger]) ::slotted([slot=button]:focus-visible){border-color:Highlight!important}}`;
|
|
1002
|
-
style.textContent += ".expand{display:inline-flex;align-items:center;justify-content:center;flex:none;color:inherit}.expand[hidden]{display:none}::slotted([slot=clear-button]){cursor:pointer}:host([read-only]) .expand,:host([button-trigger]) .expand{position:absolute;inset-inline-end:6px;top:6px}:host([read-only]) .clear,:host([read-only]) ::slotted([slot=clear-button]),:host([button-trigger]) .clear,:host([button-trigger]) ::slotted([slot=clear-button]){position:absolute;inset-inline-end:32px;top:6px}:host([read-only]) input{width:calc(100% - 56px)}:host([read-only]:not([clearable])) input{width:calc(100% - 28px)}.trigger,::slotted([slot=button]){box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:var(--spui-control-height,36px);padding:8px 12px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,#8a8886)));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,#fff)));box-shadow:var(--spui-control-shadow,none);color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font:inherit;text-align:start;cursor:pointer}.trigger:focus-visible{border-width:2px;border-color:var(--spui-focus-ring,var(--spui-semantic-focus-border,#005a9e));outline:0;outline-offset:0;box-shadow:none}::slotted([slot=button]:focus-visible){border-width:2px!important;border-color:var(--spui-focus-ring,var(--spui-semantic-focus-border,#005a9e))!important;outline:0!important;outline-offset:0;box-shadow:none!important}:host([appearance=underline]) .trigger:focus-visible,:host([appearance=underline]) ::slotted([slot=button]:focus-visible){border-width:0 0 2px!important}.trigger:disabled,::slotted([slot=button][disabled]){opacity:.55;cursor:not-allowed}:host([button-trigger][size=small]) .trigger,:host([button-trigger][size=small]) ::slotted([slot=button]){min-height:30px}:host([button-trigger][size=large]) .trigger,:host([button-trigger][size=large]) ::slotted([slot=button]){min-height:44px}:host([button-trigger]) .trigger-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media(forced-colors:active){.trigger,:host([button-trigger]) ::slotted([slot=button]){border-color:ButtonText;background:ButtonFace;color:ButtonText}.trigger:focus-visible,:host([button-trigger]) ::slotted([slot=button]:focus-visible){border-color:Highlight!important;outline:0!important}}";
|
|
1003
|
-
style.textContent = style.textContent?.replaceAll("li.empty", ".empty") ?? "";
|
|
1004
|
-
style.textContent += `::slotted([slot=input]){box-sizing:border-box;flex:1 1 120px;min-width:80px;min-height:calc(var(--spui-control-height,36px) - 10px);padding:4px 8px;border:0;outline:0;background:transparent;color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font:inherit}:host([multiselect]) ::slotted([slot=input]){border:0!important;outline:0!important;box-shadow:none!important}::slotted([slot=input]:focus-visible){border:0!important;outline:0!important;outline-offset:0;box-shadow:none!important}:host(:not([multiselect])) ::slotted([slot=input]:focus-visible){border-style:solid!important;border-width:2px!important;border-color:var(--spui-focus-ring,var(--spui-ui-accent,#0078d4))!important}:host([appearance=underline]:not([multiselect])) ::slotted([slot=input]:focus-visible){border-width:0 0 2px!important}:host(:not([multiselect])) ::slotted([slot=input]){box-sizing:border-box;width:100%;min-height:var(--spui-control-height,36px);padding:8px 12px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,#8a8886)));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,#fff)));color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font:inherit}`;
|
|
1005
|
-
shadow.replaceChildren(style, label, wrapper);
|
|
1006
|
-
this.bindInput();
|
|
1007
|
-
this.syncExpandIcon();
|
|
1008
|
-
this.updateOptions();
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
customElements.define("spui-combobox", SpuiCombobox);
|
|
1012
|
-
}
|