@starwind-ui/core 1.6.2 → 1.7.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.
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/src/components/accordion/Accordion.astro +224 -224
- package/dist/src/components/accordion/AccordionContent.astro +13 -13
- package/dist/src/components/accordion/AccordionItem.astro +6 -6
- package/dist/src/components/accordion/AccordionTrigger.astro +13 -13
- package/dist/src/components/accordion/index.ts +4 -4
- package/dist/src/components/alert/Alert.astro +15 -15
- package/dist/src/components/alert/AlertDescription.astro +1 -1
- package/dist/src/components/alert/AlertTitle.astro +2 -2
- package/dist/src/components/alert/index.ts +3 -3
- package/dist/src/components/avatar/Avatar.astro +16 -16
- package/dist/src/components/avatar/AvatarFallback.astro +3 -3
- package/dist/src/components/avatar/AvatarImage.astro +12 -12
- package/dist/src/components/avatar/index.ts +4 -4
- package/dist/src/components/badge/Badge.astro +33 -33
- package/dist/src/components/breadcrumb/Breadcrumb.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +6 -6
- package/dist/src/components/breadcrumb/BreadcrumbItem.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbLink.astro +8 -8
- package/dist/src/components/breadcrumb/BreadcrumbList.astro +2 -2
- package/dist/src/components/breadcrumb/BreadcrumbPage.astro +6 -6
- package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +7 -7
- package/dist/src/components/breadcrumb/index.ts +14 -14
- package/dist/src/components/button/Button.astro +38 -38
- package/dist/src/components/card/Card.astro +1 -1
- package/dist/src/components/card/CardContent.astro +1 -1
- package/dist/src/components/card/CardDescription.astro +1 -1
- package/dist/src/components/card/CardFooter.astro +1 -1
- package/dist/src/components/card/CardHeader.astro +1 -1
- package/dist/src/components/card/CardTitle.astro +1 -1
- package/dist/src/components/card/index.ts +7 -7
- package/dist/src/components/checkbox/Checkbox.astro +89 -89
- package/dist/src/components/dialog/Dialog.astro +237 -178
- package/dist/src/components/dialog/DialogClose.astro +14 -14
- package/dist/src/components/dialog/DialogContent.astro +32 -32
- package/dist/src/components/dialog/DialogDescription.astro +1 -1
- package/dist/src/components/dialog/DialogFooter.astro +1 -1
- package/dist/src/components/dialog/DialogHeader.astro +1 -1
- package/dist/src/components/dialog/DialogTitle.astro +6 -6
- package/dist/src/components/dialog/DialogTrigger.astro +26 -20
- package/dist/src/components/dialog/index.ts +16 -16
- package/dist/src/components/dropdown/Dropdown.astro +359 -359
- package/dist/src/components/dropdown/DropdownContent.astro +63 -63
- package/dist/src/components/dropdown/DropdownItem.astro +31 -31
- package/dist/src/components/dropdown/DropdownLabel.astro +14 -14
- package/dist/src/components/dropdown/DropdownSeparator.astro +5 -5
- package/dist/src/components/dropdown/DropdownTrigger.astro +26 -26
- package/dist/src/components/dropdown/index.ts +12 -12
- package/dist/src/components/dropzone/Dropzone.astro +232 -0
- package/dist/src/components/dropzone/DropzoneFilesList.astro +25 -0
- package/dist/src/components/dropzone/DropzoneLoadingIndicator.astro +10 -0
- package/dist/src/components/dropzone/DropzoneUploadIndicator.astro +10 -0
- package/dist/src/components/dropzone/index.ts +13 -0
- package/dist/src/components/input/Input.astro +12 -12
- package/dist/src/components/label/Label.astro +8 -8
- package/dist/src/components/pagination/Pagination.astro +1 -1
- package/dist/src/components/pagination/PaginationContent.astro +3 -3
- package/dist/src/components/pagination/PaginationEllipsis.astro +2 -2
- package/dist/src/components/pagination/PaginationItem.astro +3 -3
- package/dist/src/components/pagination/PaginationLink.astro +27 -27
- package/dist/src/components/pagination/PaginationNext.astro +7 -6
- package/dist/src/components/pagination/PaginationPrevious.astro +7 -6
- package/dist/src/components/pagination/index.ts +14 -14
- package/dist/src/components/progress/Progress.astro +151 -0
- package/dist/src/components/progress/index.ts +5 -0
- package/dist/src/components/radio-group/RadioGroup.astro +156 -0
- package/dist/src/components/radio-group/RadioGroupItem.astro +125 -0
- package/dist/src/components/radio-group/RadioGroupTypes.ts +6 -0
- package/dist/src/components/radio-group/index.ts +10 -0
- package/dist/src/components/select/Select.astro +515 -475
- package/dist/src/components/select/SelectContent.astro +62 -62
- package/dist/src/components/select/SelectItem.astro +27 -27
- package/dist/src/components/select/SelectLabel.astro +1 -1
- package/dist/src/components/select/SelectTrigger.astro +28 -28
- package/dist/src/components/select/SelectTypes.ts +11 -5
- package/dist/src/components/select/SelectValue.astro +5 -5
- package/dist/src/components/select/index.ts +16 -16
- package/dist/src/components/skeleton/Skeleton.astro +14 -0
- package/dist/src/components/skeleton/index.ts +5 -0
- package/dist/src/components/switch/Switch.astro +150 -150
- package/dist/src/components/switch/SwitchTypes.ts +4 -4
- package/dist/src/components/table/Table.astro +5 -5
- package/dist/src/components/table/TableBody.astro +3 -3
- package/dist/src/components/table/TableCaption.astro +3 -3
- package/dist/src/components/table/TableCell.astro +3 -3
- package/dist/src/components/table/TableFoot.astro +3 -3
- package/dist/src/components/table/TableHead.astro +3 -3
- package/dist/src/components/table/TableHeader.astro +3 -3
- package/dist/src/components/table/TableRow.astro +3 -3
- package/dist/src/components/table/index.ts +8 -8
- package/dist/src/components/tabs/Tabs.astro +250 -250
- package/dist/src/components/tabs/TabsContent.astro +10 -10
- package/dist/src/components/tabs/TabsList.astro +2 -2
- package/dist/src/components/tabs/TabsTrigger.astro +15 -15
- package/dist/src/components/tabs/index.ts +4 -4
- package/dist/src/components/textarea/Textarea.astro +16 -16
- package/dist/src/components/tooltip/Tooltip.astro +217 -217
- package/dist/src/components/tooltip/TooltipContent.astro +81 -81
- package/dist/src/components/tooltip/index.ts +3 -3
- package/package.json +6 -6
|
@@ -2,492 +2,532 @@
|
|
|
2
2
|
import type { HTMLAttributes } from "astro/types";
|
|
3
3
|
|
|
4
4
|
type Props = HTMLAttributes<"div"> & {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
15
|
};
|
|
16
16
|
|
|
17
17
|
const { class: className, name, defaultValue, ...rest } = Astro.props;
|
|
18
18
|
---
|
|
19
19
|
|
|
20
20
|
<div
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
class:list={["starwind-select", "relative", className]}
|
|
22
|
+
data-name={name}
|
|
23
|
+
data-value={defaultValue}
|
|
24
|
+
{...rest}
|
|
25
25
|
>
|
|
26
|
-
|
|
26
|
+
<slot />
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
29
|
<script>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
30
|
+
import type { SelectChangeEvent, SelectEvent } from "./SelectTypes";
|
|
31
|
+
|
|
32
|
+
class SelectHandler {
|
|
33
|
+
private select: HTMLElement;
|
|
34
|
+
private trigger: HTMLButtonElement | null;
|
|
35
|
+
private content: HTMLElement | null;
|
|
36
|
+
private isOpen: boolean = false;
|
|
37
|
+
private selectedItem: HTMLElement | null = null;
|
|
38
|
+
private animationDuration = 150;
|
|
39
|
+
private typeaheadTimerRef: number | null = null;
|
|
40
|
+
private typeaheadSearch = "";
|
|
41
|
+
private returnFocusOnClose: boolean = true;
|
|
42
|
+
|
|
43
|
+
constructor(select: HTMLElement, selectIdx: number) {
|
|
44
|
+
this.select = select;
|
|
45
|
+
this.trigger = select.querySelector(".starwind-select-trigger");
|
|
46
|
+
this.content = select.querySelector(".starwind-select-content");
|
|
47
|
+
|
|
48
|
+
if (!this.trigger || !this.content) return;
|
|
49
|
+
|
|
50
|
+
// animationDuration is set with inline styles through passed prop to SelectContent
|
|
51
|
+
const animationDurationString = this.content.style.animationDuration;
|
|
52
|
+
if (animationDurationString.endsWith("ms")) {
|
|
53
|
+
this.animationDuration = parseFloat(animationDurationString);
|
|
54
|
+
} else if (animationDurationString.endsWith("s")) {
|
|
55
|
+
// using something like @playform/compress might optimize to use "s" instead of "ms"
|
|
56
|
+
this.animationDuration = parseFloat(animationDurationString) * 1000;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.init(selectIdx);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private init(selectIdx: number) {
|
|
63
|
+
this.setupAccessibility(selectIdx);
|
|
64
|
+
this.setupEvents();
|
|
65
|
+
this.setupSelectField();
|
|
66
|
+
this.setInitialState();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setupSelectField() {
|
|
70
|
+
if (!this.trigger || !this.content) return;
|
|
71
|
+
// build the standard select field
|
|
72
|
+
const selectField = document.createElement("select");
|
|
73
|
+
selectField.tabIndex = -1;
|
|
74
|
+
selectField.setAttribute("aria-hidden", "true");
|
|
75
|
+
selectField.setAttribute("placeholder", "select");
|
|
76
|
+
const selectName = this.select.getAttribute("data-name");
|
|
77
|
+
if (selectName) {
|
|
78
|
+
selectField.name = selectName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// you can comment out this "sr-only" class line below if you want to see the native select in action
|
|
82
|
+
selectField.classList.add("starwind-sr-only");
|
|
83
|
+
|
|
84
|
+
// The first option is a placeholder
|
|
85
|
+
const placeholderOption = document.createElement("option");
|
|
86
|
+
placeholderOption.value = "";
|
|
87
|
+
placeholderOption.textContent = "Select";
|
|
88
|
+
placeholderOption.disabled = true;
|
|
89
|
+
placeholderOption.selected = true;
|
|
90
|
+
selectField.appendChild(placeholderOption);
|
|
91
|
+
|
|
92
|
+
// add all options to the select field
|
|
93
|
+
this.content.querySelectorAll('[role="option"]').forEach((option) => {
|
|
94
|
+
const optionValue = option.getAttribute("data-value");
|
|
95
|
+
const optionText = option.textContent;
|
|
96
|
+
const optionElement = document.createElement("option");
|
|
97
|
+
optionElement.value = optionValue || "";
|
|
98
|
+
optionElement.textContent = optionText || "";
|
|
99
|
+
selectField.appendChild(optionElement);
|
|
100
|
+
});
|
|
101
|
+
this.trigger.appendChild(selectField);
|
|
102
|
+
|
|
103
|
+
// add this select field right after the trigger
|
|
104
|
+
this.trigger.parentElement?.insertBefore(selectField, this.trigger.nextSibling);
|
|
105
|
+
|
|
106
|
+
this.setSize();
|
|
107
|
+
this.content.style.width = "var(--starwind-select-trigger-width)";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private setupAccessibility(selectIdx: number) {
|
|
111
|
+
if (!this.trigger || !this.content) return;
|
|
112
|
+
|
|
113
|
+
// Generate unique IDs for accessibility
|
|
114
|
+
this.trigger.id = `starwind-select${selectIdx}-trigger`;
|
|
115
|
+
this.content.id = `starwind-select${selectIdx}-content`;
|
|
116
|
+
|
|
117
|
+
// Set up additional ARIA attributes
|
|
118
|
+
this.trigger.setAttribute("aria-controls", this.content.id);
|
|
119
|
+
this.content.setAttribute("aria-labelledby", this.trigger.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private setupEvents() {
|
|
123
|
+
if (!this.trigger || !this.content) return;
|
|
124
|
+
|
|
125
|
+
// Handle pointerdown
|
|
126
|
+
this.trigger.addEventListener("pointerdown", (e) => {
|
|
127
|
+
// prevent implicit pointer capture
|
|
128
|
+
// https://www.w3.org/TR/pointerevents3/#implicit-pointer-capture
|
|
129
|
+
const target = e.target as HTMLElement;
|
|
130
|
+
if (target.hasPointerCapture(e.pointerId)) {
|
|
131
|
+
target.releasePointerCapture(e.pointerId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// prevent trigger from stealing focus from the active item after opening.
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
|
|
137
|
+
// only call handler if it's the left button (mousedown gets triggered by all mouse buttons)
|
|
138
|
+
// but not when the control key is pressed (avoiding MacOS right click); also not for touch
|
|
139
|
+
// devices because that would open the menu on scroll. (pen devices behave as touch on iOS).
|
|
140
|
+
if (e.button === 0 && e.ctrlKey === false && e.pointerType === "mouse") {
|
|
141
|
+
this.returnFocusOnClose = true;
|
|
142
|
+
this.toggleSelect();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Handle click event for mobile devices
|
|
147
|
+
this.trigger.addEventListener("click", (e) => {
|
|
148
|
+
if (window.matchMedia("(pointer: coarse)").matches) {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
this.returnFocusOnClose = true;
|
|
151
|
+
this.toggleSelect();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// add enter or space key to select trigger to toggle select
|
|
156
|
+
this.trigger.addEventListener("keydown", (e) => {
|
|
157
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
this.returnFocusOnClose = true;
|
|
160
|
+
this.toggleSelect();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Handle keyboard navigation inside select content
|
|
165
|
+
this.content.addEventListener("keydown", (e) => {
|
|
166
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
167
|
+
// set element based on current focused element
|
|
168
|
+
const activeElement = document.activeElement;
|
|
169
|
+
this.returnFocusOnClose = true;
|
|
170
|
+
this.handleSelection(activeElement as HTMLElement);
|
|
171
|
+
} else if (e.key === "Escape" && this.isOpen) {
|
|
172
|
+
this.returnFocusOnClose = true;
|
|
173
|
+
this.closeSelect();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// add key navigation for accessibility
|
|
177
|
+
// "Home" goes to first element
|
|
178
|
+
// "End" goes to last element
|
|
179
|
+
// "ArrowUp" goes to previous element
|
|
180
|
+
// "ArrowDown" goes to next element
|
|
181
|
+
else if (["ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) {
|
|
182
|
+
this.handleNavigationKeys(e);
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
} else {
|
|
185
|
+
const isModifierKey = e.ctrlKey || e.altKey || e.metaKey;
|
|
186
|
+
|
|
187
|
+
// select should not be navigated using tab key so we prevent it
|
|
188
|
+
if (e.key === "Tab") e.preventDefault();
|
|
189
|
+
|
|
190
|
+
if (!isModifierKey && e.key.length === 1) {
|
|
191
|
+
this.handleTypeahead(e.key);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Handle hover on select items
|
|
197
|
+
this.content.addEventListener("mouseover", (e) => {
|
|
198
|
+
const target = e.target as HTMLElement;
|
|
199
|
+
const option = target.closest('[role="option"]');
|
|
200
|
+
if (option && option instanceof HTMLElement && this.isOpen === true) {
|
|
201
|
+
option.focus();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// handle pointerdown outside select content to close
|
|
206
|
+
document.addEventListener("pointerdown", (e) => {
|
|
207
|
+
// only close if not a mouse pointer
|
|
208
|
+
if (!window.matchMedia("(pointer: coarse)").matches) {
|
|
209
|
+
if (
|
|
210
|
+
!(
|
|
211
|
+
this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)
|
|
212
|
+
) &&
|
|
213
|
+
this.isOpen
|
|
214
|
+
) {
|
|
215
|
+
this.returnFocusOnClose = false;
|
|
216
|
+
this.closeSelect();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Handle click outside select content to close
|
|
222
|
+
document.addEventListener("click", (e) => {
|
|
223
|
+
if (
|
|
224
|
+
!(this.trigger?.contains(e.target as Node) || this.content?.contains(e.target as Node)) &&
|
|
225
|
+
this.isOpen
|
|
226
|
+
) {
|
|
227
|
+
this.returnFocusOnClose = false;
|
|
228
|
+
this.closeSelect();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Handle selection of items
|
|
233
|
+
this.content?.addEventListener("click", (e) => {
|
|
234
|
+
const item = (e.target as HTMLElement).closest("[role='option']");
|
|
235
|
+
if (item instanceof HTMLElement) {
|
|
236
|
+
this.returnFocusOnClose = true;
|
|
237
|
+
this.handleSelection(item);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// passive resize listener to call setSize()
|
|
242
|
+
window.addEventListener("resize", () => this.setSize(), { passive: true });
|
|
243
|
+
|
|
244
|
+
// Listen for programmatic selection events
|
|
245
|
+
document.addEventListener("starwind-select:select", (e: Event) => {
|
|
246
|
+
const selectEvent = e as SelectEvent;
|
|
247
|
+
const selectId = selectEvent.detail.selectId;
|
|
248
|
+
const selectName = selectEvent.detail.selectName;
|
|
249
|
+
const selectValue = selectEvent.detail.value;
|
|
250
|
+
|
|
251
|
+
// Check if this event is for this select
|
|
252
|
+
if (
|
|
253
|
+
(selectId && this.select.id === selectId) ||
|
|
254
|
+
(selectName && this.select.getAttribute("data-name") === selectName)
|
|
255
|
+
) {
|
|
256
|
+
this.programmaticallySelect(selectValue);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private handleNavigationKeys(e: KeyboardEvent) {
|
|
262
|
+
if (!this.content) return;
|
|
263
|
+
const items = this.content.querySelectorAll('[role="option"]');
|
|
264
|
+
|
|
265
|
+
// current, or first item, is focused upon opening the select
|
|
266
|
+
const activeElement = document.activeElement;
|
|
267
|
+
const currentIndex = Array.from(items).indexOf(activeElement as HTMLElement);
|
|
268
|
+
if (e.key === "Home") {
|
|
269
|
+
const firstEnabledItem = Array.from(items).find(
|
|
270
|
+
(item) => item.getAttribute("data-disabled") !== "true",
|
|
271
|
+
) as HTMLElement;
|
|
272
|
+
if (firstEnabledItem) {
|
|
273
|
+
firstEnabledItem.focus();
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (e.key === "End") {
|
|
278
|
+
const lastEnabledItem = Array.from(items)
|
|
279
|
+
.reverse()
|
|
280
|
+
.find((item) => item.getAttribute("data-disabled") !== "true") as HTMLElement;
|
|
281
|
+
if (lastEnabledItem) {
|
|
282
|
+
lastEnabledItem.focus();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (e.key === "ArrowUp" && currentIndex > 0) {
|
|
287
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
288
|
+
const item = items[i] as HTMLElement;
|
|
289
|
+
if (item.getAttribute("data-disabled") !== "true") {
|
|
290
|
+
item.focus();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (e.key === "ArrowDown" && currentIndex < items.length - 1) {
|
|
297
|
+
for (let i = currentIndex + 1; i < items.length; i++) {
|
|
298
|
+
const item = items[i] as HTMLElement;
|
|
299
|
+
if (item.getAttribute("data-disabled") !== "true") {
|
|
300
|
+
item.focus();
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private handleTypeahead(key: string) {
|
|
309
|
+
if (!this.content) return;
|
|
310
|
+
const search = this.typeaheadSearch + key;
|
|
311
|
+
const items = this.content.querySelectorAll('[role="option"]');
|
|
312
|
+
|
|
313
|
+
// find and focus the first matching option
|
|
314
|
+
const matches = Array.from(items).filter((item) =>
|
|
315
|
+
item.textContent?.toLowerCase().trim().startsWith(search.toLowerCase()),
|
|
316
|
+
) as HTMLElement[];
|
|
317
|
+
if (matches.length > 0) {
|
|
318
|
+
matches[0].focus();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// update the typeahead search and reset the timer
|
|
322
|
+
this.typeaheadSearch = search;
|
|
323
|
+
if (this.typeaheadTimerRef) {
|
|
324
|
+
window.clearTimeout(this.typeaheadTimerRef);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// set a timer to clear the search after 1 second
|
|
328
|
+
this.typeaheadTimerRef = window.setTimeout(() => {
|
|
329
|
+
this.typeaheadSearch = "";
|
|
330
|
+
this.typeaheadTimerRef = null;
|
|
331
|
+
}, 1000);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private setSize() {
|
|
335
|
+
if (!this.trigger || !this.content) return;
|
|
336
|
+
this.content.style.setProperty(
|
|
337
|
+
"--starwind-select-content-width",
|
|
338
|
+
`${this.content.offsetWidth}px`,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
this.content.style.setProperty(
|
|
342
|
+
"--starwind-select-trigger-width",
|
|
343
|
+
`${this.trigger.offsetWidth}px`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private toggleSelect() {
|
|
348
|
+
if (this.isOpen) {
|
|
349
|
+
this.closeSelect();
|
|
350
|
+
} else {
|
|
351
|
+
this.openSelect();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private openSelect() {
|
|
356
|
+
if (!this.content || !this.trigger || this.trigger.disabled) return;
|
|
357
|
+
|
|
358
|
+
this.isOpen = true;
|
|
359
|
+
this.content.setAttribute("data-state", "open");
|
|
360
|
+
this.trigger.setAttribute("aria-expanded", "true");
|
|
361
|
+
this.content.style.removeProperty("display");
|
|
362
|
+
|
|
363
|
+
// set focus on the current selected item
|
|
364
|
+
if (this.selectedItem) {
|
|
365
|
+
this.selectedItem.focus();
|
|
366
|
+
} else {
|
|
367
|
+
// if no item is selected, focus on the first item
|
|
368
|
+
const firstItem = this.content.querySelector('[role="option"]') as HTMLElement;
|
|
369
|
+
if (firstItem) {
|
|
370
|
+
firstItem.focus();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this.positionContent();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private closeSelect() {
|
|
378
|
+
if (!this.content || !this.trigger) return;
|
|
379
|
+
|
|
380
|
+
this.isOpen = false;
|
|
381
|
+
this.content.setAttribute("data-state", "closed");
|
|
382
|
+
|
|
383
|
+
// Remove focus from any currently focused element
|
|
384
|
+
const activeElement = document.activeElement;
|
|
385
|
+
if (activeElement instanceof HTMLElement) {
|
|
386
|
+
activeElement.blur();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Set focus on trigger if returnFocusOnClose is true
|
|
390
|
+
if (this.returnFocusOnClose) {
|
|
391
|
+
requestAnimationFrame(() => {
|
|
392
|
+
if (!this.trigger) return;
|
|
393
|
+
this.trigger.focus();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// give the content time to animate before hiding
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
if (!this.content) return;
|
|
400
|
+
this.content.style.display = "none";
|
|
401
|
+
}, this.animationDuration);
|
|
402
|
+
|
|
403
|
+
this.trigger.setAttribute("aria-expanded", "false");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private handleSelection(item: HTMLElement) {
|
|
407
|
+
if (!this.trigger) return;
|
|
408
|
+
|
|
409
|
+
// update the hidden select field
|
|
410
|
+
const selectField = this.select.querySelector("select");
|
|
411
|
+
if (selectField) {
|
|
412
|
+
const newValue = item.getAttribute("data-value") || "";
|
|
413
|
+
selectField.value = newValue;
|
|
414
|
+
|
|
415
|
+
// Dispatch custom event with the new value
|
|
416
|
+
const event = new CustomEvent<SelectChangeEvent["detail"]>("starwind-select:change", {
|
|
417
|
+
detail: { value: newValue, selectId: this.select.id, label: item.textContent || "" },
|
|
418
|
+
bubbles: true,
|
|
419
|
+
cancelable: true,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
selectField.dispatchEvent(event);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Update trigger content
|
|
426
|
+
const triggerSpan = this.trigger.firstElementChild as HTMLSpanElement;
|
|
427
|
+
if (triggerSpan) {
|
|
428
|
+
triggerSpan.textContent = item.textContent;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Update selected states after select finishes closing
|
|
432
|
+
setTimeout(() => {
|
|
433
|
+
if (this.selectedItem) {
|
|
434
|
+
this.selectedItem.setAttribute("aria-selected", "false");
|
|
435
|
+
}
|
|
436
|
+
item.setAttribute("aria-selected", "true");
|
|
437
|
+
this.selectedItem = item;
|
|
438
|
+
}, this.animationDuration);
|
|
439
|
+
|
|
440
|
+
// Close the select
|
|
441
|
+
this.closeSelect();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Sets the initial state based on the default value attribute
|
|
446
|
+
*/
|
|
447
|
+
private setInitialState(): void {
|
|
448
|
+
const defaultValue = this.select.dataset.value;
|
|
449
|
+
if (defaultValue) {
|
|
450
|
+
const item = this.content?.querySelector(`[data-value="${defaultValue}"]`);
|
|
451
|
+
|
|
452
|
+
if (item && item instanceof HTMLElement) {
|
|
453
|
+
this.returnFocusOnClose = false;
|
|
454
|
+
this.handleSelection(item);
|
|
455
|
+
this.selectedItem = item;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Programmatically selects an option by value
|
|
462
|
+
*/
|
|
463
|
+
private programmaticallySelect(value: string): void {
|
|
464
|
+
if (!this.content) return;
|
|
465
|
+
|
|
466
|
+
const item = this.content.querySelector(`[data-value="${value}"]`);
|
|
467
|
+
if (item instanceof HTMLElement) {
|
|
468
|
+
this.returnFocusOnClose = false;
|
|
469
|
+
|
|
470
|
+
// Update aria-selected attributes immediately
|
|
471
|
+
if (this.selectedItem) {
|
|
472
|
+
this.selectedItem.setAttribute("aria-selected", "false");
|
|
473
|
+
}
|
|
474
|
+
item.setAttribute("aria-selected", "true");
|
|
475
|
+
|
|
476
|
+
// Then call handleSelection which will update the rest
|
|
477
|
+
this.handleSelection(item);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* TODO: add position logic to avoid collisions with window boundary
|
|
483
|
+
* It will need to switch to top or bottom depending on space available
|
|
484
|
+
* It will also need to set the content max height so it doesn't overflow the viewport
|
|
485
|
+
*/
|
|
486
|
+
private positionContent() {
|
|
487
|
+
// if (!this.content || !this.trigger) return;
|
|
488
|
+
// const triggerRect = this.trigger.getBoundingClientRect();
|
|
489
|
+
// const contentRect = this.content.getBoundingClientRect();
|
|
490
|
+
// const viewportHeight = window.innerHeight;
|
|
491
|
+
// // Position the content below the trigger by default
|
|
492
|
+
// let top = triggerRect.bottom;
|
|
493
|
+
// // If there's not enough space below, position it above
|
|
494
|
+
// if (top + contentRect.height > viewportHeight) {
|
|
495
|
+
// top = triggerRect.top - contentRect.height;
|
|
496
|
+
// }
|
|
497
|
+
// this.content.style.position = "absolute";
|
|
498
|
+
// this.content.style.top = `${top}px`;
|
|
499
|
+
// this.content.style.left = `${triggerRect.left}px`;
|
|
500
|
+
// this.content.style.width = `${triggerRect.width}px`;
|
|
501
|
+
// this.content.style.zIndex = "50";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Store instances in a WeakMap to avoid memory leaks
|
|
506
|
+
const selectInstances = new WeakMap<HTMLElement, SelectHandler>();
|
|
507
|
+
|
|
508
|
+
// Initialize selects
|
|
509
|
+
const initSelects = () => {
|
|
510
|
+
document.querySelectorAll(".starwind-select").forEach((select, idx) => {
|
|
511
|
+
if (select instanceof HTMLElement && !selectInstances.has(select)) {
|
|
512
|
+
selectInstances.set(select, new SelectHandler(select, idx));
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
initSelects();
|
|
518
|
+
document.addEventListener("astro:after-swap", initSelects);
|
|
479
519
|
</script>
|
|
480
520
|
|
|
481
521
|
<style is:global>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
522
|
+
.starwind-sr-only {
|
|
523
|
+
position: absolute;
|
|
524
|
+
width: 1px;
|
|
525
|
+
height: 1px;
|
|
526
|
+
padding: 0;
|
|
527
|
+
margin: -1px;
|
|
528
|
+
overflow: hidden;
|
|
529
|
+
clip: rect(0, 0, 0, 0);
|
|
530
|
+
white-space: nowrap;
|
|
531
|
+
border-width: 0;
|
|
532
|
+
}
|
|
493
533
|
</style>
|