@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +67 -43
- package/dist/index.js.map +1 -1
- package/dist/src/components/accordion/Accordion.astro +1 -1
- package/dist/src/components/accordion/AccordionContent.astro +1 -1
- package/dist/src/components/accordion/AccordionItem.astro +1 -1
- package/dist/src/components/accordion/AccordionTrigger.astro +1 -1
- package/dist/src/components/alert/Alert.astro +1 -1
- package/dist/src/components/alert/AlertDescription.astro +1 -1
- package/dist/src/components/alert/AlertTitle.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialog.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogAction.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogCancel.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogContent.astro +3 -3
- package/dist/src/components/alert-dialog/AlertDialogDescription.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogFooter.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogHeader.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogTitle.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialogTrigger.astro +2 -2
- package/dist/src/components/aspect-ratio/AspectRatio.astro +1 -1
- package/dist/src/components/avatar/Avatar.astro +1 -1
- package/dist/src/components/avatar/AvatarFallback.astro +1 -1
- package/dist/src/components/avatar/AvatarImage.astro +2 -2
- package/dist/src/components/badge/Badge.astro +1 -1
- package/dist/src/components/breadcrumb/Breadcrumb.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbEllipsis.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbItem.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbLink.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbList.astro +2 -2
- package/dist/src/components/breadcrumb/BreadcrumbPage.astro +1 -1
- package/dist/src/components/breadcrumb/BreadcrumbSeparator.astro +1 -1
- package/dist/src/components/button/Button.astro +1 -1
- package/dist/src/components/button-group/ButtonGroup.astro +1 -1
- package/dist/src/components/button-group/ButtonGroupSeparator.astro +1 -1
- package/dist/src/components/button-group/ButtonGroupText.astro +1 -1
- package/dist/src/components/card/Card.astro +1 -1
- package/dist/src/components/card/CardAction.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/carousel/Carousel.astro +1 -1
- package/dist/src/components/carousel/CarouselContent.astro +1 -1
- package/dist/src/components/carousel/CarouselItem.astro +1 -1
- package/dist/src/components/carousel/CarouselNext.astro +1 -1
- package/dist/src/components/carousel/CarouselPrevious.astro +1 -1
- package/dist/src/components/checkbox/Checkbox.astro +1 -1
- package/dist/src/components/collapsible/Collapsible.astro +1 -1
- package/dist/src/components/collapsible/CollapsibleContent.astro +1 -1
- package/dist/src/components/collapsible/CollapsibleTrigger.astro +1 -1
- package/dist/src/components/dialog/Dialog.astro +1 -1
- package/dist/src/components/dialog/DialogClose.astro +1 -1
- package/dist/src/components/dialog/DialogContent.astro +2 -2
- 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 +1 -1
- package/dist/src/components/dialog/DialogTrigger.astro +1 -1
- package/dist/src/components/dropdown/Dropdown.astro +261 -34
- package/dist/src/components/dropdown/DropdownContent.astro +41 -5
- package/dist/src/components/dropdown/DropdownItem.astro +2 -2
- package/dist/src/components/dropdown/DropdownLabel.astro +2 -2
- package/dist/src/components/dropdown/DropdownSeparator.astro +1 -1
- package/dist/src/components/dropdown/DropdownShortcut.astro +17 -0
- package/dist/src/components/dropdown/DropdownSub.astro +15 -0
- package/dist/src/components/dropdown/DropdownSubContent.astro +36 -0
- package/dist/src/components/dropdown/DropdownSubTrigger.astro +34 -0
- package/dist/src/components/dropdown/DropdownTrigger.astro +1 -1
- package/dist/src/components/dropdown/index.ts +12 -0
- package/dist/src/components/dropzone/DropzoneFilesList.astro +1 -1
- package/dist/src/components/image/Image.astro +1 -1
- package/dist/src/components/input/Input.astro +1 -1
- package/dist/src/components/input-group/InputGroup.astro +28 -0
- package/dist/src/components/input-group/InputGroupAddon.astro +79 -0
- package/dist/src/components/input-group/InputGroupButton.astro +34 -0
- package/dist/src/components/input-group/InputGroupInput.astro +16 -0
- package/dist/src/components/input-group/InputGroupText.astro +17 -0
- package/dist/src/components/input-group/InputGroupTextarea.astro +20 -0
- package/dist/src/components/input-group/index.ts +33 -0
- package/dist/src/components/input-otp/InputOtp.astro +1 -1
- package/dist/src/components/input-otp/InputOtpGroup.astro +1 -1
- package/dist/src/components/input-otp/InputOtpSeparator.astro +1 -1
- package/dist/src/components/input-otp/InputOtpSlot.astro +1 -1
- package/dist/src/components/item/Item.astro +1 -1
- package/dist/src/components/item/ItemActions.astro +1 -1
- package/dist/src/components/item/ItemContent.astro +1 -1
- package/dist/src/components/item/ItemDescription.astro +1 -1
- package/dist/src/components/item/ItemFooter.astro +1 -1
- package/dist/src/components/item/ItemGroup.astro +1 -1
- package/dist/src/components/item/ItemHeader.astro +1 -1
- package/dist/src/components/item/ItemMedia.astro +1 -1
- package/dist/src/components/item/ItemSeparator.astro +1 -1
- package/dist/src/components/item/ItemTitle.astro +1 -1
- package/dist/src/components/kbd/Kbd.astro +1 -1
- package/dist/src/components/kbd/KbdGroup.astro +1 -1
- package/dist/src/components/label/Label.astro +1 -1
- package/dist/src/components/native-select/NativeSelect.astro +64 -0
- package/dist/src/components/native-select/NativeSelectOptGroup.astro +15 -0
- package/dist/src/components/native-select/NativeSelectOption.astro +15 -0
- package/dist/src/components/native-select/index.ts +21 -0
- package/dist/src/components/pagination/Pagination.astro +1 -1
- package/dist/src/components/pagination/PaginationContent.astro +1 -1
- package/dist/src/components/pagination/PaginationEllipsis.astro +1 -1
- package/dist/src/components/pagination/PaginationItem.astro +1 -1
- package/dist/src/components/pagination/PaginationLink.astro +1 -1
- package/dist/src/components/pagination/PaginationNext.astro +1 -1
- package/dist/src/components/pagination/PaginationPrevious.astro +1 -1
- package/dist/src/components/popover/Popover.astro +717 -0
- package/dist/src/components/popover/PopoverContent.astro +102 -0
- package/dist/src/components/popover/PopoverDescription.astro +14 -0
- package/dist/src/components/popover/PopoverHeader.astro +14 -0
- package/dist/src/components/popover/PopoverTitle.astro +14 -0
- package/dist/src/components/popover/PopoverTrigger.astro +51 -0
- package/dist/src/components/popover/index.ts +34 -0
- package/dist/src/components/progress/Progress.astro +1 -1
- package/dist/src/components/prose/Prose.astro +1 -1
- package/dist/src/components/radio-group/RadioGroup.astro +1 -1
- package/dist/src/components/radio-group/RadioGroupItem.astro +1 -1
- package/dist/src/components/select/Select.astro +1 -1
- package/dist/src/components/select/SelectContent.astro +1 -1
- package/dist/src/components/select/SelectItem.astro +1 -1
- package/dist/src/components/select/SelectLabel.astro +1 -1
- package/dist/src/components/select/SelectSearch.astro +1 -1
- package/dist/src/components/select/SelectSeparator.astro +1 -1
- package/dist/src/components/select/SelectTrigger.astro +1 -1
- package/dist/src/components/select/SelectValue.astro +1 -1
- package/dist/src/components/separator/Separator.astro +1 -1
- package/dist/src/components/sheet/Sheet.astro +1 -1
- package/dist/src/components/sheet/SheetContent.astro +1 -1
- package/dist/src/components/sheet/SheetDescription.astro +1 -1
- package/dist/src/components/sheet/SheetFooter.astro +1 -1
- package/dist/src/components/sheet/SheetHeader.astro +1 -1
- package/dist/src/components/sheet/SheetTitle.astro +1 -1
- package/dist/src/components/sidebar/Sidebar.astro +19 -2
- package/dist/src/components/sidebar/SidebarContent.astro +1 -1
- package/dist/src/components/sidebar/SidebarFooter.astro +1 -1
- package/dist/src/components/sidebar/SidebarGroup.astro +1 -1
- package/dist/src/components/sidebar/SidebarGroupContent.astro +1 -1
- package/dist/src/components/sidebar/SidebarGroupLabel.astro +2 -2
- package/dist/src/components/sidebar/SidebarHeader.astro +1 -1
- package/dist/src/components/sidebar/SidebarInput.astro +1 -1
- package/dist/src/components/sidebar/SidebarInset.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenu.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuAction.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuBadge.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuButton.astro +2 -14
- package/dist/src/components/sidebar/SidebarMenuItem.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuSkeleton.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuSub.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuSubButton.astro +1 -1
- package/dist/src/components/sidebar/SidebarMenuSubItem.astro +1 -1
- package/dist/src/components/sidebar/SidebarProvider.astro +1 -1
- package/dist/src/components/sidebar/SidebarRail.astro +2 -2
- package/dist/src/components/sidebar/SidebarSeparator.astro +1 -1
- package/dist/src/components/sidebar/SidebarTrigger.astro +1 -1
- package/dist/src/components/skeleton/Skeleton.astro +1 -1
- package/dist/src/components/slider/Slider.astro +1 -1
- package/dist/src/components/spinner/Spinner.astro +1 -1
- package/dist/src/components/switch/Switch.astro +1 -1
- package/dist/src/components/table/Table.astro +1 -1
- package/dist/src/components/table/TableBody.astro +1 -1
- package/dist/src/components/table/TableCaption.astro +1 -1
- package/dist/src/components/table/TableCell.astro +1 -1
- package/dist/src/components/table/TableFoot.astro +1 -1
- package/dist/src/components/table/TableHead.astro +1 -1
- package/dist/src/components/table/TableHeader.astro +1 -1
- package/dist/src/components/table/TableRow.astro +1 -1
- package/dist/src/components/tabs/Tabs.astro +1 -1
- package/dist/src/components/tabs/TabsContent.astro +1 -1
- package/dist/src/components/tabs/TabsList.astro +1 -1
- package/dist/src/components/tabs/TabsTrigger.astro +1 -1
- package/dist/src/components/textarea/Textarea.astro +1 -1
- package/dist/src/components/theme-toggle/ThemeToggle.astro +1 -1
- package/dist/src/components/toast/ToastDescription.astro +1 -1
- package/dist/src/components/toast/ToastItem.astro +1 -1
- package/dist/src/components/toast/Toaster.astro +1 -1
- package/dist/src/components/toggle/Toggle.astro +1 -1
- package/dist/src/components/tooltip/Tooltip.astro +260 -122
- package/dist/src/components/tooltip/TooltipContent.astro +13 -23
- package/dist/src/components/video/Video.astro +2 -2
- package/dist/src/lib/utils/starwind/positioning.ts +318 -0
- package/package.json +1 -1
|
@@ -22,8 +22,8 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
22
22
|
class:list={["starwind-dropdown", "relative", className]}
|
|
23
23
|
data-open-on-hover={openOnHover ? "true" : undefined}
|
|
24
24
|
data-close-delay={closeDelay}
|
|
25
|
-
data-slot="dropdown"
|
|
26
25
|
{...rest}
|
|
26
|
+
data-slot="dropdown"
|
|
27
27
|
>
|
|
28
28
|
<slot />
|
|
29
29
|
</div>
|
|
@@ -31,7 +31,7 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
31
31
|
<script>
|
|
32
32
|
class DropdownHandler {
|
|
33
33
|
private dropdown: HTMLElement;
|
|
34
|
-
private trigger:
|
|
34
|
+
private trigger: HTMLElement | null;
|
|
35
35
|
private content: HTMLElement | null;
|
|
36
36
|
private items: HTMLElement[] = [];
|
|
37
37
|
private currentFocusIndex: number = -1;
|
|
@@ -43,23 +43,38 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
43
43
|
private closeTimerRef: number | null = null;
|
|
44
44
|
private lastOpenSource: "keyboard" | "mouse" = "keyboard";
|
|
45
45
|
private lastCloseSource: "keyboard" | "mouse" = "keyboard";
|
|
46
|
+
private isSubmenu: boolean;
|
|
47
|
+
private rootDropdown: HTMLElement;
|
|
48
|
+
private contentPlaceholder: Comment | null = null;
|
|
49
|
+
private cleanupAutoUpdate: (() => void) | null = null;
|
|
50
|
+
private openChildCount: number = 0;
|
|
51
|
+
private parentHandler: DropdownHandler | null = null;
|
|
46
52
|
|
|
47
53
|
constructor(dropdown: HTMLElement, dropdownIdx: number) {
|
|
48
54
|
this.dropdown = dropdown;
|
|
49
|
-
this.
|
|
55
|
+
this.isSubmenu = dropdown.classList.contains("starwind-dropdown-sub");
|
|
56
|
+
this.openOnHover = dropdown.getAttribute("data-open-on-hover") === "true" || this.isSubmenu;
|
|
50
57
|
this.closeDelay = parseInt(dropdown.getAttribute("data-close-delay") || "200");
|
|
51
58
|
|
|
52
|
-
//
|
|
53
|
-
|
|
59
|
+
// Find the trigger and content that belong to this dropdown, avoiding nested ones
|
|
60
|
+
this.trigger = this.findOwnElement('[data-slot="dropdown-trigger"], [data-sub-trigger]');
|
|
54
61
|
|
|
55
62
|
// if trigger is set with asChild, use the first child element for trigger button
|
|
56
|
-
if (
|
|
57
|
-
this.trigger =
|
|
58
|
-
} else {
|
|
59
|
-
this.trigger = tempTrigger as HTMLButtonElement;
|
|
63
|
+
if (this.trigger?.hasAttribute("data-as-child")) {
|
|
64
|
+
this.trigger = this.trigger.firstElementChild as HTMLElement;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
this.content =
|
|
67
|
+
this.content = this.findOwnElement('[data-slot="dropdown-content"]');
|
|
68
|
+
|
|
69
|
+
// Resolve root dropdown before any portaling (DOM is in original state)
|
|
70
|
+
this.rootDropdown =
|
|
71
|
+
(this.dropdown.closest(".starwind-dropdown") as HTMLElement) || this.dropdown;
|
|
72
|
+
|
|
73
|
+
// Store handler reference and root dropdown on content element for lookups
|
|
74
|
+
if (this.content) {
|
|
75
|
+
(this.content as any).__dropdownHandler = this;
|
|
76
|
+
(this.content as any).__rootDropdown = this.rootDropdown;
|
|
77
|
+
}
|
|
63
78
|
|
|
64
79
|
if (!this.trigger || !this.content) return;
|
|
65
80
|
|
|
@@ -74,6 +89,16 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
74
89
|
this.init(dropdownIdx);
|
|
75
90
|
}
|
|
76
91
|
|
|
92
|
+
private findOwnElement(selector: string): HTMLElement | null {
|
|
93
|
+
const elements = this.dropdown.querySelectorAll(selector);
|
|
94
|
+
for (const el of elements) {
|
|
95
|
+
if (el.closest(".starwind-dropdown, .starwind-dropdown-sub") === this.dropdown) {
|
|
96
|
+
return el as HTMLElement;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
77
102
|
private init(dropdownIdx: number) {
|
|
78
103
|
this.setupAccessibility(dropdownIdx);
|
|
79
104
|
this.setupEvents();
|
|
@@ -101,18 +126,28 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
101
126
|
this.toggleDropdown();
|
|
102
127
|
});
|
|
103
128
|
|
|
104
|
-
// Handle keyboard navigation
|
|
129
|
+
// Handle keyboard navigation on trigger
|
|
105
130
|
this.trigger.addEventListener("keydown", (e) => {
|
|
106
131
|
if (e.key === "Enter" || e.key === " ") {
|
|
107
132
|
e.preventDefault();
|
|
133
|
+
e.stopPropagation();
|
|
108
134
|
this.lastOpenSource = "keyboard";
|
|
109
|
-
this.
|
|
135
|
+
if (this.isSubmenu) {
|
|
136
|
+
this.openDropdown();
|
|
137
|
+
} else {
|
|
138
|
+
this.toggleDropdown();
|
|
139
|
+
}
|
|
110
140
|
} else if (e.key === "Escape" && this.isOpen) {
|
|
111
141
|
e.preventDefault();
|
|
142
|
+
e.stopPropagation();
|
|
112
143
|
this.lastCloseSource = "keyboard";
|
|
113
|
-
|
|
144
|
+
// Close all menus in the group, focus root trigger
|
|
145
|
+
this.rootDropdown.dispatchEvent(
|
|
146
|
+
new CustomEvent("starwind-dropdown:close-all", { detail: { focusRoot: true } }),
|
|
147
|
+
);
|
|
114
148
|
} else if (this.isOpen && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
115
149
|
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
116
151
|
this.lastOpenSource = "keyboard";
|
|
117
152
|
this.updateDropdownItems();
|
|
118
153
|
if (e.key === "ArrowDown") {
|
|
@@ -120,15 +155,37 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
120
155
|
} else {
|
|
121
156
|
this.focusItem(this.items.length - 1); // Focus last item when opening with arrow up
|
|
122
157
|
}
|
|
158
|
+
} else if (e.key === "ArrowRight" && this.isSubmenu) {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
e.stopPropagation();
|
|
161
|
+
if (!this.isOpen) {
|
|
162
|
+
this.lastOpenSource = "keyboard";
|
|
163
|
+
this.openDropdown();
|
|
164
|
+
} else {
|
|
165
|
+
this.updateDropdownItems();
|
|
166
|
+
this.focusItem(0);
|
|
167
|
+
}
|
|
123
168
|
}
|
|
124
169
|
});
|
|
125
170
|
|
|
171
|
+
// Listen for close-all event (routed through rootDropdown to work across portaled content)
|
|
172
|
+
this.rootDropdown.addEventListener("starwind-dropdown:close-all", ((e: CustomEvent) => {
|
|
173
|
+
this.closeDropdown({ skipFocus: e.detail?.focusRoot && this.isSubmenu });
|
|
174
|
+
}) as EventListener);
|
|
175
|
+
|
|
176
|
+
// Listen for cancel-close event (routed through rootDropdown)
|
|
177
|
+
this.rootDropdown.addEventListener("starwind-dropdown:cancel-close", () => {
|
|
178
|
+
this.clearCloseTimer();
|
|
179
|
+
});
|
|
180
|
+
|
|
126
181
|
// Close dropdown when clicking outside for mouse
|
|
127
182
|
document.addEventListener("pointerdown", (e) => {
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
183
|
+
if (
|
|
184
|
+
this.isOpen &&
|
|
185
|
+
!this.dropdown.contains(e.target as Node) &&
|
|
186
|
+
!this.content?.contains(e.target as Node) &&
|
|
187
|
+
!this.isTargetInGroup(e.target as Node)
|
|
188
|
+
) {
|
|
132
189
|
if (e.button === 0 && e.ctrlKey === false && e.pointerType === "mouse") {
|
|
133
190
|
this.closeDropdown();
|
|
134
191
|
}
|
|
@@ -138,8 +195,10 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
138
195
|
// Handle click outside select content to close for mobile
|
|
139
196
|
document.addEventListener("click", (e) => {
|
|
140
197
|
if (
|
|
141
|
-
|
|
142
|
-
this.
|
|
198
|
+
this.isOpen &&
|
|
199
|
+
!this.trigger?.contains(e.target as Node) &&
|
|
200
|
+
!this.content?.contains(e.target as Node) &&
|
|
201
|
+
!this.isTargetInGroup(e.target as Node)
|
|
143
202
|
) {
|
|
144
203
|
this.closeDropdown();
|
|
145
204
|
}
|
|
@@ -149,8 +208,11 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
149
208
|
this.content.addEventListener("keydown", (e) => {
|
|
150
209
|
if (e.key === "Escape") {
|
|
151
210
|
e.preventDefault();
|
|
152
|
-
|
|
153
|
-
|
|
211
|
+
e.stopPropagation();
|
|
212
|
+
// Close all menus in the group, focus root trigger
|
|
213
|
+
this.rootDropdown.dispatchEvent(
|
|
214
|
+
new CustomEvent("starwind-dropdown:close-all", { detail: { focusRoot: true } }),
|
|
215
|
+
);
|
|
154
216
|
} else if (this.isOpen) {
|
|
155
217
|
this.handleMenuKeydown(e);
|
|
156
218
|
}
|
|
@@ -161,9 +223,13 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
161
223
|
const target = e.target as HTMLElement;
|
|
162
224
|
const item = target.closest('[role="menuitem"]');
|
|
163
225
|
if (item && !(item as HTMLElement).hasAttribute("data-disabled")) {
|
|
226
|
+
if (item.hasAttribute("aria-haspopup")) return;
|
|
227
|
+
|
|
164
228
|
// Close the dropdown after item selection
|
|
165
229
|
this.closeDropdown();
|
|
166
|
-
|
|
230
|
+
|
|
231
|
+
// Dispatch close-all through root to close all menus in the group
|
|
232
|
+
this.rootDropdown.dispatchEvent(new CustomEvent("starwind-dropdown:close-all"));
|
|
167
233
|
}
|
|
168
234
|
});
|
|
169
235
|
|
|
@@ -198,6 +264,7 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
198
264
|
|
|
199
265
|
this.dropdown.addEventListener("pointerleave", (e) => {
|
|
200
266
|
if (e.pointerType !== "mouse") return;
|
|
267
|
+
if (this.openChildCount > 0) return;
|
|
201
268
|
if (this.isOpen) {
|
|
202
269
|
this.lastCloseSource = "mouse";
|
|
203
270
|
this.closeDropdownDelayed();
|
|
@@ -220,40 +287,69 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
220
287
|
if (this.items.length === 0) return;
|
|
221
288
|
|
|
222
289
|
const currentIdx = this.currentFocusIndex;
|
|
290
|
+
const currentItem = this.items[currentIdx];
|
|
223
291
|
|
|
224
292
|
switch (e.key) {
|
|
225
293
|
case "ArrowDown":
|
|
226
294
|
e.preventDefault();
|
|
295
|
+
e.stopPropagation();
|
|
227
296
|
this.focusItem(currentIdx === -1 ? 0 : currentIdx + 1);
|
|
228
297
|
break;
|
|
229
298
|
case "ArrowUp":
|
|
230
299
|
e.preventDefault();
|
|
300
|
+
e.stopPropagation();
|
|
231
301
|
this.focusItem(currentIdx === -1 ? this.items.length - 1 : currentIdx - 1);
|
|
232
302
|
break;
|
|
303
|
+
case "ArrowRight":
|
|
304
|
+
if (currentItem?.getAttribute("aria-haspopup") === "true") {
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
e.stopPropagation();
|
|
307
|
+
currentItem.click();
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
case "ArrowLeft":
|
|
311
|
+
if (this.isSubmenu) {
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
this.closeDropdown();
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
233
317
|
case "Home":
|
|
234
318
|
e.preventDefault();
|
|
319
|
+
e.stopPropagation();
|
|
235
320
|
this.focusItem(0);
|
|
236
321
|
break;
|
|
237
322
|
case "End":
|
|
238
323
|
e.preventDefault();
|
|
324
|
+
e.stopPropagation();
|
|
239
325
|
this.focusItem(this.items.length - 1);
|
|
240
326
|
break;
|
|
241
327
|
case "Enter":
|
|
242
328
|
case " ":
|
|
243
329
|
if (currentIdx !== -1) {
|
|
244
330
|
e.preventDefault();
|
|
331
|
+
e.stopPropagation();
|
|
245
332
|
this.items[currentIdx].click();
|
|
246
333
|
}
|
|
247
334
|
break;
|
|
248
335
|
}
|
|
249
336
|
}
|
|
250
337
|
|
|
338
|
+
private isTargetInGroup(target: Node): boolean {
|
|
339
|
+
const el = target instanceof HTMLElement ? target : target.parentElement;
|
|
340
|
+
if (!el) return false;
|
|
341
|
+
const contentEl = el.closest('[data-slot="dropdown-content"]');
|
|
342
|
+
return !!contentEl && (contentEl as any).__rootDropdown === this.rootDropdown;
|
|
343
|
+
}
|
|
344
|
+
|
|
251
345
|
private updateDropdownItems() {
|
|
252
346
|
if (!this.content) return;
|
|
253
|
-
// Get all interactive menuitem elements
|
|
254
|
-
this.items =
|
|
255
|
-
|
|
256
|
-
|
|
347
|
+
// Get all interactive menuitem elements belonging to this menu level only
|
|
348
|
+
this.items = (
|
|
349
|
+
Array.from(
|
|
350
|
+
this.content.querySelectorAll('[role="menuitem"]:not([data-disabled="true"])'),
|
|
351
|
+
) as HTMLElement[]
|
|
352
|
+
).filter((item) => item.closest('[role="menu"]') === this.content);
|
|
257
353
|
}
|
|
258
354
|
|
|
259
355
|
private focusItem(idx: number) {
|
|
@@ -276,13 +372,19 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
276
372
|
|
|
277
373
|
private openDropdown() {
|
|
278
374
|
if (this.isClosing) return;
|
|
279
|
-
if (!this.content || !this.trigger || this.trigger.disabled) return;
|
|
375
|
+
if (!this.content || !this.trigger || (this.trigger as any).disabled) return;
|
|
280
376
|
|
|
281
377
|
this.isOpen = true;
|
|
282
|
-
this.content.setAttribute("data-state", "open");
|
|
283
378
|
this.trigger.setAttribute("aria-expanded", "true");
|
|
284
379
|
this.content.style.removeProperty("display");
|
|
285
380
|
|
|
381
|
+
// Portal sub-content to body before triggering animation
|
|
382
|
+
if (this.isSubmenu) {
|
|
383
|
+
this.portalContent();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
this.content.setAttribute("data-state", "open");
|
|
387
|
+
|
|
286
388
|
// Update the list of dropdown items
|
|
287
389
|
this.updateDropdownItems();
|
|
288
390
|
|
|
@@ -290,9 +392,17 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
290
392
|
this.currentFocusIndex = -1;
|
|
291
393
|
|
|
292
394
|
this.positionContent();
|
|
395
|
+
|
|
396
|
+
// For submenus opened by keyboard, focus the first item
|
|
397
|
+
if (this.isSubmenu && this.lastOpenSource === "keyboard") {
|
|
398
|
+
requestAnimationFrame(() => {
|
|
399
|
+
this.focusItem(0);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
293
402
|
}
|
|
294
403
|
|
|
295
|
-
private closeDropdown() {
|
|
404
|
+
private closeDropdown(options?: { skipFocus?: boolean }) {
|
|
405
|
+
if (!this.isOpen) return;
|
|
296
406
|
if (!this.content || !this.trigger) return;
|
|
297
407
|
|
|
298
408
|
this.isClosing = true;
|
|
@@ -300,10 +410,12 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
300
410
|
this.content.setAttribute("data-state", "closed");
|
|
301
411
|
|
|
302
412
|
// Set focus back on trigger only if opened or closed by keyboard
|
|
413
|
+
// Skip focus if a close-all with focusRoot is closing this submenu (root will handle focus)
|
|
303
414
|
if (
|
|
304
|
-
!
|
|
305
|
-
this.
|
|
306
|
-
|
|
415
|
+
!options?.skipFocus &&
|
|
416
|
+
(!this.openOnHover ||
|
|
417
|
+
this.lastOpenSource === "keyboard" ||
|
|
418
|
+
this.lastCloseSource === "keyboard")
|
|
307
419
|
) {
|
|
308
420
|
requestAnimationFrame(() => {
|
|
309
421
|
if (!this.trigger) return;
|
|
@@ -314,6 +426,9 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
314
426
|
// Give the content time to animate before hiding
|
|
315
427
|
setTimeout(() => {
|
|
316
428
|
if (!this.content) return;
|
|
429
|
+
if (this.isSubmenu) {
|
|
430
|
+
this.unportalContent();
|
|
431
|
+
}
|
|
317
432
|
this.content.style.display = "none";
|
|
318
433
|
this.isClosing = false;
|
|
319
434
|
}, this.animationDuration);
|
|
@@ -346,9 +461,121 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
346
461
|
}
|
|
347
462
|
}
|
|
348
463
|
|
|
349
|
-
private
|
|
464
|
+
private portalContent() {
|
|
350
465
|
if (!this.content || !this.trigger) return;
|
|
351
466
|
|
|
467
|
+
// Save the original position with a placeholder
|
|
468
|
+
this.contentPlaceholder = document.createComment("dropdown-sub-placeholder");
|
|
469
|
+
this.content.parentNode?.insertBefore(this.contentPlaceholder, this.content);
|
|
470
|
+
|
|
471
|
+
// Move content to body
|
|
472
|
+
document.body.appendChild(this.content);
|
|
473
|
+
|
|
474
|
+
// Apply fixed positioning (overrides the CSS absolute class)
|
|
475
|
+
this.content.style.position = "fixed";
|
|
476
|
+
this.content.style.zIndex = "50";
|
|
477
|
+
|
|
478
|
+
// Position relative to trigger
|
|
479
|
+
this.positionSubContent();
|
|
480
|
+
|
|
481
|
+
// Set up auto-update for scroll/resize
|
|
482
|
+
const updatePosition = () => this.positionSubContent();
|
|
483
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
484
|
+
window.addEventListener("resize", updatePosition);
|
|
485
|
+
|
|
486
|
+
// Find and link to parent handler for child-tracking
|
|
487
|
+
const parentContent = this.trigger.closest('[data-slot="dropdown-content"]');
|
|
488
|
+
if (parentContent) {
|
|
489
|
+
this.parentHandler = (parentContent as any).__dropdownHandler || null;
|
|
490
|
+
if (this.parentHandler) {
|
|
491
|
+
this.parentHandler.openChildCount++;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Handle hover interactions for portaled content
|
|
496
|
+
const onPointerEnter = (e: PointerEvent) => {
|
|
497
|
+
if (e.pointerType !== "mouse") return;
|
|
498
|
+
this.clearCloseTimer();
|
|
499
|
+
// Cancel close timers for all handlers in the group (parents included)
|
|
500
|
+
this.rootDropdown.dispatchEvent(new CustomEvent("starwind-dropdown:cancel-close"));
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const onPointerLeave = (e: PointerEvent) => {
|
|
504
|
+
if (e.pointerType !== "mouse") return;
|
|
505
|
+
// Don't close if a child submenu is still open
|
|
506
|
+
if (this.openChildCount > 0) return;
|
|
507
|
+
this.lastCloseSource = "mouse";
|
|
508
|
+
this.closeDropdownDelayed();
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
this.content.addEventListener("pointerenter", onPointerEnter);
|
|
512
|
+
this.content.addEventListener("pointerleave", onPointerLeave);
|
|
513
|
+
|
|
514
|
+
this.cleanupAutoUpdate = () => {
|
|
515
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
516
|
+
window.removeEventListener("resize", updatePosition);
|
|
517
|
+
this.content?.removeEventListener("pointerenter", onPointerEnter);
|
|
518
|
+
this.content?.removeEventListener("pointerleave", onPointerLeave);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private unportalContent() {
|
|
523
|
+
if (!this.content || !this.contentPlaceholder) return;
|
|
524
|
+
|
|
525
|
+
// Decrement parent's open child count
|
|
526
|
+
if (this.parentHandler) {
|
|
527
|
+
this.parentHandler.openChildCount--;
|
|
528
|
+
this.parentHandler = null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Clean up auto-update and hover listeners
|
|
532
|
+
this.cleanupAutoUpdate?.();
|
|
533
|
+
this.cleanupAutoUpdate = null;
|
|
534
|
+
|
|
535
|
+
// Move content back to its original position
|
|
536
|
+
this.contentPlaceholder.parentNode?.insertBefore(this.content, this.contentPlaceholder);
|
|
537
|
+
this.contentPlaceholder.remove();
|
|
538
|
+
this.contentPlaceholder = null;
|
|
539
|
+
|
|
540
|
+
// Remove fixed positioning styles
|
|
541
|
+
this.content.style.removeProperty("position");
|
|
542
|
+
this.content.style.removeProperty("z-index");
|
|
543
|
+
this.content.style.removeProperty("top");
|
|
544
|
+
this.content.style.removeProperty("left");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private positionSubContent() {
|
|
548
|
+
if (!this.content || !this.trigger) return;
|
|
549
|
+
|
|
550
|
+
const triggerRect = this.trigger.getBoundingClientRect();
|
|
551
|
+
|
|
552
|
+
// Position to the right of the trigger by default
|
|
553
|
+
let top = triggerRect.top;
|
|
554
|
+
let left = triggerRect.right;
|
|
555
|
+
|
|
556
|
+
// Measure content dimensions
|
|
557
|
+
const contentWidth = this.content.offsetWidth;
|
|
558
|
+
const contentHeight = this.content.offsetHeight;
|
|
559
|
+
const viewportWidth = window.innerWidth;
|
|
560
|
+
const viewportHeight = window.innerHeight;
|
|
561
|
+
|
|
562
|
+
// If it would overflow on the right, flip to the left
|
|
563
|
+
if (left + contentWidth > viewportWidth) {
|
|
564
|
+
left = triggerRect.left - contentWidth;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// If it would overflow on the bottom, shift up
|
|
568
|
+
if (top + contentHeight > viewportHeight) {
|
|
569
|
+
top = Math.max(0, viewportHeight - contentHeight);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
this.content.style.top = `${top}px`;
|
|
573
|
+
this.content.style.left = `${left}px`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private positionContent() {
|
|
577
|
+
if (!this.content || !this.trigger || this.isSubmenu) return;
|
|
578
|
+
|
|
352
579
|
// Set content width to match trigger width
|
|
353
580
|
this.content.style.width = "var(--starwind-dropdown-trigger-width)";
|
|
354
581
|
this.content.style.setProperty(
|
|
@@ -364,7 +591,7 @@ const { class: className, openOnHover = false, closeDelay = 200, ...rest } = Ast
|
|
|
364
591
|
|
|
365
592
|
// Initialize dropdowns
|
|
366
593
|
const initDropdowns = () => {
|
|
367
|
-
document.querySelectorAll(".starwind-dropdown").forEach((dropdown) => {
|
|
594
|
+
document.querySelectorAll(".starwind-dropdown, .starwind-dropdown-sub").forEach((dropdown) => {
|
|
368
595
|
if (dropdown instanceof HTMLElement && !dropdownInstances.has(dropdown)) {
|
|
369
596
|
dropdownInstances.set(dropdown, new DropdownHandler(dropdown, dropdownCounter++));
|
|
370
597
|
}
|
|
@@ -7,7 +7,7 @@ type Props = HTMLAttributes<"div"> & {
|
|
|
7
7
|
* Side of the dropdown
|
|
8
8
|
* @default bottom
|
|
9
9
|
*/
|
|
10
|
-
side?: "top" | "bottom";
|
|
10
|
+
side?: "top" | "bottom" | "left" | "right";
|
|
11
11
|
/**
|
|
12
12
|
* Alignment of the dropdown
|
|
13
13
|
* @default start
|
|
@@ -37,13 +37,47 @@ export const dropdownContent = tv({
|
|
|
37
37
|
side: {
|
|
38
38
|
bottom: "slide-in-from-top-2 slide-out-to-top-2 top-full",
|
|
39
39
|
top: "slide-in-from-bottom-2 slide-out-to-bottom-2 bottom-full",
|
|
40
|
+
right: "slide-in-from-left-2 slide-out-to-left-2 top-0 left-full",
|
|
41
|
+
left: "slide-in-from-right-2 slide-out-to-right-2 top-0 right-full",
|
|
40
42
|
},
|
|
41
43
|
align: {
|
|
42
|
-
start: "
|
|
43
|
-
center: "
|
|
44
|
-
end: "
|
|
44
|
+
start: "",
|
|
45
|
+
center: "",
|
|
46
|
+
end: "",
|
|
45
47
|
},
|
|
46
48
|
},
|
|
49
|
+
compoundVariants: [
|
|
50
|
+
{
|
|
51
|
+
side: ["top", "bottom"],
|
|
52
|
+
align: "start",
|
|
53
|
+
class: "slide-in-from-left-1 slide-out-to-left-1 left-0",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
side: ["top", "bottom"],
|
|
57
|
+
align: "center",
|
|
58
|
+
class: "left-1/2 -translate-x-1/2",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
side: ["top", "bottom"],
|
|
62
|
+
align: "end",
|
|
63
|
+
class: "slide-in-from-right-1 slide-out-to-right-1 right-0",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
side: ["left", "right"],
|
|
67
|
+
align: "start",
|
|
68
|
+
class: "top-0",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
side: ["left", "right"],
|
|
72
|
+
align: "center",
|
|
73
|
+
class: "top-1/2 -translate-y-1/2",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
side: ["left", "right"],
|
|
77
|
+
align: "end",
|
|
78
|
+
class: "bottom-0",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
47
81
|
defaultVariants: {
|
|
48
82
|
side: "bottom",
|
|
49
83
|
align: "start",
|
|
@@ -66,7 +100,6 @@ const {
|
|
|
66
100
|
data-side={side}
|
|
67
101
|
data-align={align}
|
|
68
102
|
data-state="closed"
|
|
69
|
-
data-slot="dropdown-content"
|
|
70
103
|
tabindex="-1"
|
|
71
104
|
aria-orientation="vertical"
|
|
72
105
|
style={{
|
|
@@ -74,8 +107,11 @@ const {
|
|
|
74
107
|
animationDuration: `${animationDuration}ms`,
|
|
75
108
|
marginTop: side === "bottom" ? `${sideOffset}px` : undefined,
|
|
76
109
|
marginBottom: side === "top" ? `${sideOffset}px` : undefined,
|
|
110
|
+
marginLeft: side === "right" ? `${sideOffset}px` : undefined,
|
|
111
|
+
marginRight: side === "left" ? `${sideOffset}px` : undefined,
|
|
77
112
|
}}
|
|
78
113
|
{...rest}
|
|
114
|
+
data-slot="dropdown-content"
|
|
79
115
|
>
|
|
80
116
|
<slot />
|
|
81
117
|
</div>
|
|
@@ -17,7 +17,7 @@ export const dropdownItem = tv({
|
|
|
17
17
|
base: [
|
|
18
18
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 transition-colors outline-none select-none",
|
|
19
19
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
20
|
-
"[&>svg]:size-4 [&>svg]:shrink-0",
|
|
20
|
+
"group/dropdown-item [&>svg]:size-4 [&>svg]:shrink-0",
|
|
21
21
|
],
|
|
22
22
|
variants: {
|
|
23
23
|
inset: {
|
|
@@ -41,8 +41,8 @@ const { class: className, inset = false, disabled = false, as: Tag = "div", ...r
|
|
|
41
41
|
role="menuitem"
|
|
42
42
|
tabindex={disabled ? "-1" : "0"}
|
|
43
43
|
data-disabled={disabled ? "true" : undefined}
|
|
44
|
-
data-slot="dropdown-item"
|
|
45
44
|
{...rest}
|
|
45
|
+
data-slot="dropdown-item"
|
|
46
46
|
>
|
|
47
47
|
<slot />
|
|
48
48
|
</Tag>
|
|
@@ -10,7 +10,7 @@ type Props = HTMLAttributes<"div"> & {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export const dropdownLabel = tv({
|
|
13
|
-
base: ["px-2 py-1.5 font-
|
|
13
|
+
base: ["text-muted-foreground px-2 py-1.5 text-sm font-medium"],
|
|
14
14
|
variants: {
|
|
15
15
|
inset: {
|
|
16
16
|
true: "pl-8",
|
|
@@ -24,6 +24,6 @@ export const dropdownLabel = tv({
|
|
|
24
24
|
const { class: className, inset = false, ...rest } = Astro.props;
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
<div class={dropdownLabel({ inset, class: className })} data-slot="dropdown-label"
|
|
27
|
+
<div class={dropdownLabel({ inset, class: className })} {...rest} data-slot="dropdown-label">
|
|
28
28
|
<slot />
|
|
29
29
|
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<"span">;
|
|
5
|
+
|
|
6
|
+
const { class: className, ...rest } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<span
|
|
10
|
+
class:list={[
|
|
11
|
+
"group-focus/dropdown-item:text-accent-foreground text-muted-foreground ml-auto text-sm tracking-widest transition-colors",
|
|
12
|
+
className,
|
|
13
|
+
]}
|
|
14
|
+
{...rest}
|
|
15
|
+
>
|
|
16
|
+
<slot />
|
|
17
|
+
</span>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<"div">;
|
|
5
|
+
|
|
6
|
+
const { class: className, ...rest } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div
|
|
10
|
+
class:list={["starwind-dropdown-sub", "relative", className]}
|
|
11
|
+
{...rest}
|
|
12
|
+
data-slot="dropdown-sub"
|
|
13
|
+
>
|
|
14
|
+
<slot />
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
|
|
4
|
+
import { dropdownContent } from "./DropdownContent.astro";
|
|
5
|
+
|
|
6
|
+
type Props = HTMLAttributes<"div"> & {
|
|
7
|
+
/**
|
|
8
|
+
* Open and close animation duration in milliseconds
|
|
9
|
+
* @default 150
|
|
10
|
+
*/
|
|
11
|
+
animationDuration?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const { class: className, animationDuration = 150, ...rest } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class={dropdownContent({
|
|
19
|
+
side: "right",
|
|
20
|
+
align: "start",
|
|
21
|
+
class: ["starwind-dropdown-sub-content", className],
|
|
22
|
+
})}
|
|
23
|
+
data-sub-content
|
|
24
|
+
role="menu"
|
|
25
|
+
data-state="closed"
|
|
26
|
+
tabindex="-1"
|
|
27
|
+
aria-orientation="vertical"
|
|
28
|
+
style={{
|
|
29
|
+
display: "none",
|
|
30
|
+
animationDuration: `${animationDuration}ms`,
|
|
31
|
+
}}
|
|
32
|
+
{...rest}
|
|
33
|
+
data-slot="dropdown-content"
|
|
34
|
+
>
|
|
35
|
+
<slot />
|
|
36
|
+
</div>
|