@starwind-ui/core 1.12.0 → 1.12.1

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 (166) hide show
  1. package/package.json +1 -1
  2. package/dist/index.d.ts +0 -28
  3. package/dist/index.js +0 -81
  4. package/dist/index.js.map +0 -1
  5. package/dist/src/components/accordion/Accordion.astro +0 -247
  6. package/dist/src/components/accordion/AccordionContent.astro +0 -33
  7. package/dist/src/components/accordion/AccordionItem.astro +0 -27
  8. package/dist/src/components/accordion/AccordionTrigger.astro +0 -32
  9. package/dist/src/components/accordion/index.ts +0 -15
  10. package/dist/src/components/alert/Alert.astro +0 -31
  11. package/dist/src/components/alert/AlertDescription.astro +0 -14
  12. package/dist/src/components/alert/AlertTitle.astro +0 -16
  13. package/dist/src/components/alert/index.ts +0 -13
  14. package/dist/src/components/alert-dialog/AlertDialog.astro +0 -273
  15. package/dist/src/components/alert-dialog/AlertDialogAction.astro +0 -44
  16. package/dist/src/components/alert-dialog/AlertDialogCancel.astro +0 -45
  17. package/dist/src/components/alert-dialog/AlertDialogContent.astro +0 -52
  18. package/dist/src/components/alert-dialog/AlertDialogDescription.astro +0 -18
  19. package/dist/src/components/alert-dialog/AlertDialogFooter.astro +0 -16
  20. package/dist/src/components/alert-dialog/AlertDialogHeader.astro +0 -14
  21. package/dist/src/components/alert-dialog/AlertDialogTitle.astro +0 -20
  22. package/dist/src/components/alert-dialog/AlertDialogTrigger.astro +0 -47
  23. package/dist/src/components/alert-dialog/index.ts +0 -46
  24. package/dist/src/components/aspect-ratio/AspectRatio.astro +0 -32
  25. package/dist/src/components/aspect-ratio/index.ts +0 -7
  26. package/dist/src/components/avatar/Avatar.astro +0 -29
  27. package/dist/src/components/avatar/AvatarFallback.astro +0 -18
  28. package/dist/src/components/avatar/AvatarImage.astro +0 -49
  29. package/dist/src/components/avatar/index.ts +0 -13
  30. package/dist/src/components/badge/Badge.astro +0 -51
  31. package/dist/src/components/badge/index.ts +0 -7
  32. package/dist/src/components/breadcrumb/Breadcrumb.astro +0 -11
  33. package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +0 -28
  34. package/dist/src/components/breadcrumb/BreadcrumbItem.astro +0 -14
  35. package/dist/src/components/breadcrumb/BreadcrumbLink.astro +0 -22
  36. package/dist/src/components/breadcrumb/BreadcrumbList.astro +0 -16
  37. package/dist/src/components/breadcrumb/BreadcrumbPage.astro +0 -21
  38. package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +0 -23
  39. package/dist/src/components/breadcrumb/index.ts +0 -37
  40. package/dist/src/components/button/Button.astro +0 -53
  41. package/dist/src/components/button/index.ts +0 -7
  42. package/dist/src/components/button-group/ButtonGroup.astro +0 -62
  43. package/dist/src/components/button-group/ButtonGroupSeparator.astro +0 -27
  44. package/dist/src/components/button-group/ButtonGroupText.astro +0 -19
  45. package/dist/src/components/button-group/index.ts +0 -17
  46. package/dist/src/components/card/Card.astro +0 -14
  47. package/dist/src/components/card/CardContent.astro +0 -14
  48. package/dist/src/components/card/CardDescription.astro +0 -14
  49. package/dist/src/components/card/CardFooter.astro +0 -14
  50. package/dist/src/components/card/CardHeader.astro +0 -14
  51. package/dist/src/components/card/CardTitle.astro +0 -14
  52. package/dist/src/components/card/index.ts +0 -26
  53. package/dist/src/components/carousel/Carousel.astro +0 -55
  54. package/dist/src/components/carousel/CarouselContent.astro +0 -26
  55. package/dist/src/components/carousel/CarouselItem.astro +0 -26
  56. package/dist/src/components/carousel/CarouselNext.astro +0 -37
  57. package/dist/src/components/carousel/CarouselPrevious.astro +0 -37
  58. package/dist/src/components/carousel/carousel-script.ts +0 -191
  59. package/dist/src/components/carousel/index.ts +0 -32
  60. package/dist/src/components/checkbox/Checkbox.astro +0 -127
  61. package/dist/src/components/checkbox/index.ts +0 -7
  62. package/dist/src/components/dialog/Dialog.astro +0 -263
  63. package/dist/src/components/dialog/DialogClose.astro +0 -35
  64. package/dist/src/components/dialog/DialogContent.astro +0 -67
  65. package/dist/src/components/dialog/DialogDescription.astro +0 -14
  66. package/dist/src/components/dialog/DialogFooter.astro +0 -14
  67. package/dist/src/components/dialog/DialogHeader.astro +0 -14
  68. package/dist/src/components/dialog/DialogTitle.astro +0 -20
  69. package/dist/src/components/dialog/DialogTrigger.astro +0 -47
  70. package/dist/src/components/dialog/index.ts +0 -45
  71. package/dist/src/components/dropdown/Dropdown.astro +0 -375
  72. package/dist/src/components/dropdown/DropdownContent.astro +0 -81
  73. package/dist/src/components/dropdown/DropdownItem.astro +0 -48
  74. package/dist/src/components/dropdown/DropdownLabel.astro +0 -29
  75. package/dist/src/components/dropdown/DropdownSeparator.astro +0 -21
  76. package/dist/src/components/dropdown/DropdownTrigger.astro +0 -52
  77. package/dist/src/components/dropdown/index.ts +0 -33
  78. package/dist/src/components/dropzone/Dropzone.astro +0 -233
  79. package/dist/src/components/dropzone/DropzoneFilesList.astro +0 -26
  80. package/dist/src/components/dropzone/DropzoneLoadingIndicator.astro +0 -10
  81. package/dist/src/components/dropzone/DropzoneUploadIndicator.astro +0 -10
  82. package/dist/src/components/dropzone/index.ts +0 -24
  83. package/dist/src/components/input/Input.astro +0 -24
  84. package/dist/src/components/input/index.ts +0 -7
  85. package/dist/src/components/item/Item.astro +0 -52
  86. package/dist/src/components/item/ItemActions.astro +0 -16
  87. package/dist/src/components/item/ItemContent.astro +0 -16
  88. package/dist/src/components/item/ItemDescription.astro +0 -19
  89. package/dist/src/components/item/ItemFooter.astro +0 -16
  90. package/dist/src/components/item/ItemGroup.astro +0 -16
  91. package/dist/src/components/item/ItemHeader.astro +0 -16
  92. package/dist/src/components/item/ItemMedia.astro +0 -40
  93. package/dist/src/components/item/ItemSeparator.astro +0 -21
  94. package/dist/src/components/item/ItemTitle.astro +0 -16
  95. package/dist/src/components/item/index.ts +0 -50
  96. package/dist/src/components/kbd/Kbd.astro +0 -21
  97. package/dist/src/components/kbd/KbdGroup.astro +0 -16
  98. package/dist/src/components/kbd/index.ts +0 -11
  99. package/dist/src/components/label/Label.astro +0 -22
  100. package/dist/src/components/label/index.ts +0 -7
  101. package/dist/src/components/pagination/Pagination.astro +0 -20
  102. package/dist/src/components/pagination/PaginationContent.astro +0 -16
  103. package/dist/src/components/pagination/PaginationEllipsis.astro +0 -35
  104. package/dist/src/components/pagination/PaginationItem.astro +0 -16
  105. package/dist/src/components/pagination/PaginationLink.astro +0 -24
  106. package/dist/src/components/pagination/PaginationNext.astro +0 -30
  107. package/dist/src/components/pagination/PaginationPrevious.astro +0 -30
  108. package/dist/src/components/pagination/index.ts +0 -38
  109. package/dist/src/components/progress/Progress.astro +0 -154
  110. package/dist/src/components/progress/index.ts +0 -10
  111. package/dist/src/components/radio-group/RadioGroup.astro +0 -157
  112. package/dist/src/components/radio-group/RadioGroupItem.astro +0 -129
  113. package/dist/src/components/radio-group/RadioGroupTypes.ts +0 -6
  114. package/dist/src/components/radio-group/index.ts +0 -23
  115. package/dist/src/components/select/Select.astro +0 -696
  116. package/dist/src/components/select/SelectContent.astro +0 -94
  117. package/dist/src/components/select/SelectGroup.astro +0 -9
  118. package/dist/src/components/select/SelectItem.astro +0 -51
  119. package/dist/src/components/select/SelectLabel.astro +0 -14
  120. package/dist/src/components/select/SelectSearch.astro +0 -49
  121. package/dist/src/components/select/SelectSeparator.astro +0 -12
  122. package/dist/src/components/select/SelectTrigger.astro +0 -58
  123. package/dist/src/components/select/SelectTypes.ts +0 -13
  124. package/dist/src/components/select/SelectValue.astro +0 -19
  125. package/dist/src/components/select/index.ts +0 -49
  126. package/dist/src/components/separator/Separator.astro +0 -36
  127. package/dist/src/components/separator/index.ts +0 -7
  128. package/dist/src/components/sheet/Sheet.astro +0 -13
  129. package/dist/src/components/sheet/SheetClose.astro +0 -13
  130. package/dist/src/components/sheet/SheetContent.astro +0 -92
  131. package/dist/src/components/sheet/SheetDescription.astro +0 -16
  132. package/dist/src/components/sheet/SheetFooter.astro +0 -16
  133. package/dist/src/components/sheet/SheetHeader.astro +0 -16
  134. package/dist/src/components/sheet/SheetTitle.astro +0 -16
  135. package/dist/src/components/sheet/SheetTrigger.astro +0 -13
  136. package/dist/src/components/sheet/index.ts +0 -41
  137. package/dist/src/components/skeleton/Skeleton.astro +0 -14
  138. package/dist/src/components/skeleton/index.ts +0 -9
  139. package/dist/src/components/spinner/Spinner.astro +0 -21
  140. package/dist/src/components/spinner/index.ts +0 -7
  141. package/dist/src/components/switch/Switch.astro +0 -191
  142. package/dist/src/components/switch/SwitchTypes.ts +0 -6
  143. package/dist/src/components/switch/index.ts +0 -12
  144. package/dist/src/components/table/Table.astro +0 -18
  145. package/dist/src/components/table/TableBody.astro +0 -16
  146. package/dist/src/components/table/TableCaption.astro +0 -16
  147. package/dist/src/components/table/TableCell.astro +0 -16
  148. package/dist/src/components/table/TableFoot.astro +0 -16
  149. package/dist/src/components/table/TableHead.astro +0 -16
  150. package/dist/src/components/table/TableHeader.astro +0 -16
  151. package/dist/src/components/table/TableRow.astro +0 -16
  152. package/dist/src/components/table/index.ts +0 -42
  153. package/dist/src/components/tabs/Tabs.astro +0 -269
  154. package/dist/src/components/tabs/TabsContent.astro +0 -28
  155. package/dist/src/components/tabs/TabsList.astro +0 -22
  156. package/dist/src/components/tabs/TabsTrigger.astro +0 -34
  157. package/dist/src/components/tabs/index.ts +0 -20
  158. package/dist/src/components/textarea/Textarea.astro +0 -28
  159. package/dist/src/components/textarea/index.ts +0 -9
  160. package/dist/src/components/toggle/Toggle.astro +0 -172
  161. package/dist/src/components/toggle/ToggleTypes.ts +0 -14
  162. package/dist/src/components/toggle/index.ts +0 -8
  163. package/dist/src/components/tooltip/Tooltip.astro +0 -237
  164. package/dist/src/components/tooltip/TooltipContent.astro +0 -114
  165. package/dist/src/components/tooltip/TooltipTrigger.astro +0 -10
  166. package/dist/src/components/tooltip/index.ts +0 -16
@@ -1,696 +0,0 @@
1
- ---
2
- import type { HTMLAttributes } from "astro/types";
3
-
4
- type Props = HTMLAttributes<"div"> & {
5
- /**
6
- * The name for the hidden <select> element - used for standard form handling
7
- */
8
- name?: string;
9
- /**
10
- * The value of the item that should be selected by default
11
- */
12
- defaultValue?: string;
13
-
14
- children: any;
15
- };
16
-
17
- const { class: className, name, defaultValue, ...rest } = Astro.props;
18
- ---
19
-
20
- <div
21
- class:list={["starwind-select", "relative", className]}
22
- data-name={name}
23
- data-value={defaultValue}
24
- data-slot="select"
25
- {...rest}
26
- >
27
- <slot />
28
- </div>
29
-
30
- <script>
31
- import type { SelectChangeEvent, SelectEvent } from "./SelectTypes";
32
-
33
- class SelectHandler {
34
- private select: HTMLElement;
35
- private trigger: HTMLButtonElement | null;
36
- private content: HTMLElement | null;
37
- private searchInput: HTMLInputElement | null = null;
38
- private emptyElement: HTMLElement | null = null;
39
- private isOpen: boolean = false;
40
- private selectedItem: HTMLElement | null = null;
41
- private activeItem: HTMLElement | null = null;
42
- private animationDuration = 150;
43
- private typeaheadTimerRef: number | null = null;
44
- private typeaheadSearch = "";
45
- private returnFocusOnClose: boolean = true;
46
-
47
- constructor(select: HTMLElement, selectIdx: number) {
48
- this.select = select;
49
- this.trigger = select.querySelector(".starwind-select-trigger");
50
- this.content = select.querySelector(".starwind-select-content");
51
- this.searchInput = select.querySelector('[data-slot="select-search"]');
52
- this.emptyElement = select.querySelector('[data-slot="select-empty"]');
53
-
54
- if (!this.trigger || !this.content) return;
55
-
56
- // animationDuration is set with inline styles through passed prop to SelectContent
57
- const animationDurationString = this.content.style.animationDuration;
58
- if (animationDurationString.endsWith("ms")) {
59
- this.animationDuration = parseFloat(animationDurationString);
60
- } else if (animationDurationString.endsWith("s")) {
61
- // using something like @playform/compress might optimize to use "s" instead of "ms"
62
- this.animationDuration = parseFloat(animationDurationString) * 1000;
63
- }
64
-
65
- this.init(selectIdx);
66
- }
67
-
68
- private init(selectIdx: number) {
69
- this.setupAccessibility(selectIdx);
70
- this.setupEvents();
71
- this.setupSelectField();
72
- this.setInitialState();
73
- }
74
-
75
- private setupSelectField() {
76
- if (!this.trigger || !this.content) return;
77
- // build the standard select field
78
- const selectField = document.createElement("select");
79
- selectField.tabIndex = -1;
80
- selectField.setAttribute("aria-hidden", "true");
81
- selectField.setAttribute("placeholder", "select");
82
- const selectName = this.select.getAttribute("data-name");
83
- if (selectName) {
84
- selectField.name = selectName;
85
- }
86
-
87
- // you can comment out this "sr-only" class line below if you want to see the native select in action
88
- selectField.classList.add("starwind-sr-only");
89
-
90
- // The first option is a placeholder
91
- const placeholderOption = document.createElement("option");
92
- placeholderOption.value = "";
93
- placeholderOption.textContent = "Select";
94
- placeholderOption.disabled = true;
95
- placeholderOption.selected = true;
96
- selectField.appendChild(placeholderOption);
97
-
98
- // add all options to the select field
99
- this.content.querySelectorAll('[role="option"]').forEach((option) => {
100
- const optionValue = option.getAttribute("data-value");
101
- const optionText = option.textContent;
102
- const optionElement = document.createElement("option");
103
- optionElement.value = optionValue || "";
104
- optionElement.textContent = optionText || "";
105
- selectField.appendChild(optionElement);
106
- });
107
- this.trigger.appendChild(selectField);
108
-
109
- // add this select field right after the trigger
110
- this.trigger.parentElement?.insertBefore(selectField, this.trigger.nextSibling);
111
-
112
- this.setSize();
113
- this.content.style.width = "var(--starwind-select-trigger-width)";
114
- }
115
-
116
- private setupAccessibility(selectIdx: number) {
117
- if (!this.trigger || !this.content) return;
118
-
119
- // Generate unique IDs for accessibility
120
- this.trigger.id = `starwind-select${selectIdx}-trigger`;
121
- this.content.id = `starwind-select${selectIdx}-content`;
122
-
123
- // Set up additional ARIA attributes
124
- this.trigger.setAttribute("aria-controls", this.content.id);
125
- this.content.setAttribute("aria-labelledby", this.trigger.id);
126
-
127
- // If search input exists, add IDs to all items for aria-activedescendant
128
- if (this.searchInput) {
129
- if (!this.searchInput.id) {
130
- this.searchInput.id = `starwind-select${selectIdx}-search`;
131
- }
132
- // Link search input to the listbox it filters
133
- this.searchInput.setAttribute("aria-controls", this.content.id);
134
-
135
- const items = this.content.querySelectorAll('[role="option"]');
136
- items.forEach((item, index) => {
137
- if (!item.id) {
138
- item.id = `starwind-select${selectIdx}-option${index}`;
139
- }
140
- });
141
- }
142
- }
143
-
144
- private setupEvents() {
145
- if (!this.trigger || !this.content) return;
146
-
147
- // Handle search input if it exists
148
- if (this.searchInput) {
149
- this.setupSearchInput();
150
- }
151
-
152
- // Handle pointerdown
153
- this.trigger.addEventListener("pointerdown", (e) => {
154
- // prevent implicit pointer capture
155
- // https://www.w3.org/TR/pointerevents3/#implicit-pointer-capture
156
- const target = e.target as HTMLElement;
157
- if (target.hasPointerCapture(e.pointerId)) {
158
- target.releasePointerCapture(e.pointerId);
159
- }
160
-
161
- // prevent trigger from stealing focus from the active item after opening.
162
- e.preventDefault();
163
-
164
- // only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
165
- // but not when the control key is pressed (avoiding MacOS right click); also not for touch
166
- // devices because that would open the menu on scroll. (pen devices behave as touch on iOS).
167
- if (e.button === 0 && e.ctrlKey === false && e.pointerType === "mouse") {
168
- this.returnFocusOnClose = true;
169
- this.toggleSelect();
170
- }
171
- });
172
-
173
- // Handle click event for mobile devices
174
- this.trigger.addEventListener("click", (e) => {
175
- if (window.matchMedia("(pointer: coarse)").matches) {
176
- e.preventDefault();
177
- this.returnFocusOnClose = true;
178
- this.toggleSelect();
179
- }
180
- });
181
-
182
- // add enter or space key to select trigger to toggle select
183
- this.trigger.addEventListener("keydown", (e) => {
184
- if (e.key === "Enter" || e.key === " ") {
185
- e.preventDefault();
186
- this.returnFocusOnClose = true;
187
- this.toggleSelect();
188
- }
189
- });
190
-
191
- // Handle keyboard navigation inside select content
192
- this.content.addEventListener("keydown", (e) => {
193
- // Check if the event originated from the search input
194
- const isFromSearchInput = e.target === this.searchInput;
195
-
196
- if (e.key === "Enter" || e.key === " ") {
197
- // Only handle selection if not typing in search input
198
- if (!isFromSearchInput) {
199
- // set element based on current focused element
200
- const activeElement = document.activeElement;
201
- this.returnFocusOnClose = true;
202
- this.handleSelection(activeElement as HTMLElement);
203
- }
204
- // If from search input, don't handle it here - let the input handle it naturally
205
- } else if (e.key === "Escape" && this.isOpen) {
206
- this.returnFocusOnClose = true;
207
- this.closeSelect();
208
- }
209
-
210
- // add key navigation for accessibility
211
- // "Home" goes to first element
212
- // "End" goes to last element
213
- // "ArrowUp" goes to previous element
214
- // "ArrowDown" goes to next element
215
- else if (["ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) {
216
- this.handleNavigationKeys(e);
217
- e.preventDefault();
218
- } else {
219
- const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
220
-
221
- // select should not be navigated using tab key so we prevent it
222
- if (e.key === "Tab") e.preventDefault();
223
-
224
- if (!isModifierKey && e.key.length === 1 && !this.searchInput) {
225
- this.handleTypeahead(e.key);
226
- }
227
- }
228
- });
229
-
230
- // Handle hover on select items
231
- this.content.addEventListener("mouseover", (e) => {
232
- const target = e.target as HTMLElement;
233
- const option = target.closest('[role="option"]');
234
- if (option && option instanceof HTMLElement && this.isOpen === true) {
235
- this.setActiveItem(option);
236
- }
237
- });
238
-
239
- // handle pointerdown outside select content to close
240
- document.addEventListener("pointerdown", (e) => {
241
- // only close if not a mouse pointer
242
- if (!window.matchMedia("(pointer: coarse)").matches) {
243
- if (
244
- !(
245
- this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)
246
- ) &&
247
- this.isOpen
248
- ) {
249
- this.returnFocusOnClose = false;
250
- this.closeSelect();
251
- }
252
- }
253
- });
254
-
255
- // Handle click outside select content to close
256
- document.addEventListener("click", (e) => {
257
- if (
258
- !(this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)) &&
259
- this.isOpen
260
- ) {
261
- this.returnFocusOnClose = false;
262
- this.closeSelect();
263
- }
264
- });
265
-
266
- // Handle selection of items
267
- this.content?.addEventListener("click", (e) => {
268
- const item = (e.target as HTMLElement).closest("[role='option']");
269
- if (item instanceof HTMLElement) {
270
- this.returnFocusOnClose = true;
271
- this.handleSelection(item);
272
- }
273
- });
274
-
275
- // passive resize listener to call setSize()
276
- window.addEventListener("resize", () => this.setSize(), { passive: true });
277
-
278
- // Listen for programmatic selection events
279
- document.addEventListener("starwind-select:select", (e: Event) => {
280
- const selectEvent = e as SelectEvent;
281
- const selectId = selectEvent.detail.selectId;
282
- const selectName = selectEvent.detail.selectName;
283
- const selectValue = selectEvent.detail.value;
284
-
285
- // Check if this event is for this select
286
- if (
287
- (selectId && this.select.id === selectId) ||
288
- (selectName && this.select.getAttribute("data-name") === selectName)
289
- ) {
290
- this.programmaticallySelect(selectValue);
291
- }
292
- });
293
- }
294
-
295
- private setActiveItem(item: HTMLElement | null) {
296
- if (!this.content) return;
297
-
298
- // Remove active state from previous item
299
- if (this.activeItem) {
300
- this.activeItem.removeAttribute("data-active");
301
- }
302
-
303
- // Set new active item
304
- this.activeItem = item;
305
- if (item) {
306
- item.setAttribute("data-active", "true");
307
-
308
- // Scroll item into view if needed
309
- item.scrollIntoView({ block: "nearest" });
310
-
311
- // For search mode, set aria-activedescendant for assistive technologies
312
- if (this.searchInput) {
313
- // Item should already have an ID from setupAccessibility
314
- this.searchInput.setAttribute("aria-activedescendant", item.id);
315
- } else {
316
- // For non-search mode, set focus for keyboard accessibility
317
- item.focus();
318
- }
319
- } else if (this.searchInput) {
320
- // Clear aria-activedescendant when no item is active
321
- this.searchInput.removeAttribute("aria-activedescendant");
322
- }
323
- }
324
-
325
- private findNavigableItem(
326
- items: NodeListOf<Element>,
327
- startIndex: number,
328
- direction: "forward" | "backward",
329
- ): HTMLElement | null {
330
- const step = direction === "forward" ? 1 : -1;
331
- const end = direction === "forward" ? items.length : -1;
332
-
333
- for (let i = startIndex; i !== end; i += step) {
334
- const item = items[i] as HTMLElement;
335
- if (
336
- item.getAttribute("data-disabled") !== "true" &&
337
- item.getAttribute("data-filtered") !== "true"
338
- ) {
339
- return item;
340
- }
341
- }
342
- return null;
343
- }
344
-
345
- private handleNavigationKeys(e: KeyboardEvent) {
346
- if (!this.content) return;
347
- const items = this.content.querySelectorAll('[role="option"]');
348
- const currentIndex = Array.from(items).indexOf(this.activeItem as HTMLElement);
349
-
350
- let targetItem: HTMLElement | null = null;
351
-
352
- switch (e.key) {
353
- case "Home":
354
- targetItem = this.findNavigableItem(items, 0, "forward");
355
- break;
356
-
357
- case "End":
358
- targetItem = this.findNavigableItem(items, items.length - 1, "backward");
359
- break;
360
-
361
- case "ArrowUp":
362
- if (currentIndex > 0) {
363
- targetItem = this.findNavigableItem(items, currentIndex - 1, "backward");
364
- }
365
- break;
366
-
367
- case "ArrowDown":
368
- if (currentIndex < items.length - 1) {
369
- targetItem = this.findNavigableItem(items, currentIndex + 1, "forward");
370
- }
371
- break;
372
- }
373
-
374
- if (targetItem) {
375
- this.setActiveItem(targetItem);
376
- }
377
- }
378
-
379
- private setupSearchInput() {
380
- if (!this.searchInput || !this.content) return;
381
-
382
- this.searchInput.addEventListener("input", (e) => {
383
- const target = e.target as HTMLInputElement;
384
- const searchValue = target.value.toLowerCase().trim();
385
- this.filterItems(searchValue);
386
- });
387
-
388
- // Handle keyboard navigation from search input
389
- this.searchInput.addEventListener("keydown", (e) => {
390
- if (e.key === "Escape") {
391
- e.stopPropagation();
392
- this.returnFocusOnClose = true;
393
- this.closeSelect();
394
- }
395
- // Allow arrow keys to navigate to items
396
- else if (["ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) {
397
- e.preventDefault();
398
- e.stopPropagation();
399
- this.handleNavigationKeys(e);
400
- }
401
- // Handle Enter to select the active or first visible item
402
- else if (e.key === "Enter") {
403
- e.preventDefault();
404
- const itemToSelect = this.activeItem || this.getFirstVisibleItem();
405
- if (itemToSelect) {
406
- this.returnFocusOnClose = true;
407
- this.handleSelection(itemToSelect);
408
- }
409
- }
410
- // Prevent space from scrolling the page, but allow it to be typed
411
- else if (e.key === " ") {
412
- e.stopPropagation();
413
- }
414
- });
415
- }
416
-
417
- private getFirstVisibleItem(): HTMLElement | null {
418
- if (!this.content) return null;
419
-
420
- const items = this.content.querySelectorAll('[role="option"]');
421
- for (const item of items) {
422
- if (
423
- item.getAttribute("data-disabled") !== "true" &&
424
- item.getAttribute("data-filtered") !== "true"
425
- ) {
426
- return item as HTMLElement;
427
- }
428
- }
429
- return null;
430
- }
431
-
432
- private filterItems(searchValue: string) {
433
- if (!this.content) return;
434
-
435
- const items = this.content.querySelectorAll('[role="option"]');
436
- let visibleCount = 0;
437
- let firstVisibleItem: HTMLElement | null = null;
438
-
439
- items.forEach((item) => {
440
- const itemText = item.textContent?.toLowerCase().trim() || "";
441
- const matches = itemText.includes(searchValue);
442
-
443
- if (matches || searchValue === "") {
444
- item.classList.remove("starwind-sr-only");
445
- item.removeAttribute("data-filtered");
446
- visibleCount++;
447
- if (!firstVisibleItem) {
448
- firstVisibleItem = item as HTMLElement;
449
- }
450
- } else {
451
- item.classList.add("starwind-sr-only");
452
- item.setAttribute("data-filtered", "true");
453
- }
454
- });
455
-
456
- // Update active item to first visible item after filtering
457
- if (this.searchInput && firstVisibleItem) {
458
- this.setActiveItem(firstVisibleItem);
459
- } else if (this.searchInput && visibleCount === 0) {
460
- this.setActiveItem(null);
461
- }
462
-
463
- // Show/hide empty message
464
- if (this.emptyElement) {
465
- if (visibleCount === 0 && searchValue !== "") {
466
- this.emptyElement.classList.remove("hidden");
467
- } else {
468
- this.emptyElement.classList.add("hidden");
469
- }
470
- }
471
- }
472
-
473
- private handleTypeahead(key: string) {
474
- if (!this.content) return;
475
- const search = this.typeaheadSearch + key;
476
- const items = this.content.querySelectorAll('[role="option"]');
477
-
478
- // find and set active the first matching option
479
- const matches = Array.from(items).filter((item) =>
480
- item.textContent?.toLowerCase().trim().startsWith(search.toLowerCase()),
481
- ) as HTMLElement[];
482
- if (matches.length > 0) {
483
- this.setActiveItem(matches[0]);
484
- }
485
-
486
- // update the typeahead search and reset the timer
487
- this.typeaheadSearch = search;
488
- if (this.typeaheadTimerRef) {
489
- window.clearTimeout(this.typeaheadTimerRef);
490
- }
491
-
492
- // set a timer to clear the search after 1 second
493
- this.typeaheadTimerRef = window.setTimeout(() => {
494
- this.typeaheadSearch = "";
495
- this.typeaheadTimerRef = null;
496
- }, 1000);
497
- }
498
-
499
- private setSize() {
500
- if (!this.trigger || !this.content) return;
501
- this.content.style.setProperty(
502
- "--starwind-select-content-width",
503
- `${this.content.offsetWidth}px`,
504
- );
505
-
506
- this.content.style.setProperty(
507
- "--starwind-select-trigger-width",
508
- `${this.trigger.offsetWidth}px`,
509
- );
510
- }
511
-
512
- private toggleSelect() {
513
- if (this.isOpen) {
514
- this.closeSelect();
515
- } else {
516
- this.openSelect();
517
- }
518
- }
519
-
520
- private openSelect() {
521
- if (!this.content || !this.trigger || this.trigger.disabled) return;
522
-
523
- this.isOpen = true;
524
- this.content.setAttribute("data-state", "open");
525
- this.trigger.setAttribute("aria-expanded", "true");
526
- this.content.style.removeProperty("display");
527
-
528
- // If search input exists, focus it and clear any previous search
529
- if (this.searchInput) {
530
- this.searchInput.value = "";
531
- this.filterItems("");
532
-
533
- // Set the selected item or first item as active
534
- const initialItem = this.selectedItem || this.getFirstVisibleItem();
535
- if (initialItem) {
536
- this.setActiveItem(initialItem);
537
- }
538
-
539
- requestAnimationFrame(() => {
540
- this.searchInput?.focus();
541
- });
542
- } else {
543
- // set active on the current selected item
544
- if (this.selectedItem) {
545
- this.setActiveItem(this.selectedItem);
546
- } else {
547
- // if no item is selected, set active on the first item
548
- const firstItem = this.content.querySelector('[role="option"]') as HTMLElement;
549
- if (firstItem) {
550
- this.setActiveItem(firstItem);
551
- }
552
- }
553
- }
554
- }
555
-
556
- private closeSelect() {
557
- if (!this.content || !this.trigger) return;
558
-
559
- this.isOpen = false;
560
- this.content.setAttribute("data-state", "closed");
561
-
562
- // Remove focus from any currently focused element
563
- const activeElement = document.activeElement;
564
- if (activeElement instanceof HTMLElement) {
565
- activeElement.blur();
566
- }
567
-
568
- // Set focus on trigger if returnFocusOnClose is true
569
- if (this.returnFocusOnClose) {
570
- requestAnimationFrame(() => {
571
- if (!this.trigger) return;
572
- this.trigger.focus();
573
- });
574
- }
575
-
576
- // give the content time to animate before hiding
577
- setTimeout(() => {
578
- if (!this.content) return;
579
- this.content.style.display = "none";
580
-
581
- // Clear search and show all items after animation completes
582
- if (this.searchInput) {
583
- this.searchInput.value = "";
584
- this.filterItems("");
585
- this.setActiveItem(null);
586
- }
587
- }, this.animationDuration);
588
-
589
- this.trigger.setAttribute("aria-expanded", "false");
590
- }
591
-
592
- private handleSelection(item: HTMLElement) {
593
- if (!this.trigger) return;
594
-
595
- // update the hidden select field
596
- const selectField = this.select.querySelector("select");
597
- if (selectField) {
598
- const newValue = item.getAttribute("data-value") || "";
599
- selectField.value = newValue;
600
-
601
- // Dispatch custom event with the new value
602
- const event = new CustomEvent<SelectChangeEvent["detail"]>("starwind-select:change", {
603
- detail: { value: newValue, selectId: this.select.id, label: item.textContent || "" },
604
- bubbles: true,
605
- cancelable: true,
606
- });
607
-
608
- selectField.dispatchEvent(event);
609
- }
610
-
611
- // Update trigger content
612
- const triggerSpan = this.trigger.firstElementChild as HTMLSpanElement;
613
- if (triggerSpan) {
614
- triggerSpan.textContent = item.textContent;
615
- }
616
-
617
- // Update selected states after select finishes closing
618
- setTimeout(() => {
619
- if (this.selectedItem) {
620
- this.selectedItem.setAttribute("aria-selected", "false");
621
- }
622
- item.setAttribute("aria-selected", "true");
623
- this.selectedItem = item;
624
- }, this.animationDuration);
625
-
626
- // Close the select
627
- this.closeSelect();
628
- }
629
-
630
- /**
631
- * Sets the initial state based on the default value attribute
632
- */
633
- private setInitialState(): void {
634
- const defaultValue = this.select.dataset.value;
635
- if (defaultValue) {
636
- const item = this.content?.querySelector(`[data-value="${defaultValue}"]`);
637
-
638
- if (item && item instanceof HTMLElement) {
639
- this.returnFocusOnClose = false;
640
- this.handleSelection(item);
641
- this.selectedItem = item;
642
- }
643
- }
644
- }
645
-
646
- /**
647
- * Programmatically selects an option by value
648
- */
649
- private programmaticallySelect(value: string): void {
650
- if (!this.content) return;
651
-
652
- const item = this.content.querySelector(`[data-value="${value}"]`);
653
- if (item instanceof HTMLElement) {
654
- this.returnFocusOnClose = false;
655
-
656
- // Update aria-selected attributes immediately
657
- if (this.selectedItem) {
658
- this.selectedItem.setAttribute("aria-selected", "false");
659
- }
660
- item.setAttribute("aria-selected", "true");
661
-
662
- // Then call handleSelection which will update the rest
663
- this.handleSelection(item);
664
- }
665
- }
666
- }
667
-
668
- // Store instances in a WeakMap to avoid memory leaks
669
- const selectInstances = new WeakMap<HTMLElement, SelectHandler>();
670
-
671
- // Initialize selects
672
- const initSelects = () => {
673
- document.querySelectorAll(".starwind-select").forEach((select, idx) => {
674
- if (select instanceof HTMLElement && !selectInstances.has(select)) {
675
- selectInstances.set(select, new SelectHandler(select, idx));
676
- }
677
- });
678
- };
679
-
680
- initSelects();
681
- document.addEventListener("astro:after-swap", initSelects);
682
- </script>
683
-
684
- <style is:global>
685
- .starwind-sr-only {
686
- position: absolute;
687
- width: 1px;
688
- height: 1px;
689
- padding: 0;
690
- margin: -1px;
691
- overflow: hidden;
692
- clip: rect(0, 0, 0, 0);
693
- white-space: nowrap;
694
- border-width: 0;
695
- }
696
- </style>