@spfx-fast/sharepoint-ui 0.4.2 → 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.
Files changed (276) hide show
  1. package/QUICKSTART.md +18 -0
  2. package/README.md +19 -24
  3. package/components.json +240 -5694
  4. package/dist/accordion.d.ts +11 -0
  5. package/dist/accordion.js +4 -0
  6. package/dist/alert.d.ts +3 -0
  7. package/dist/alert.js +4 -0
  8. package/dist/attachment-list.d.ts +15 -0
  9. package/dist/attachment-list.js +14 -0
  10. package/dist/avatar-group.d.ts +12 -0
  11. package/dist/avatar-group.js +7 -0
  12. package/dist/avatar.d.ts +6 -0
  13. package/dist/avatar.js +5 -0
  14. package/dist/badge.d.ts +3 -0
  15. package/dist/badge.js +4 -0
  16. package/dist/breadcrumb.d.ts +10 -0
  17. package/dist/breadcrumb.js +9 -0
  18. package/dist/button-group.d.ts +6 -0
  19. package/dist/button-group.js +4 -0
  20. package/dist/button.d.ts +3 -0
  21. package/dist/button.js +4 -0
  22. package/dist/card.d.ts +3 -0
  23. package/dist/card.js +4 -0
  24. package/dist/carousel.d.ts +11 -0
  25. package/dist/carousel.js +6 -0
  26. package/dist/checkbox.d.ts +3 -0
  27. package/dist/checkbox.js +4 -0
  28. package/dist/combobox.d.ts +9 -0
  29. package/dist/combobox.js +6 -0
  30. package/dist/command-bar.d.ts +6 -0
  31. package/dist/command-bar.js +4 -0
  32. package/dist/copy-button.d.ts +7 -0
  33. package/dist/copy-button.js +19 -0
  34. package/dist/data-grid.d.ts +25 -0
  35. package/dist/data-grid.js +39 -0
  36. package/dist/date-range-picker.d.ts +12 -0
  37. package/dist/date-range-picker.js +4 -0
  38. package/dist/dialog.d.ts +9 -0
  39. package/dist/dialog.js +21 -0
  40. package/dist/divider.d.ts +3 -0
  41. package/dist/divider.js +4 -0
  42. package/dist/drawer.d.ts +6 -0
  43. package/dist/drawer.js +5 -0
  44. package/dist/empty-state.d.ts +7 -0
  45. package/dist/empty-state.js +5 -0
  46. package/dist/field.d.ts +15 -0
  47. package/dist/field.js +11 -0
  48. package/dist/file-dropzone.d.ts +15 -0
  49. package/dist/file-dropzone.js +21 -0
  50. package/dist/filter-bar.d.ts +9 -0
  51. package/dist/filter-bar.js +9 -0
  52. package/dist/image.d.ts +5 -0
  53. package/dist/image.js +4 -0
  54. package/dist/index.d.ts +68 -76
  55. package/dist/index.js +60 -66
  56. package/dist/input.d.ts +3 -0
  57. package/dist/input.js +4 -0
  58. package/dist/label.d.ts +3 -0
  59. package/dist/label.js +4 -0
  60. package/dist/link.d.ts +3 -0
  61. package/dist/link.js +9 -0
  62. package/dist/list-view.d.ts +29 -0
  63. package/dist/list-view.js +14 -0
  64. package/dist/list.d.ts +6 -0
  65. package/dist/list.js +5 -0
  66. package/dist/lookup-picker.d.ts +12 -0
  67. package/dist/lookup-picker.js +4 -0
  68. package/dist/menu.d.ts +6 -0
  69. package/dist/menu.js +4 -0
  70. package/dist/nav.d.ts +12 -0
  71. package/dist/nav.js +4 -0
  72. package/dist/pagination.d.ts +11 -0
  73. package/dist/pagination.js +5 -0
  74. package/dist/person-picker.d.ts +13 -0
  75. package/dist/person-picker.js +4 -0
  76. package/dist/persona.d.ts +7 -0
  77. package/dist/persona.js +5 -0
  78. package/dist/popover.d.ts +6 -0
  79. package/dist/popover.js +4 -0
  80. package/dist/progress-bar.d.ts +5 -0
  81. package/dist/progress-bar.js +5 -0
  82. package/dist/radio-group.d.ts +6 -0
  83. package/dist/radio-group.js +4 -0
  84. package/dist/radio.d.ts +3 -0
  85. package/dist/radio.js +4 -0
  86. package/dist/rating.d.ts +8 -0
  87. package/dist/rating.js +5 -0
  88. package/dist/react.d.ts +1 -4583
  89. package/dist/react.js +1 -6837
  90. package/dist/select.d.ts +3 -0
  91. package/dist/select.js +4 -0
  92. package/dist/sharepoint-field.d.ts +18 -0
  93. package/dist/sharepoint-field.js +54 -0
  94. package/dist/skeleton.d.ts +6 -0
  95. package/dist/skeleton.js +9 -0
  96. package/dist/spinner.d.ts +5 -0
  97. package/dist/spinner.js +4 -0
  98. package/dist/swatch-picker.d.ts +17 -0
  99. package/dist/swatch-picker.js +11 -0
  100. package/dist/switch.d.ts +3 -0
  101. package/dist/switch.js +4 -0
  102. package/dist/table.d.ts +6 -0
  103. package/dist/table.js +4 -0
  104. package/dist/tabs.d.ts +14 -0
  105. package/dist/tabs.js +30 -0
  106. package/dist/tag-group.d.ts +6 -0
  107. package/dist/tag-group.js +4 -0
  108. package/dist/tag-picker.d.ts +12 -0
  109. package/dist/tag-picker.js +4 -0
  110. package/dist/tag.d.ts +7 -0
  111. package/dist/tag.js +5 -0
  112. package/dist/text.d.ts +5 -0
  113. package/dist/text.js +4 -0
  114. package/dist/textarea.d.ts +3 -0
  115. package/dist/textarea.js +4 -0
  116. package/dist/theme.d.ts +15 -167
  117. package/dist/theme.js +36 -162
  118. package/dist/toast.d.ts +9 -0
  119. package/dist/toast.js +12 -0
  120. package/dist/tree.d.ts +13 -0
  121. package/dist/tree.js +9 -0
  122. package/dist/view-card.d.ts +8 -0
  123. package/dist/view-card.js +4 -0
  124. package/dist/view-switcher.d.ts +13 -0
  125. package/dist/view-switcher.js +7 -0
  126. package/dist/virtual-list.d.ts +11 -0
  127. package/dist/virtual-list.js +25 -0
  128. package/measurements.json +365 -0
  129. package/package.json +19 -42
  130. package/src/theme.css +125 -0
  131. package/AGENTS.md +0 -211
  132. package/dist/appearance.d.ts +0 -6
  133. package/dist/appearance.js +0 -217
  134. package/dist/elements/accordion.d.ts +0 -9
  135. package/dist/elements/accordion.js +0 -338
  136. package/dist/elements/alert.d.ts +0 -2
  137. package/dist/elements/alert.js +0 -62
  138. package/dist/elements/avatar-group-primitives.d.ts +0 -3
  139. package/dist/elements/avatar-group-primitives.js +0 -189
  140. package/dist/elements/avatar-group.d.ts +0 -2
  141. package/dist/elements/avatar-group.js +0 -110
  142. package/dist/elements/avatar.d.ts +0 -4
  143. package/dist/elements/avatar.js +0 -251
  144. package/dist/elements/badge.d.ts +0 -8
  145. package/dist/elements/badge.js +0 -182
  146. package/dist/elements/breadcrumb.d.ts +0 -4
  147. package/dist/elements/breadcrumb.js +0 -233
  148. package/dist/elements/button-group.d.ts +0 -1
  149. package/dist/elements/button-group.js +0 -20
  150. package/dist/elements/button-variants.d.ts +0 -6
  151. package/dist/elements/button-variants.js +0 -339
  152. package/dist/elements/button.d.ts +0 -5
  153. package/dist/elements/button.js +0 -233
  154. package/dist/elements/card-primitives.d.ts +0 -3
  155. package/dist/elements/card-primitives.js +0 -83
  156. package/dist/elements/card.d.ts +0 -5
  157. package/dist/elements/card.js +0 -213
  158. package/dist/elements/carousel.d.ts +0 -92
  159. package/dist/elements/carousel.js +0 -1112
  160. package/dist/elements/color-picker.d.ts +0 -24
  161. package/dist/elements/color-picker.js +0 -1004
  162. package/dist/elements/combobox.d.ts +0 -1
  163. package/dist/elements/combobox.js +0 -1012
  164. package/dist/elements/data-grid-model.d.ts +0 -117
  165. package/dist/elements/data-grid-model.js +0 -294
  166. package/dist/elements/data-grid.d.ts +0 -29
  167. package/dist/elements/data-grid.js +0 -1567
  168. package/dist/elements/date-picker.d.ts +0 -1
  169. package/dist/elements/date-picker.js +0 -486
  170. package/dist/elements/dialog.d.ts +0 -23
  171. package/dist/elements/dialog.js +0 -409
  172. package/dist/elements/divider.d.ts +0 -2
  173. package/dist/elements/divider.js +0 -66
  174. package/dist/elements/drawer.d.ts +0 -3
  175. package/dist/elements/drawer.js +0 -308
  176. package/dist/elements/field-composition.d.ts +0 -26
  177. package/dist/elements/field-composition.js +0 -133
  178. package/dist/elements/field.d.ts +0 -1
  179. package/dist/elements/field.js +0 -67
  180. package/dist/elements/file-upload.d.ts +0 -6
  181. package/dist/elements/file-upload.js +0 -211
  182. package/dist/elements/floating-position.d.ts +0 -26
  183. package/dist/elements/floating-position.js +0 -39
  184. package/dist/elements/image.d.ts +0 -28
  185. package/dist/elements/image.js +0 -38
  186. package/dist/elements/info.d.ts +0 -9
  187. package/dist/elements/info.js +0 -102
  188. package/dist/elements/input.d.ts +0 -5
  189. package/dist/elements/input.js +0 -255
  190. package/dist/elements/label.d.ts +0 -15
  191. package/dist/elements/label.js +0 -56
  192. package/dist/elements/link.d.ts +0 -1
  193. package/dist/elements/link.js +0 -92
  194. package/dist/elements/list.d.ts +0 -19
  195. package/dist/elements/list.js +0 -297
  196. package/dist/elements/menu.d.ts +0 -33
  197. package/dist/elements/menu.js +0 -1103
  198. package/dist/elements/message-bar.d.ts +0 -10
  199. package/dist/elements/message-bar.js +0 -299
  200. package/dist/elements/nav.d.ts +0 -32
  201. package/dist/elements/nav.js +0 -791
  202. package/dist/elements/overflow.d.ts +0 -9
  203. package/dist/elements/overflow.js +0 -218
  204. package/dist/elements/persona.d.ts +0 -3
  205. package/dist/elements/persona.js +0 -125
  206. package/dist/elements/popover.d.ts +0 -77
  207. package/dist/elements/popover.js +0 -840
  208. package/dist/elements/progress-bar.d.ts +0 -4
  209. package/dist/elements/progress-bar.js +0 -82
  210. package/dist/elements/radio-group.d.ts +0 -1
  211. package/dist/elements/radio-group.js +0 -303
  212. package/dist/elements/rating-display.d.ts +0 -2
  213. package/dist/elements/rating-display.js +0 -125
  214. package/dist/elements/rating.d.ts +0 -2
  215. package/dist/elements/rating.js +0 -206
  216. package/dist/elements/rich-text-editor.d.ts +0 -18
  217. package/dist/elements/rich-text-editor.js +0 -508
  218. package/dist/elements/search.d.ts +0 -3
  219. package/dist/elements/search.js +0 -207
  220. package/dist/elements/select.d.ts +0 -6
  221. package/dist/elements/select.js +0 -291
  222. package/dist/elements/shared.d.ts +0 -19
  223. package/dist/elements/shared.js +0 -396
  224. package/dist/elements/skeleton.d.ts +0 -6
  225. package/dist/elements/skeleton.js +0 -91
  226. package/dist/elements/slider.d.ts +0 -1
  227. package/dist/elements/slider.js +0 -248
  228. package/dist/elements/spin-button.d.ts +0 -7
  229. package/dist/elements/spin-button.js +0 -499
  230. package/dist/elements/spinner.d.ts +0 -4
  231. package/dist/elements/spinner.js +0 -124
  232. package/dist/elements/swatch-picker.d.ts +0 -5
  233. package/dist/elements/swatch-picker.js +0 -363
  234. package/dist/elements/table.d.ts +0 -21
  235. package/dist/elements/table.js +0 -261
  236. package/dist/elements/tabs.d.ts +0 -1
  237. package/dist/elements/tabs.js +0 -276
  238. package/dist/elements/tag-picker.d.ts +0 -29
  239. package/dist/elements/tag-picker.js +0 -697
  240. package/dist/elements/tags.d.ts +0 -21
  241. package/dist/elements/tags.js +0 -421
  242. package/dist/elements/teaching-popover.d.ts +0 -32
  243. package/dist/elements/teaching-popover.js +0 -412
  244. package/dist/elements/text.d.ts +0 -9
  245. package/dist/elements/text.js +0 -75
  246. package/dist/elements/textarea.d.ts +0 -1
  247. package/dist/elements/textarea.js +0 -182
  248. package/dist/elements/toast.d.ts +0 -25
  249. package/dist/elements/toast.js +0 -337
  250. package/dist/elements/toaster.d.ts +0 -6
  251. package/dist/elements/toaster.js +0 -356
  252. package/dist/elements/toggle.d.ts +0 -2
  253. package/dist/elements/toggle.js +0 -246
  254. package/dist/elements/toolbar.d.ts +0 -12
  255. package/dist/elements/toolbar.js +0 -381
  256. package/dist/elements/tooltip.d.ts +0 -76
  257. package/dist/elements/tooltip.js +0 -510
  258. package/dist/elements/tree-model.d.ts +0 -19
  259. package/dist/elements/tree-model.js +0 -34
  260. package/dist/elements/tree.d.ts +0 -5
  261. package/dist/elements/tree.js +0 -273
  262. package/dist/elements/view-card.d.ts +0 -2
  263. package/dist/elements/view-card.js +0 -78
  264. package/dist/elements/virtual-list.d.ts +0 -41
  265. package/dist/elements/virtual-list.js +0 -369
  266. package/dist/elements.d.ts +0 -72
  267. package/dist/elements.js +0 -207
  268. package/dist/virtualizer-hooks.d.ts +0 -80
  269. package/dist/virtualizer-hooks.js +0 -253
  270. package/scripts/generate-token-index.mjs +0 -43
  271. package/scripts/sync-component-catalog-signatures.mjs +0 -55
  272. package/scripts/validate-component-catalog.mjs +0 -44
  273. package/skills/sharepoint-ui/SKILL.md +0 -12
  274. package/skills/sharepoint-ui/references/choosing-components.md +0 -20
  275. package/skills/sharepoint-ui/references/sharepoint-patterns.md +0 -17
  276. 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", "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 ? "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{outline:2px solid var(--spui-focus-ring,#005a9e);outline-offset:2px}:host([multiselect]) .control:focus-within{border-color:var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));box-shadow:0 0 0 1px var(--spui-focus-ring,var(--spui-ui-accent,#0078d4))}@media(forced-colors:active){:host(:not([multiselect])) input{border-color:FieldText;background:Field;color:FieldText}}`;
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,::slotted([slot=button]:focus-visible){outline:2px solid var(--spui-focus-ring,var(--spui-semantic-focus-border,#005a9e));outline-offset:2px}.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){outline-color:Highlight}}";
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}::slotted([slot=input]:focus-visible){outline:2px solid var(--spui-focus-ring,#005a9e);outline-offset:1px}: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
- }