@starwind-ui/core 1.15.4 → 1.16.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 (183) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +67 -43
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/components/accordion/Accordion.astro +1 -1
  5. package/dist/src/components/accordion/AccordionContent.astro +1 -1
  6. package/dist/src/components/accordion/AccordionItem.astro +1 -1
  7. package/dist/src/components/accordion/AccordionTrigger.astro +1 -1
  8. package/dist/src/components/alert/Alert.astro +1 -1
  9. package/dist/src/components/alert/AlertDescription.astro +1 -1
  10. package/dist/src/components/alert/AlertTitle.astro +1 -1
  11. package/dist/src/components/alert-dialog/AlertDialog.astro +1 -1
  12. package/dist/src/components/alert-dialog/AlertDialogAction.astro +1 -1
  13. package/dist/src/components/alert-dialog/AlertDialogCancel.astro +1 -1
  14. package/dist/src/components/alert-dialog/AlertDialogContent.astro +3 -3
  15. package/dist/src/components/alert-dialog/AlertDialogDescription.astro +1 -1
  16. package/dist/src/components/alert-dialog/AlertDialogFooter.astro +1 -1
  17. package/dist/src/components/alert-dialog/AlertDialogHeader.astro +1 -1
  18. package/dist/src/components/alert-dialog/AlertDialogTitle.astro +1 -1
  19. package/dist/src/components/alert-dialog/AlertDialogTrigger.astro +2 -2
  20. package/dist/src/components/aspect-ratio/AspectRatio.astro +1 -1
  21. package/dist/src/components/avatar/Avatar.astro +1 -1
  22. package/dist/src/components/avatar/AvatarFallback.astro +1 -1
  23. package/dist/src/components/avatar/AvatarImage.astro +2 -2
  24. package/dist/src/components/badge/Badge.astro +1 -1
  25. package/dist/src/components/breadcrumb/Breadcrumb.astro +1 -1
  26. package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +1 -1
  27. package/dist/src/components/breadcrumb/BreadcrumbItem.astro +1 -1
  28. package/dist/src/components/breadcrumb/BreadcrumbLink.astro +1 -1
  29. package/dist/src/components/breadcrumb/BreadcrumbList.astro +2 -2
  30. package/dist/src/components/breadcrumb/BreadcrumbPage.astro +1 -1
  31. package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +1 -1
  32. package/dist/src/components/button/Button.astro +1 -1
  33. package/dist/src/components/button-group/ButtonGroup.astro +1 -1
  34. package/dist/src/components/button-group/ButtonGroupSeparator.astro +1 -1
  35. package/dist/src/components/button-group/ButtonGroupText.astro +1 -1
  36. package/dist/src/components/card/Card.astro +1 -1
  37. package/dist/src/components/card/CardAction.astro +1 -1
  38. package/dist/src/components/card/CardContent.astro +1 -1
  39. package/dist/src/components/card/CardDescription.astro +1 -1
  40. package/dist/src/components/card/CardFooter.astro +1 -1
  41. package/dist/src/components/card/CardHeader.astro +1 -1
  42. package/dist/src/components/card/CardTitle.astro +1 -1
  43. package/dist/src/components/carousel/Carousel.astro +1 -1
  44. package/dist/src/components/carousel/CarouselContent.astro +1 -1
  45. package/dist/src/components/carousel/CarouselItem.astro +1 -1
  46. package/dist/src/components/carousel/CarouselNext.astro +1 -1
  47. package/dist/src/components/carousel/CarouselPrevious.astro +1 -1
  48. package/dist/src/components/checkbox/Checkbox.astro +1 -1
  49. package/dist/src/components/collapsible/Collapsible.astro +1 -1
  50. package/dist/src/components/collapsible/CollapsibleContent.astro +1 -1
  51. package/dist/src/components/collapsible/CollapsibleTrigger.astro +1 -1
  52. package/dist/src/components/dialog/Dialog.astro +1 -1
  53. package/dist/src/components/dialog/DialogClose.astro +1 -1
  54. package/dist/src/components/dialog/DialogContent.astro +2 -2
  55. package/dist/src/components/dialog/DialogDescription.astro +1 -1
  56. package/dist/src/components/dialog/DialogFooter.astro +1 -1
  57. package/dist/src/components/dialog/DialogHeader.astro +1 -1
  58. package/dist/src/components/dialog/DialogTitle.astro +1 -1
  59. package/dist/src/components/dialog/DialogTrigger.astro +1 -1
  60. package/dist/src/components/dropdown/Dropdown.astro +261 -34
  61. package/dist/src/components/dropdown/DropdownContent.astro +41 -5
  62. package/dist/src/components/dropdown/DropdownItem.astro +2 -2
  63. package/dist/src/components/dropdown/DropdownLabel.astro +2 -2
  64. package/dist/src/components/dropdown/DropdownSeparator.astro +1 -1
  65. package/dist/src/components/dropdown/DropdownShortcut.astro +17 -0
  66. package/dist/src/components/dropdown/DropdownSub.astro +15 -0
  67. package/dist/src/components/dropdown/DropdownSubContent.astro +36 -0
  68. package/dist/src/components/dropdown/DropdownSubTrigger.astro +34 -0
  69. package/dist/src/components/dropdown/DropdownTrigger.astro +1 -1
  70. package/dist/src/components/dropdown/index.ts +12 -0
  71. package/dist/src/components/dropzone/DropzoneFilesList.astro +1 -1
  72. package/dist/src/components/image/Image.astro +1 -1
  73. package/dist/src/components/input/Input.astro +1 -1
  74. package/dist/src/components/input-group/InputGroup.astro +28 -0
  75. package/dist/src/components/input-group/InputGroupAddon.astro +79 -0
  76. package/dist/src/components/input-group/InputGroupButton.astro +34 -0
  77. package/dist/src/components/input-group/InputGroupInput.astro +16 -0
  78. package/dist/src/components/input-group/InputGroupText.astro +17 -0
  79. package/dist/src/components/input-group/InputGroupTextarea.astro +20 -0
  80. package/dist/src/components/input-group/index.ts +33 -0
  81. package/dist/src/components/input-otp/InputOtp.astro +1 -1
  82. package/dist/src/components/input-otp/InputOtpGroup.astro +1 -1
  83. package/dist/src/components/input-otp/InputOtpSeparator.astro +1 -1
  84. package/dist/src/components/input-otp/InputOtpSlot.astro +1 -1
  85. package/dist/src/components/item/Item.astro +1 -1
  86. package/dist/src/components/item/ItemActions.astro +1 -1
  87. package/dist/src/components/item/ItemContent.astro +1 -1
  88. package/dist/src/components/item/ItemDescription.astro +1 -1
  89. package/dist/src/components/item/ItemFooter.astro +1 -1
  90. package/dist/src/components/item/ItemGroup.astro +1 -1
  91. package/dist/src/components/item/ItemHeader.astro +1 -1
  92. package/dist/src/components/item/ItemMedia.astro +1 -1
  93. package/dist/src/components/item/ItemSeparator.astro +1 -1
  94. package/dist/src/components/item/ItemTitle.astro +1 -1
  95. package/dist/src/components/kbd/Kbd.astro +1 -1
  96. package/dist/src/components/kbd/KbdGroup.astro +1 -1
  97. package/dist/src/components/label/Label.astro +1 -1
  98. package/dist/src/components/native-select/NativeSelect.astro +64 -0
  99. package/dist/src/components/native-select/NativeSelectOptGroup.astro +15 -0
  100. package/dist/src/components/native-select/NativeSelectOption.astro +15 -0
  101. package/dist/src/components/native-select/index.ts +21 -0
  102. package/dist/src/components/pagination/Pagination.astro +1 -1
  103. package/dist/src/components/pagination/PaginationContent.astro +1 -1
  104. package/dist/src/components/pagination/PaginationEllipsis.astro +1 -1
  105. package/dist/src/components/pagination/PaginationItem.astro +1 -1
  106. package/dist/src/components/pagination/PaginationLink.astro +1 -1
  107. package/dist/src/components/pagination/PaginationNext.astro +1 -1
  108. package/dist/src/components/pagination/PaginationPrevious.astro +1 -1
  109. package/dist/src/components/popover/Popover.astro +717 -0
  110. package/dist/src/components/popover/PopoverContent.astro +102 -0
  111. package/dist/src/components/popover/PopoverDescription.astro +14 -0
  112. package/dist/src/components/popover/PopoverHeader.astro +14 -0
  113. package/dist/src/components/popover/PopoverTitle.astro +14 -0
  114. package/dist/src/components/popover/PopoverTrigger.astro +51 -0
  115. package/dist/src/components/popover/index.ts +34 -0
  116. package/dist/src/components/progress/Progress.astro +1 -1
  117. package/dist/src/components/prose/Prose.astro +1 -1
  118. package/dist/src/components/radio-group/RadioGroup.astro +1 -1
  119. package/dist/src/components/radio-group/RadioGroupItem.astro +1 -1
  120. package/dist/src/components/select/Select.astro +1 -1
  121. package/dist/src/components/select/SelectContent.astro +1 -1
  122. package/dist/src/components/select/SelectItem.astro +1 -1
  123. package/dist/src/components/select/SelectLabel.astro +1 -1
  124. package/dist/src/components/select/SelectSearch.astro +1 -1
  125. package/dist/src/components/select/SelectSeparator.astro +1 -1
  126. package/dist/src/components/select/SelectTrigger.astro +1 -1
  127. package/dist/src/components/select/SelectValue.astro +1 -1
  128. package/dist/src/components/separator/Separator.astro +1 -1
  129. package/dist/src/components/sheet/Sheet.astro +1 -1
  130. package/dist/src/components/sheet/SheetContent.astro +1 -1
  131. package/dist/src/components/sheet/SheetDescription.astro +1 -1
  132. package/dist/src/components/sheet/SheetFooter.astro +1 -1
  133. package/dist/src/components/sheet/SheetHeader.astro +1 -1
  134. package/dist/src/components/sheet/SheetTitle.astro +1 -1
  135. package/dist/src/components/sidebar/Sidebar.astro +19 -2
  136. package/dist/src/components/sidebar/SidebarContent.astro +1 -1
  137. package/dist/src/components/sidebar/SidebarFooter.astro +1 -1
  138. package/dist/src/components/sidebar/SidebarGroup.astro +1 -1
  139. package/dist/src/components/sidebar/SidebarGroupContent.astro +1 -1
  140. package/dist/src/components/sidebar/SidebarGroupLabel.astro +2 -2
  141. package/dist/src/components/sidebar/SidebarHeader.astro +1 -1
  142. package/dist/src/components/sidebar/SidebarInput.astro +1 -1
  143. package/dist/src/components/sidebar/SidebarInset.astro +1 -1
  144. package/dist/src/components/sidebar/SidebarMenu.astro +1 -1
  145. package/dist/src/components/sidebar/SidebarMenuAction.astro +1 -1
  146. package/dist/src/components/sidebar/SidebarMenuBadge.astro +1 -1
  147. package/dist/src/components/sidebar/SidebarMenuButton.astro +2 -14
  148. package/dist/src/components/sidebar/SidebarMenuItem.astro +1 -1
  149. package/dist/src/components/sidebar/SidebarMenuSkeleton.astro +1 -1
  150. package/dist/src/components/sidebar/SidebarMenuSub.astro +1 -1
  151. package/dist/src/components/sidebar/SidebarMenuSubButton.astro +1 -1
  152. package/dist/src/components/sidebar/SidebarMenuSubItem.astro +1 -1
  153. package/dist/src/components/sidebar/SidebarProvider.astro +1 -1
  154. package/dist/src/components/sidebar/SidebarRail.astro +2 -2
  155. package/dist/src/components/sidebar/SidebarSeparator.astro +1 -1
  156. package/dist/src/components/sidebar/SidebarTrigger.astro +1 -1
  157. package/dist/src/components/skeleton/Skeleton.astro +1 -1
  158. package/dist/src/components/slider/Slider.astro +1 -1
  159. package/dist/src/components/spinner/Spinner.astro +1 -1
  160. package/dist/src/components/switch/Switch.astro +1 -1
  161. package/dist/src/components/table/Table.astro +1 -1
  162. package/dist/src/components/table/TableBody.astro +1 -1
  163. package/dist/src/components/table/TableCaption.astro +1 -1
  164. package/dist/src/components/table/TableCell.astro +1 -1
  165. package/dist/src/components/table/TableFoot.astro +1 -1
  166. package/dist/src/components/table/TableHead.astro +1 -1
  167. package/dist/src/components/table/TableHeader.astro +1 -1
  168. package/dist/src/components/table/TableRow.astro +1 -1
  169. package/dist/src/components/tabs/Tabs.astro +1 -1
  170. package/dist/src/components/tabs/TabsContent.astro +1 -1
  171. package/dist/src/components/tabs/TabsList.astro +1 -1
  172. package/dist/src/components/tabs/TabsTrigger.astro +1 -1
  173. package/dist/src/components/textarea/Textarea.astro +1 -1
  174. package/dist/src/components/theme-toggle/ThemeToggle.astro +1 -1
  175. package/dist/src/components/toast/ToastDescription.astro +1 -1
  176. package/dist/src/components/toast/ToastItem.astro +1 -1
  177. package/dist/src/components/toast/Toaster.astro +1 -1
  178. package/dist/src/components/toggle/Toggle.astro +1 -1
  179. package/dist/src/components/tooltip/Tooltip.astro +260 -122
  180. package/dist/src/components/tooltip/TooltipContent.astro +13 -23
  181. package/dist/src/components/video/Video.astro +2 -2
  182. package/dist/src/lib/utils/starwind/positioning.ts +318 -0
  183. package/package.json +1 -1
@@ -0,0 +1,717 @@
1
+ ---
2
+ import type { HTMLAttributes } from "astro/types";
3
+ import { tv } from "tailwind-variants";
4
+
5
+ type Props = HTMLAttributes<"div"> & {
6
+ /**
7
+ * When true, the popover will open on hover in addition to click
8
+ * @default false
9
+ */
10
+ openOnHover?: boolean;
11
+ /**
12
+ * Time in milliseconds to wait before closing when hover-open is enabled
13
+ * @default 200
14
+ */
15
+ closeDelay?: number;
16
+ /**
17
+ * Whether the popover should be open when first rendered
18
+ * @default false
19
+ */
20
+ defaultOpen?: boolean;
21
+
22
+ children: any;
23
+ };
24
+
25
+ export const popover = tv({ base: "starwind-popover" });
26
+
27
+ const {
28
+ class: className,
29
+ openOnHover = false,
30
+ closeDelay = 200,
31
+ defaultOpen = false,
32
+ ...rest
33
+ } = Astro.props;
34
+ ---
35
+
36
+ <div
37
+ class={popover({ class: className })}
38
+ data-open-on-hover={openOnHover ? "true" : undefined}
39
+ data-close-delay={closeDelay}
40
+ data-state={defaultOpen ? "open" : "closed"}
41
+ data-default-open={defaultOpen ? "true" : undefined}
42
+ {...rest}
43
+ data-slot="popover"
44
+ >
45
+ <slot />
46
+ </div>
47
+
48
+ <script>
49
+ import {
50
+ type FloatingAlign,
51
+ type FloatingSide,
52
+ getTransformOrigin,
53
+ resolvePlacement,
54
+ } from "@/lib/utils/starwind/positioning";
55
+
56
+ let globalPointerClientX: number | null = null;
57
+ let globalPointerClientY: number | null = null;
58
+ let globalPointerTrackingRefCount = 0;
59
+ let isGlobalPointerTrackingAttached = false;
60
+
61
+ const handleGlobalPointerMove = (e: PointerEvent) => {
62
+ if (e.pointerType !== "mouse") return;
63
+ globalPointerClientX = e.clientX;
64
+ globalPointerClientY = e.clientY;
65
+ };
66
+
67
+ const registerGlobalPointerTracking = () => {
68
+ globalPointerTrackingRefCount += 1;
69
+
70
+ if (isGlobalPointerTrackingAttached) {
71
+ return;
72
+ }
73
+
74
+ document.addEventListener("pointermove", handleGlobalPointerMove);
75
+ isGlobalPointerTrackingAttached = true;
76
+ };
77
+
78
+ const unregisterGlobalPointerTracking = () => {
79
+ globalPointerTrackingRefCount = Math.max(0, globalPointerTrackingRefCount - 1);
80
+
81
+ if (globalPointerTrackingRefCount > 0 || !isGlobalPointerTrackingAttached) {
82
+ return;
83
+ }
84
+
85
+ document.removeEventListener("pointermove", handleGlobalPointerMove);
86
+ isGlobalPointerTrackingAttached = false;
87
+ globalPointerClientX = null;
88
+ globalPointerClientY = null;
89
+ };
90
+
91
+ class PopoverHandler {
92
+ private popover: HTMLElement;
93
+ private trigger: HTMLElement | null;
94
+ private content: HTMLElement | null;
95
+ private title: HTMLElement | null;
96
+ private description: HTMLElement | null;
97
+ private isOpen: boolean = false;
98
+ private isClosing: boolean = false;
99
+ private animationDuration = 150;
100
+ private side: FloatingSide = "bottom";
101
+ private align: FloatingAlign = "center";
102
+ private sideOffset = 4;
103
+ private parentHandler: PopoverHandler | null = null;
104
+ private contentPlaceholder: Comment | null = null;
105
+ private cleanupAutoUpdate: (() => void) | null = null;
106
+ private abortController: AbortController;
107
+ private closeTimeoutRef: number | null = null;
108
+ private openOnHover = false;
109
+ private hoverCloseDelay = 200;
110
+ private hoverCloseTimerRef: number | null = null;
111
+ private openChildCount = 0;
112
+ private registeredWithParentAsOpenChild = false;
113
+ private usesGlobalPointerTracking = false;
114
+
115
+ constructor(popover: HTMLElement, popoverIdx: number) {
116
+ this.popover = popover;
117
+ this.abortController = new AbortController();
118
+ this.trigger = this.findOwnElement('[data-slot="popover-trigger"]');
119
+ this.content = this.findOwnElement('[data-slot="popover-content"]');
120
+ this.title = this.findOwnElement('[data-slot="popover-title"]');
121
+ this.description = this.findOwnElement('[data-slot="popover-description"]');
122
+
123
+ if (this.content) {
124
+ (this.content as any).__popoverHandler = this;
125
+ }
126
+
127
+ this.openOnHover = this.popover.getAttribute("data-open-on-hover") === "true";
128
+ const closeDelay = parseInt(this.popover.getAttribute("data-close-delay") || "200");
129
+ if (Number.isFinite(closeDelay) && closeDelay >= 0) {
130
+ this.hoverCloseDelay = closeDelay;
131
+ }
132
+
133
+ this.parentHandler = this.resolveParentHandler();
134
+
135
+ // if trigger is set with asChild, use the first child element as trigger button
136
+ if (this.trigger?.hasAttribute("data-as-child")) {
137
+ this.trigger = this.trigger.firstElementChild as HTMLElement;
138
+ }
139
+
140
+ if (!this.trigger || !this.content) return;
141
+
142
+ if (this.openOnHover) {
143
+ registerGlobalPointerTracking();
144
+ this.usesGlobalPointerTracking = true;
145
+ }
146
+
147
+ const animationDurationString = this.content.style.animationDuration;
148
+ if (animationDurationString.endsWith("ms")) {
149
+ this.animationDuration = parseFloat(animationDurationString);
150
+ } else if (animationDurationString.endsWith("s")) {
151
+ this.animationDuration = parseFloat(animationDurationString) * 1000;
152
+ }
153
+
154
+ const contentSide = this.content.getAttribute("data-side");
155
+ if (
156
+ contentSide === "top" ||
157
+ contentSide === "bottom" ||
158
+ contentSide === "left" ||
159
+ contentSide === "right"
160
+ ) {
161
+ this.side = contentSide;
162
+ }
163
+
164
+ const contentAlign = this.content.getAttribute("data-align");
165
+ if (contentAlign === "start" || contentAlign === "center" || contentAlign === "end") {
166
+ this.align = contentAlign;
167
+ }
168
+
169
+ const contentSideOffset = parseFloat(this.content.getAttribute("data-side-offset") || "4");
170
+ if (Number.isFinite(contentSideOffset)) {
171
+ this.sideOffset = contentSideOffset;
172
+ }
173
+
174
+ this.isOpen =
175
+ this.popover.getAttribute("data-state") === "open" ||
176
+ this.popover.getAttribute("data-default-open") === "true";
177
+
178
+ this.init(popoverIdx);
179
+ }
180
+
181
+ private findOwnElement(selector: string): HTMLElement | null {
182
+ const elements = this.popover.querySelectorAll(selector);
183
+ for (const element of elements) {
184
+ if (element.closest(".starwind-popover") === this.popover) {
185
+ return element as HTMLElement;
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+
191
+ private resolveParentHandler() {
192
+ const parentContent = this.popover.parentElement?.closest('[data-slot="popover-content"]');
193
+ if (!parentContent) return null;
194
+
195
+ return ((parentContent as any).__popoverHandler as PopoverHandler | undefined) ?? null;
196
+ }
197
+
198
+ private init(popoverIdx: number) {
199
+ this.setupAccessibility(popoverIdx);
200
+ this.setupEvents();
201
+ this.setInitialState();
202
+ }
203
+
204
+ private setupAccessibility(popoverIdx: number) {
205
+ if (!this.trigger || !this.content) return;
206
+
207
+ if (!this.trigger.id) {
208
+ this.trigger.id = `starwind-popover${popoverIdx}-trigger`;
209
+ }
210
+
211
+ if (!this.content.id) {
212
+ this.content.id = `starwind-popover${popoverIdx}-content`;
213
+ }
214
+
215
+ this.trigger.setAttribute("aria-controls", this.content.id);
216
+ this.trigger.setAttribute("aria-haspopup", "dialog");
217
+ this.content.setAttribute("aria-labelledby", this.trigger.id);
218
+
219
+ if (this.title) {
220
+ if (!this.title.id) {
221
+ this.title.id = `starwind-popover${popoverIdx}-title`;
222
+ }
223
+ this.content.setAttribute("aria-labelledby", this.title.id);
224
+ }
225
+
226
+ if (this.description) {
227
+ if (!this.description.id) {
228
+ this.description.id = `starwind-popover${popoverIdx}-description`;
229
+ }
230
+ this.content.setAttribute("aria-describedby", this.description.id);
231
+ }
232
+ }
233
+
234
+ private setupEvents() {
235
+ if (!this.trigger || !this.content) return;
236
+
237
+ this.trigger.addEventListener("click", (e) => {
238
+ if (this.isTriggerDisabled()) return;
239
+ e.preventDefault();
240
+ this.togglePopover();
241
+ });
242
+
243
+ this.trigger.addEventListener("keydown", (e) => {
244
+ if (this.isTriggerDisabled()) return;
245
+
246
+ if (e.key === "Enter" || e.key === " ") {
247
+ e.preventDefault();
248
+ e.stopPropagation();
249
+ this.togglePopover();
250
+ } else if (e.key === "ArrowDown" && !this.isOpen) {
251
+ e.preventDefault();
252
+ e.stopPropagation();
253
+ this.openPopover();
254
+ } else if (e.key === "Escape" && this.isOpen) {
255
+ e.preventDefault();
256
+ e.stopPropagation();
257
+ this.closePopover({ focusTrigger: true });
258
+ }
259
+ });
260
+
261
+ this.content.addEventListener("keydown", (e) => {
262
+ if (e.key === "Escape" && this.isOpen) {
263
+ e.preventDefault();
264
+ e.stopPropagation();
265
+ this.closePopover({ focusTrigger: true });
266
+ }
267
+ });
268
+
269
+ if (this.openOnHover) {
270
+ this.trigger.addEventListener(
271
+ "pointerenter",
272
+ (e) => {
273
+ if (e.pointerType !== "mouse") return;
274
+ if (this.isTriggerDisabled()) return;
275
+
276
+ if (!this.isOpen) {
277
+ this.openPopover();
278
+ } else {
279
+ this.clearHoverCloseTimersInTree();
280
+ }
281
+ },
282
+ { signal: this.abortController.signal },
283
+ );
284
+
285
+ this.trigger.addEventListener(
286
+ "pointerleave",
287
+ (e) => {
288
+ if (e.pointerType !== "mouse") return;
289
+ if (this.isOpen) {
290
+ this.closePopoverDelayed();
291
+ }
292
+ },
293
+ { signal: this.abortController.signal },
294
+ );
295
+
296
+ this.content.addEventListener(
297
+ "pointerenter",
298
+ (e) => {
299
+ if (e.pointerType !== "mouse") return;
300
+ this.clearHoverCloseTimersInTree();
301
+ },
302
+ { signal: this.abortController.signal },
303
+ );
304
+
305
+ this.content.addEventListener(
306
+ "pointerleave",
307
+ (e) => {
308
+ if (e.pointerType !== "mouse") return;
309
+ if (this.isOpen) {
310
+ this.closePopoverDelayed();
311
+ }
312
+ },
313
+ { signal: this.abortController.signal },
314
+ );
315
+ }
316
+
317
+ this.popover.addEventListener("starwind-popover:close", ((
318
+ event: CustomEvent<{ focusTrigger?: boolean }>,
319
+ ) => {
320
+ this.closePopover({ focusTrigger: event.detail?.focusTrigger });
321
+ }) as EventListener);
322
+
323
+ document.addEventListener(
324
+ "pointerdown",
325
+ (e) => {
326
+ if (
327
+ this.isOpen &&
328
+ !this.popover.contains(e.target as Node) &&
329
+ !this.content?.contains(e.target as Node) &&
330
+ !this.isTargetInPopoverTree(e.target as Node)
331
+ ) {
332
+ this.closePopover();
333
+ }
334
+ },
335
+ { signal: this.abortController.signal },
336
+ );
337
+
338
+ document.addEventListener(
339
+ "click",
340
+ (e) => {
341
+ if (
342
+ this.isOpen &&
343
+ !this.popover.contains(e.target as Node) &&
344
+ !this.content?.contains(e.target as Node) &&
345
+ !this.isTargetInPopoverTree(e.target as Node)
346
+ ) {
347
+ this.closePopover();
348
+ }
349
+ },
350
+ { signal: this.abortController.signal },
351
+ );
352
+ }
353
+
354
+ private setInitialState() {
355
+ if (!this.trigger || !this.content) return;
356
+
357
+ if (this.isOpen) {
358
+ this.popover.setAttribute("data-state", "open");
359
+ this.trigger.setAttribute("aria-expanded", "true");
360
+ this.trigger.setAttribute("data-state", "open");
361
+ this.content.setAttribute("data-state", "open");
362
+ this.content.style.removeProperty("display");
363
+ this.portalContent();
364
+ this.positionContent();
365
+ requestAnimationFrame(() => this.positionContent());
366
+ } else {
367
+ this.popover.setAttribute("data-state", "closed");
368
+ this.trigger.setAttribute("aria-expanded", "false");
369
+ this.trigger.setAttribute("data-state", "closed");
370
+ this.content.setAttribute("data-state", "closed");
371
+ this.unportalContent();
372
+ this.content.style.display = "none";
373
+ }
374
+ }
375
+
376
+ private isTriggerDisabled() {
377
+ if (!this.trigger) return true;
378
+ return (
379
+ this.trigger.hasAttribute("disabled") ||
380
+ this.trigger.getAttribute("aria-disabled") === "true" ||
381
+ this.trigger.getAttribute("data-disabled") === "true"
382
+ );
383
+ }
384
+
385
+ private togglePopover() {
386
+ if (this.isOpen) {
387
+ this.closePopover();
388
+ } else {
389
+ this.openPopover();
390
+ }
391
+ }
392
+
393
+ private openPopover() {
394
+ if (this.isOpen || this.isClosing) return;
395
+ if (!this.trigger || !this.content || this.isTriggerDisabled()) return;
396
+
397
+ this.clearHoverCloseTimersInTree();
398
+ this.registerAsOpenChildOnParent();
399
+
400
+ this.isOpen = true;
401
+ this.popover.setAttribute("data-state", "open");
402
+ this.trigger.setAttribute("aria-expanded", "true");
403
+ this.trigger.setAttribute("data-state", "open");
404
+ this.content.setAttribute("data-state", "open");
405
+ this.content.style.removeProperty("display");
406
+ this.portalContent();
407
+ this.positionContent();
408
+ requestAnimationFrame(() => this.positionContent());
409
+ }
410
+
411
+ private closePopover(options?: { focusTrigger?: boolean }) {
412
+ if (!this.isOpen) return;
413
+ if (!this.trigger || !this.content) return;
414
+
415
+ this.clearHoverCloseTimer();
416
+
417
+ this.closeNestedPopovers();
418
+
419
+ this.isClosing = true;
420
+ this.isOpen = false;
421
+ this.popover.setAttribute("data-state", "closed");
422
+ this.trigger.setAttribute("aria-expanded", "false");
423
+ this.trigger.setAttribute("data-state", "closed");
424
+ this.content.setAttribute("data-state", "closed");
425
+
426
+ if (options?.focusTrigger) {
427
+ requestAnimationFrame(() => {
428
+ this.trigger?.focus();
429
+ });
430
+ }
431
+
432
+ if (this.closeTimeoutRef !== null) {
433
+ window.clearTimeout(this.closeTimeoutRef);
434
+ }
435
+
436
+ this.closeTimeoutRef = window.setTimeout(() => {
437
+ this.closeTimeoutRef = null;
438
+ if (this.abortController.signal.aborted) return;
439
+ if (!this.content) return;
440
+ this.unportalContent();
441
+ this.content.style.display = "none";
442
+ this.unregisterAsOpenChildOnParent();
443
+ this.isClosing = false;
444
+ }, this.animationDuration);
445
+ }
446
+
447
+ private closeNestedPopovers() {
448
+ if (!this.content) return;
449
+
450
+ const nestedPopovers = this.content.querySelectorAll<HTMLElement>(
451
+ '.starwind-popover[data-state="open"]',
452
+ );
453
+
454
+ for (const nestedPopover of nestedPopovers) {
455
+ if (nestedPopover !== this.popover) {
456
+ nestedPopover.dispatchEvent(
457
+ new CustomEvent("starwind-popover:close", { detail: { focusTrigger: false } }),
458
+ );
459
+ }
460
+ }
461
+ }
462
+
463
+ destroy() {
464
+ if (this.abortController.signal.aborted) return;
465
+
466
+ this.abortController.abort();
467
+
468
+ if (this.usesGlobalPointerTracking) {
469
+ unregisterGlobalPointerTracking();
470
+ this.usesGlobalPointerTracking = false;
471
+ }
472
+
473
+ this.clearHoverCloseTimer();
474
+
475
+ if (this.closeTimeoutRef !== null) {
476
+ window.clearTimeout(this.closeTimeoutRef);
477
+ this.closeTimeoutRef = null;
478
+ }
479
+
480
+ this.unportalContent();
481
+ this.unregisterAsOpenChildOnParent();
482
+ this.isOpen = false;
483
+ this.isClosing = false;
484
+
485
+ if (this.content) {
486
+ delete (this.content as any).__popoverHandler;
487
+ }
488
+ }
489
+
490
+ private closePopoverDelayed() {
491
+ if (!this.openOnHover) return;
492
+
493
+ this.clearHoverCloseTimer();
494
+
495
+ this.hoverCloseTimerRef = window.setTimeout(() => {
496
+ this.hoverCloseTimerRef = null;
497
+ if (this.abortController.signal.aborted) return;
498
+ if (this.openChildCount > 0) return;
499
+ if (this.isFocusWithinPopoverTree()) return;
500
+ if (this.isPointerWithinPopoverTree()) return;
501
+ if (this.isOpen) {
502
+ this.closePopover();
503
+ }
504
+ }, this.hoverCloseDelay);
505
+ }
506
+
507
+ private clearHoverCloseTimer() {
508
+ if (this.hoverCloseTimerRef !== null) {
509
+ window.clearTimeout(this.hoverCloseTimerRef);
510
+ this.hoverCloseTimerRef = null;
511
+ }
512
+ }
513
+
514
+ private clearHoverCloseTimersInTree() {
515
+ this.clearHoverCloseTimer();
516
+
517
+ let currentHandler = this.parentHandler;
518
+ while (currentHandler) {
519
+ currentHandler.clearHoverCloseTimer();
520
+ currentHandler = currentHandler.parentHandler;
521
+ }
522
+ }
523
+
524
+ private isFocusWithinPopoverTree(): boolean {
525
+ const activeElement = document.activeElement;
526
+ if (!(activeElement instanceof Node)) return false;
527
+
528
+ return (
529
+ this.popover.contains(activeElement) ||
530
+ this.content?.contains(activeElement) ||
531
+ this.isTargetInPopoverTree(activeElement)
532
+ );
533
+ }
534
+
535
+ private isPointerWithinPopoverTree(): boolean {
536
+ if (this.trigger?.matches(":hover") || this.content?.matches(":hover")) {
537
+ return true;
538
+ }
539
+
540
+ if (globalPointerClientX === null || globalPointerClientY === null) {
541
+ return false;
542
+ }
543
+
544
+ const pointerTarget = document.elementFromPoint(globalPointerClientX, globalPointerClientY);
545
+ if (!(pointerTarget instanceof Node)) {
546
+ return false;
547
+ }
548
+
549
+ return (
550
+ this.popover.contains(pointerTarget) ||
551
+ this.content?.contains(pointerTarget) ||
552
+ this.isTargetInPopoverTree(pointerTarget)
553
+ );
554
+ }
555
+
556
+ private registerAsOpenChildOnParent() {
557
+ if (!this.parentHandler || this.registeredWithParentAsOpenChild) return;
558
+
559
+ this.parentHandler.openChildCount++;
560
+ this.registeredWithParentAsOpenChild = true;
561
+ }
562
+
563
+ private unregisterAsOpenChildOnParent() {
564
+ if (!this.parentHandler || !this.registeredWithParentAsOpenChild) return;
565
+
566
+ this.parentHandler.openChildCount = Math.max(0, this.parentHandler.openChildCount - 1);
567
+ this.registeredWithParentAsOpenChild = false;
568
+ this.parentHandler.handleChildPopoverClosed();
569
+ }
570
+
571
+ private handleChildPopoverClosed() {
572
+ if (!this.openOnHover || !this.isOpen) return;
573
+ if (this.openChildCount > 0) return;
574
+ if (this.isFocusWithinPopoverTree()) return;
575
+ if (this.isPointerWithinPopoverTree()) return;
576
+
577
+ this.closePopoverDelayed();
578
+ }
579
+
580
+ private isTargetInPopoverTree(target: Node): boolean {
581
+ const element = target instanceof HTMLElement ? target : target.parentElement;
582
+ if (!element) return false;
583
+
584
+ const contentElement = element.closest('[data-slot="popover-content"]');
585
+ if (!contentElement) return false;
586
+
587
+ let currentHandler: PopoverHandler | null =
588
+ ((contentElement as any).__popoverHandler as PopoverHandler | undefined) ?? null;
589
+
590
+ while (currentHandler) {
591
+ if (currentHandler === this) {
592
+ return true;
593
+ }
594
+
595
+ currentHandler = currentHandler.parentHandler;
596
+ }
597
+
598
+ return false;
599
+ }
600
+
601
+ private portalContent() {
602
+ if (!this.content) return;
603
+ if (this.contentPlaceholder || this.content.parentElement === document.body) return;
604
+
605
+ this.contentPlaceholder = document.createComment("popover-content-placeholder");
606
+ this.content.parentNode?.insertBefore(this.contentPlaceholder, this.content);
607
+
608
+ document.body.appendChild(this.content);
609
+
610
+ this.content.style.position = "fixed";
611
+ this.content.style.zIndex = "50";
612
+
613
+ const updatePosition = () => this.positionContent();
614
+ window.addEventListener("scroll", updatePosition, true);
615
+ window.addEventListener("resize", updatePosition);
616
+
617
+ const resizeObserver = new ResizeObserver(() => this.positionContent());
618
+ resizeObserver.observe(this.content);
619
+ if (this.trigger) {
620
+ resizeObserver.observe(this.trigger);
621
+ }
622
+
623
+ this.cleanupAutoUpdate = () => {
624
+ window.removeEventListener("scroll", updatePosition, true);
625
+ window.removeEventListener("resize", updatePosition);
626
+ resizeObserver.disconnect();
627
+ };
628
+ }
629
+
630
+ private unportalContent() {
631
+ if (!this.content) return;
632
+
633
+ this.cleanupAutoUpdate?.();
634
+ this.cleanupAutoUpdate = null;
635
+
636
+ if (this.contentPlaceholder) {
637
+ this.contentPlaceholder.parentNode?.insertBefore(this.content, this.contentPlaceholder);
638
+ this.contentPlaceholder.remove();
639
+ this.contentPlaceholder = null;
640
+ }
641
+
642
+ this.content.style.removeProperty("position");
643
+ this.content.style.removeProperty("z-index");
644
+ this.content.style.removeProperty("top");
645
+ this.content.style.removeProperty("left");
646
+ this.content.style.removeProperty("transform-origin");
647
+ }
648
+
649
+ private positionContent() {
650
+ if (!this.content || !this.trigger || !this.isOpen) return;
651
+
652
+ const triggerRect = this.trigger.getBoundingClientRect();
653
+ const contentWidth = this.content.offsetWidth;
654
+ const contentHeight = this.content.offsetHeight;
655
+ const viewportWidth = window.innerWidth;
656
+ const viewportHeight = window.innerHeight;
657
+ const viewportPadding = 8;
658
+
659
+ const resolvedPlacement = resolvePlacement({
660
+ side: this.side,
661
+ align: this.align,
662
+ sideOffset: this.sideOffset,
663
+ triggerRect,
664
+ contentWidth,
665
+ contentHeight,
666
+ viewportWidth,
667
+ viewportHeight,
668
+ viewportPadding,
669
+ avoidCollisions: true,
670
+ });
671
+
672
+ this.content.style.left = `${Math.round(resolvedPlacement.left)}px`;
673
+ this.content.style.top = `${Math.round(resolvedPlacement.top)}px`;
674
+ this.content.style.transformOrigin = getTransformOrigin(
675
+ resolvedPlacement.side,
676
+ resolvedPlacement.align,
677
+ );
678
+ this.content.setAttribute("data-side", resolvedPlacement.side);
679
+ this.content.setAttribute("data-align", resolvedPlacement.align);
680
+ }
681
+ }
682
+
683
+ const popoverInstances = new Map<HTMLElement, PopoverHandler>();
684
+ let popoverCounter = 0;
685
+
686
+ const cleanupStalePopovers = () => {
687
+ for (const [popoverEl, popoverHandler] of popoverInstances.entries()) {
688
+ if (!document.contains(popoverEl)) {
689
+ popoverHandler.destroy();
690
+ popoverInstances.delete(popoverEl);
691
+ }
692
+ }
693
+ };
694
+
695
+ const destroyAllPopovers = () => {
696
+ for (const popoverHandler of popoverInstances.values()) {
697
+ popoverHandler.destroy();
698
+ }
699
+
700
+ popoverInstances.clear();
701
+ };
702
+
703
+ const initPopovers = () => {
704
+ cleanupStalePopovers();
705
+
706
+ document.querySelectorAll<HTMLElement>(".starwind-popover").forEach((popoverEl) => {
707
+ if (!popoverInstances.has(popoverEl)) {
708
+ popoverInstances.set(popoverEl, new PopoverHandler(popoverEl, popoverCounter++));
709
+ }
710
+ });
711
+ };
712
+
713
+ initPopovers();
714
+ document.addEventListener("astro:before-swap", destroyAllPopovers);
715
+ document.addEventListener("astro:after-swap", initPopovers);
716
+ document.addEventListener("starwind:init", initPopovers);
717
+ </script>