@epoch32/visage 1.3.1 → 1.3.2
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/devtools/index.js +2 -2
- package/dist/{index-ptj8rt6b.js → index-dz6n88aq.js} +1 -1
- package/dist/{index-0qat98zs.js → index-k7zd3awa.js} +11 -2
- package/dist/{index-b7emqmej.js → index-kykzcs7p.js} +2 -1
- package/dist/{index-aagpn02x.js → index-vbsc8z8h.js} +118 -49
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -4
- package/dist/router/index.js +1 -1
- package/dist/router/router.d.ts +1 -0
- package/dist/ui/dropdown-menu.d.ts +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +4 -2
- package/dist/ui/overlay.d.ts +53 -0
- package/dist/ui/popover.d.ts +1 -1
- package/dist/ui/utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/router/router.tsx +12 -2
- package/src/ui/command-dialog.tsx +18 -1
- package/src/ui/dialog.tsx +16 -14
- package/src/ui/dropdown-menu.tsx +17 -14
- package/src/ui/index.ts +4 -0
- package/src/ui/overlay.ts +130 -0
- package/src/ui/popover.tsx +20 -22
- package/src/ui/utils.ts +8 -2
package/dist/devtools/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
highlightElement2,
|
|
5
5
|
VisageDevTools2,
|
|
6
6
|
connectDevTools2
|
|
7
|
-
} from "../index-
|
|
8
|
-
import"../index-
|
|
7
|
+
} from "../index-dz6n88aq.js";
|
|
8
|
+
import"../index-kykzcs7p.js";
|
|
9
9
|
import"../index-enc8cmea.js";
|
|
10
10
|
import"../index-3y73zgbk.js";
|
|
11
11
|
import"../index-xby428n5.js";
|
|
@@ -167,7 +167,16 @@ function Routes2(props) {
|
|
|
167
167
|
}
|
|
168
168
|
function Link2(props) {
|
|
169
169
|
const router = useContext2(RouterContext);
|
|
170
|
-
const {
|
|
170
|
+
const {
|
|
171
|
+
href,
|
|
172
|
+
class: classProp,
|
|
173
|
+
className: classNameProp,
|
|
174
|
+
activeClass = "active",
|
|
175
|
+
replace,
|
|
176
|
+
children,
|
|
177
|
+
...rest
|
|
178
|
+
} = props;
|
|
179
|
+
const rawClass = classProp ?? classNameProp;
|
|
171
180
|
const handleClick = (e) => {
|
|
172
181
|
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) {
|
|
173
182
|
return;
|
|
@@ -186,7 +195,7 @@ function Link2(props) {
|
|
|
186
195
|
return current === href || href !== "/" && current.startsWith(href);
|
|
187
196
|
});
|
|
188
197
|
const computedClass = computed2(() => {
|
|
189
|
-
const baseCls = typeof
|
|
198
|
+
const baseCls = typeof rawClass === "function" ? rawClass() : rawClass ?? "";
|
|
190
199
|
if (activeClass && isActive()) {
|
|
191
200
|
return baseCls ? `${baseCls} ${activeClass}` : activeClass;
|
|
192
201
|
}
|
|
@@ -69,7 +69,8 @@ function onClickOutside2(elements, handler) {
|
|
|
69
69
|
const target = event.target;
|
|
70
70
|
if (!target)
|
|
71
71
|
return;
|
|
72
|
-
const
|
|
72
|
+
const raw = typeof elements === "function" ? elements() : elements;
|
|
73
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
73
74
|
const isInside = list.some((el) => el?.contains(target));
|
|
74
75
|
if (!isInside) {
|
|
75
76
|
handler(event);
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
createControllableSignal2,
|
|
22
22
|
trapFocus2,
|
|
23
23
|
onClickOutside2
|
|
24
|
-
} from "./index-
|
|
24
|
+
} from "./index-kykzcs7p.js";
|
|
25
25
|
|
|
26
26
|
// src/ui/accordion.tsx
|
|
27
27
|
var AccordionContext = createContext2(null);
|
|
@@ -539,6 +539,67 @@ var BottomBar2 = Object.assign(BottomBarRoot2, {
|
|
|
539
539
|
Badge: BottomBarBadge2,
|
|
540
540
|
Separator: BottomBarSeparator2
|
|
541
541
|
});
|
|
542
|
+
// src/ui/overlay.ts
|
|
543
|
+
function useDismiss2(element, options) {
|
|
544
|
+
const isEnabled = () => {
|
|
545
|
+
if (typeof options.enabled === "function")
|
|
546
|
+
return options.enabled();
|
|
547
|
+
return options.enabled ?? true;
|
|
548
|
+
};
|
|
549
|
+
const handleKeyDown = (e) => {
|
|
550
|
+
if (!isEnabled() || e.defaultPrevented)
|
|
551
|
+
return;
|
|
552
|
+
if (e.key === "Escape" && (options.escapeKey ?? true)) {
|
|
553
|
+
if (options.onEscapeKeyDown) {
|
|
554
|
+
options.onEscapeKeyDown(e);
|
|
555
|
+
}
|
|
556
|
+
if (!e.defaultPrevented) {
|
|
557
|
+
options.onDismiss();
|
|
558
|
+
e.stopPropagation();
|
|
559
|
+
}
|
|
560
|
+
} else if (e.key === "Tab" && options.trapFocus) {
|
|
561
|
+
const el = element();
|
|
562
|
+
if (el) {
|
|
563
|
+
trapFocus2(el, e);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
const handleClick = (e) => {
|
|
568
|
+
if (!isEnabled() || e.defaultPrevented)
|
|
569
|
+
return;
|
|
570
|
+
if (options.closeSelector) {
|
|
571
|
+
const target = e.target;
|
|
572
|
+
if (target?.closest(options.closeSelector)) {
|
|
573
|
+
options.onDismiss();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
if (typeof document !== "undefined") {
|
|
578
|
+
const unbind = onClickOutside2(() => {
|
|
579
|
+
const el = element();
|
|
580
|
+
const rawIgnores = typeof options.outsideIgnore === "function" ? options.outsideIgnore() : options.outsideIgnore;
|
|
581
|
+
const ignores = Array.isArray(rawIgnores) ? rawIgnores : rawIgnores ? [rawIgnores] : [];
|
|
582
|
+
return [el, ...ignores];
|
|
583
|
+
}, (event) => {
|
|
584
|
+
if (!isEnabled())
|
|
585
|
+
return;
|
|
586
|
+
if (options.outsidePointer === false)
|
|
587
|
+
return;
|
|
588
|
+
if (options.onOutsidePointer) {
|
|
589
|
+
options.onOutsidePointer(event);
|
|
590
|
+
}
|
|
591
|
+
if (!event.defaultPrevented) {
|
|
592
|
+
options.onDismiss();
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
onCleanup2(unbind);
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
handleKeyDown,
|
|
599
|
+
handleClick
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
542
603
|
// src/ui/command-dialog.tsx
|
|
543
604
|
function CommandDialog2(props) {
|
|
544
605
|
const [open, setOpen] = createControllableSignal2({
|
|
@@ -546,6 +607,13 @@ function CommandDialog2(props) {
|
|
|
546
607
|
defaultValue: props.defaultOpen ?? false,
|
|
547
608
|
onChange: props.onOpenChange
|
|
548
609
|
});
|
|
610
|
+
const contentRef = { current: null };
|
|
611
|
+
const dismiss = useDismiss2(() => contentRef.current, {
|
|
612
|
+
enabled: () => open(),
|
|
613
|
+
onDismiss: () => setOpen(false),
|
|
614
|
+
escapeKey: true,
|
|
615
|
+
outsidePointer: false
|
|
616
|
+
});
|
|
549
617
|
const { overlayClass, contentClass, style, ...rest } = props;
|
|
550
618
|
return /* @__PURE__ */ jsx2(Portal2, {
|
|
551
619
|
children: /* @__PURE__ */ jsx2("div", {
|
|
@@ -565,7 +633,11 @@ function CommandDialog2(props) {
|
|
|
565
633
|
if (e.target === e.currentTarget)
|
|
566
634
|
setOpen(false);
|
|
567
635
|
},
|
|
636
|
+
onKeyDown: dismiss.handleKeyDown,
|
|
568
637
|
children: /* @__PURE__ */ jsx2("div", {
|
|
638
|
+
ref: (el) => {
|
|
639
|
+
contentRef.current = el;
|
|
640
|
+
},
|
|
569
641
|
style: { width: "100%", maxWidth: "560px" },
|
|
570
642
|
class: contentClass,
|
|
571
643
|
children: /* @__PURE__ */ jsx2(CommandRoot, {
|
|
@@ -1251,27 +1323,27 @@ function DialogOverlay2(props) {
|
|
|
1251
1323
|
}
|
|
1252
1324
|
function DialogContent2(props) {
|
|
1253
1325
|
const ctx = useContext2(DialogContext);
|
|
1326
|
+
const dismiss = useDismiss2(() => ctx?.contentRef.current, {
|
|
1327
|
+
enabled: () => ctx?.open() ?? false,
|
|
1328
|
+
onDismiss: () => ctx?.setOpen(false),
|
|
1329
|
+
escapeKey: true,
|
|
1330
|
+
trapFocus: true,
|
|
1331
|
+
outsidePointer: false,
|
|
1332
|
+
closeSelector: "[data-dialog-close]",
|
|
1333
|
+
onEscapeKeyDown: (e) => {
|
|
1334
|
+
if (props.onKeyDown)
|
|
1335
|
+
props.onKeyDown(e);
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1254
1338
|
const handleKeyDown = (e) => {
|
|
1255
1339
|
if (props.onKeyDown)
|
|
1256
1340
|
props.onKeyDown(e);
|
|
1257
|
-
|
|
1258
|
-
return;
|
|
1259
|
-
if (e.key === "Escape") {
|
|
1260
|
-
ctx?.setOpen(false);
|
|
1261
|
-
e.stopPropagation();
|
|
1262
|
-
} else if (e.key === "Tab" && ctx?.contentRef.current) {
|
|
1263
|
-
trapFocus2(ctx.contentRef.current, e);
|
|
1264
|
-
}
|
|
1341
|
+
dismiss.handleKeyDown(e);
|
|
1265
1342
|
};
|
|
1266
1343
|
const handleClick = (e) => {
|
|
1267
1344
|
if (props.onClick)
|
|
1268
1345
|
props.onClick(e);
|
|
1269
|
-
|
|
1270
|
-
return;
|
|
1271
|
-
const target = e.target;
|
|
1272
|
-
if (target?.closest("[data-dialog-close]")) {
|
|
1273
|
-
ctx?.setOpen(false);
|
|
1274
|
-
}
|
|
1346
|
+
dismiss.handleClick(e);
|
|
1275
1347
|
};
|
|
1276
1348
|
const {
|
|
1277
1349
|
as: Tag = "div",
|
|
@@ -1502,6 +1574,18 @@ function DropdownMenuPortal2(props) {
|
|
|
1502
1574
|
}
|
|
1503
1575
|
function DropdownMenuContent2(props) {
|
|
1504
1576
|
const ctx = useContext2(DropdownMenuContext);
|
|
1577
|
+
const dismiss = useDismiss2(() => ctx?.contentRef.current, {
|
|
1578
|
+
enabled: () => ctx?.open() ?? false,
|
|
1579
|
+
onDismiss: () => {
|
|
1580
|
+
if (ctx) {
|
|
1581
|
+
ctx.setOpen(false);
|
|
1582
|
+
ctx.triggerRef.current?.focus();
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
escapeKey: true,
|
|
1586
|
+
outsidePointer: true,
|
|
1587
|
+
outsideIgnore: () => [ctx?.triggerRef.current]
|
|
1588
|
+
});
|
|
1505
1589
|
const handleKeyDown = (e) => {
|
|
1506
1590
|
if (props.onKeyDown)
|
|
1507
1591
|
props.onKeyDown(e);
|
|
@@ -1528,10 +1612,7 @@ function DropdownMenuContent2(props) {
|
|
|
1528
1612
|
items[items.length - 1]?.focus();
|
|
1529
1613
|
e.preventDefault();
|
|
1530
1614
|
} else if (e.key === "Escape") {
|
|
1531
|
-
|
|
1532
|
-
ctx.setOpen(false);
|
|
1533
|
-
ctx.triggerRef.current?.focus();
|
|
1534
|
-
}
|
|
1615
|
+
dismiss.handleKeyDown(e);
|
|
1535
1616
|
e.preventDefault();
|
|
1536
1617
|
} else if (e.key === "Tab") {
|
|
1537
1618
|
if (ctx)
|
|
@@ -1552,14 +1633,6 @@ function DropdownMenuContent2(props) {
|
|
|
1552
1633
|
ref: (el) => {
|
|
1553
1634
|
if (ctx) {
|
|
1554
1635
|
ctx.contentRef.current = el;
|
|
1555
|
-
if (el) {
|
|
1556
|
-
const cleanup = onClickOutside2([ctx.triggerRef.current, el], () => {
|
|
1557
|
-
if (ctx.open()) {
|
|
1558
|
-
ctx.setOpen(false);
|
|
1559
|
-
}
|
|
1560
|
-
});
|
|
1561
|
-
onCleanup2(cleanup);
|
|
1562
|
-
}
|
|
1563
1636
|
}
|
|
1564
1637
|
},
|
|
1565
1638
|
id: ctx?.menuId,
|
|
@@ -1776,26 +1849,30 @@ function PopoverPortal2(props) {
|
|
|
1776
1849
|
}
|
|
1777
1850
|
function PopoverContent2(props) {
|
|
1778
1851
|
const ctx = useContext2(PopoverContext);
|
|
1852
|
+
const dismiss = useDismiss2(() => ctx?.contentRef.current, {
|
|
1853
|
+
enabled: () => ctx?.open() ?? false,
|
|
1854
|
+
onDismiss: () => ctx?.setOpen(false),
|
|
1855
|
+
escapeKey: true,
|
|
1856
|
+
outsidePointer: true,
|
|
1857
|
+
outsideIgnore: () => [ctx?.triggerRef.current],
|
|
1858
|
+
closeSelector: "[data-popover-close]",
|
|
1859
|
+
onEscapeKeyDown: (e) => {
|
|
1860
|
+
if (props.onKeyDown)
|
|
1861
|
+
props.onKeyDown(e);
|
|
1862
|
+
if (!e.defaultPrevented) {
|
|
1863
|
+
ctx?.triggerRef.current?.focus();
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1779
1867
|
const handleKeyDown = (e) => {
|
|
1780
1868
|
if (props.onKeyDown)
|
|
1781
1869
|
props.onKeyDown(e);
|
|
1782
|
-
|
|
1783
|
-
return;
|
|
1784
|
-
if (e.key === "Escape" && ctx) {
|
|
1785
|
-
ctx.setOpen(false);
|
|
1786
|
-
ctx.triggerRef.current?.focus();
|
|
1787
|
-
e.stopPropagation();
|
|
1788
|
-
}
|
|
1870
|
+
dismiss.handleKeyDown(e);
|
|
1789
1871
|
};
|
|
1790
1872
|
const handleClick = (e) => {
|
|
1791
1873
|
if (props.onClick)
|
|
1792
1874
|
props.onClick(e);
|
|
1793
|
-
|
|
1794
|
-
return;
|
|
1795
|
-
const target = e.target;
|
|
1796
|
-
if (target?.closest("[data-popover-close]")) {
|
|
1797
|
-
ctx?.setOpen(false);
|
|
1798
|
-
}
|
|
1875
|
+
dismiss.handleClick(e);
|
|
1799
1876
|
};
|
|
1800
1877
|
const {
|
|
1801
1878
|
as: Tag = "div",
|
|
@@ -1812,14 +1889,6 @@ function PopoverContent2(props) {
|
|
|
1812
1889
|
ref: (el) => {
|
|
1813
1890
|
if (ctx) {
|
|
1814
1891
|
ctx.contentRef.current = el;
|
|
1815
|
-
if (el) {
|
|
1816
|
-
const cleanup = onClickOutside2([ctx.triggerRef.current, el], () => {
|
|
1817
|
-
if (ctx.open()) {
|
|
1818
|
-
ctx.setOpen(false);
|
|
1819
|
-
}
|
|
1820
|
-
});
|
|
1821
|
-
onCleanup2(cleanup);
|
|
1822
|
-
}
|
|
1823
1892
|
}
|
|
1824
1893
|
},
|
|
1825
1894
|
id: ctx?.contentId,
|
|
@@ -2034,4 +2103,4 @@ var Toast2 = Object.assign(ToastRoot, {
|
|
|
2034
2103
|
Description: ToastDescription2,
|
|
2035
2104
|
Close: ToastClose2
|
|
2036
2105
|
});
|
|
2037
|
-
export { useAccordionContext2, useAccordionItemContext2, AccordionItem2, AccordionTrigger2, AccordionContent2, Accordion2, BottomBarAction2, BottomBarBadge2, useBottomBarContext2, BottomBarItem2, BottomBarRoot2, BottomBarSeparator2, BottomBar2, CommandDialog2, useCommandContext2, CommandInput2, CommandList2, CommandEmpty2, CommandGroup2, CommandItem2, Command2, DataTable2, useDialogContext2, DialogTrigger2, DialogPortal2, DialogOverlay2, DialogContent2, DialogTitle2, DialogDescription2, DialogClose2, Dialog2, useDropdownMenuContext2, DropdownMenuTrigger2, DropdownMenuPortal2, DropdownMenuContent2, DropdownMenuItem2, DropdownMenuSeparator2, DropdownMenu2, createIcon2, usePopoverContext2, PopoverTrigger2, PopoverPortal2, PopoverContent2, PopoverClose2, Popover2, createToastManager2, defaultToastManager2, toast2, useToast2, ToastProvider2, ToastViewport2, ToastTitle2, ToastDescription2, ToastClose2, Toast2 };
|
|
2106
|
+
export { useAccordionContext2, useAccordionItemContext2, AccordionItem2, AccordionTrigger2, AccordionContent2, Accordion2, BottomBarAction2, BottomBarBadge2, useBottomBarContext2, BottomBarItem2, BottomBarRoot2, BottomBarSeparator2, BottomBar2, useDismiss2, CommandDialog2, useCommandContext2, CommandInput2, CommandList2, CommandEmpty2, CommandGroup2, CommandItem2, Command2, DataTable2, useDialogContext2, DialogTrigger2, DialogPortal2, DialogOverlay2, DialogContent2, DialogTitle2, DialogDescription2, DialogClose2, Dialog2, useDropdownMenuContext2, DropdownMenuTrigger2, DropdownMenuPortal2, DropdownMenuContent2, DropdownMenuItem2, DropdownMenuSeparator2, DropdownMenu2, createIcon2, usePopoverContext2, PopoverTrigger2, PopoverPortal2, PopoverContent2, PopoverClose2, Popover2, createToastManager2, defaultToastManager2, toast2, useToast2, ToastProvider2, ToastViewport2, ToastTitle2, ToastDescription2, ToastClose2, Toast2 };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export { hydrate, renderChildToString, renderToStream, renderToString, type Stre
|
|
|
11
11
|
export { batch, type CleanupFn, Computed, type Context, computed, createContext, createEffect, createMemo, createRoot, createSignal, createStore, type Dependency, Effect, effect, getActiveScope, isSignal, mutateStore, onCleanup, type ReadonlySignal, runInScope, Scope, Signal, type Store, type Subscriber, signal, untrack, unwrapStore, useContext, type WritableSignal } from "./signals";
|
|
12
12
|
export { type BaseLayoutProps, Center, type CenterProps, type ClassValue, createStyle, css, cx, DIMENSION_KEYWORDS, em, extractStyleTags, fluid, formatStyleProp, type GlassOptions, Grid, type GridProps, getScopeId, glass, hashString, hasReactiveClass, normalizeClass, px, type RadiusVariant, Row, type RowProps, radius, rem, type ScopeOptions, type ShadowVariant, Spacer, type SpacerProps, Stack, type StackProps, Style, type StyleClasses, StyleManager, type StyleProps, scopeCSS, scopeSelector, shadow, space, styleManager, UNITLESS_PROPERTIES, useStyle } from "./style";
|
|
13
13
|
export { html, template } from "./template";
|
|
14
|
-
export { Accordion, AccordionContent, AccordionItem, type AccordionItemProps, type AccordionMultipleProps, type AccordionProps, type AccordionSingleProps, AccordionTrigger, BottomBar, BottomBarAction, type BottomBarActionProps, BottomBarBadge, type BottomBarBadgeProps, type BottomBarContextValue, BottomBarItem, type BottomBarItemConfig, type BottomBarItemProps, type BottomBarProps, BottomBarRoot, BottomBarSeparator, type BottomBarSeparatorProps, type ColumnDef, Command, CommandDialog, type CommandDialogProps, CommandEmpty, CommandGroup, type CommandGroupData, CommandInput, type CommandInputProps, CommandItem, type CommandItemData, type CommandItemProps, CommandList, type CommandProps, type CreateTabsOptions, createControllableSignal, createIcon, createId, createTabs, createToastManager, DataTable, type DataTableProps, Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDescription, DialogOverlay, DialogPortal, type DialogProps, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, defaultToastManager, type IconNode, type IconProps, onClickOutside, Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, type SortDirection, type SortState, Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, Toast, ToastClose, type ToastData, ToastDescription, type ToastManager, type ToastOptions, type ToastProps, ToastProvider, type ToastProviderProps, ToastTitle, type ToastVariant, ToastViewport, type ToastViewportProps, Tooltip, TooltipContent, type TooltipContentProps, TooltipPortal, type TooltipProps, TooltipTrigger, toast, trapFocus, useAccordionContext, useAccordionItemContext, useBottomBarContext, useCommandContext, useDialogContext, useDropdownMenuContext, usePopoverContext, useTabsContext, useToast, useTooltipContext } from "./ui";
|
|
14
|
+
export { Accordion, AccordionContent, AccordionItem, type AccordionItemProps, type AccordionMultipleProps, type AccordionProps, type AccordionSingleProps, AccordionTrigger, BottomBar, BottomBarAction, type BottomBarActionProps, BottomBarBadge, type BottomBarBadgeProps, type BottomBarContextValue, BottomBarItem, type BottomBarItemConfig, type BottomBarItemProps, type BottomBarProps, BottomBarRoot, BottomBarSeparator, type BottomBarSeparatorProps, type ColumnDef, Command, CommandDialog, type CommandDialogProps, CommandEmpty, CommandGroup, type CommandGroupData, CommandInput, type CommandInputProps, CommandItem, type CommandItemData, type CommandItemProps, CommandList, type CommandProps, type CreateTabsOptions, createControllableSignal, createIcon, createId, createTabs, createToastManager, DataTable, type DataTableProps, Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDescription, DialogOverlay, DialogPortal, type DialogProps, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, defaultToastManager, type IconNode, type IconProps, onClickOutside, Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, type SortDirection, type SortState, Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, Toast, ToastClose, type ToastData, ToastDescription, type ToastManager, type ToastOptions, type ToastProps, ToastProvider, type ToastProviderProps, ToastTitle, type ToastVariant, ToastViewport, type ToastViewportProps, Tooltip, TooltipContent, type TooltipContentProps, TooltipPortal, type TooltipProps, TooltipTrigger, toast, trapFocus, type UseDismissOptions, useAccordionContext, useAccordionItemContext, useBottomBarContext, useCommandContext, useDialogContext, useDismiss, useDropdownMenuContext, usePopoverContext, useTabsContext, useToast, useTooltipContext } from "./ui";
|
|
15
15
|
export { createVirtualizer, VirtualGrid, type VirtualGridProps, type VirtualItem, type Virtualizer, type VirtualizerOptions, VirtualList, type VirtualListProps } from "./virtual";
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
highlightElement2,
|
|
64
64
|
VisageDevTools2,
|
|
65
65
|
connectDevTools2
|
|
66
|
-
} from "./index-
|
|
66
|
+
} from "./index-dz6n88aq.js";
|
|
67
67
|
import {
|
|
68
68
|
createForm2
|
|
69
69
|
} from "./index-1jcd8gjf.js";
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
useNavigate2,
|
|
78
78
|
useParams2,
|
|
79
79
|
useSearchParams2
|
|
80
|
-
} from "./index-
|
|
80
|
+
} from "./index-k7zd3awa.js";
|
|
81
81
|
import {
|
|
82
82
|
createClient2,
|
|
83
83
|
RouterImpl2,
|
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
BottomBarRoot2,
|
|
111
111
|
BottomBarSeparator2,
|
|
112
112
|
BottomBar2,
|
|
113
|
+
useDismiss2,
|
|
113
114
|
CommandDialog2,
|
|
114
115
|
useCommandContext2,
|
|
115
116
|
CommandInput2,
|
|
@@ -152,7 +153,7 @@ import {
|
|
|
152
153
|
ToastDescription2,
|
|
153
154
|
ToastClose2,
|
|
154
155
|
Toast2
|
|
155
|
-
} from "./index-
|
|
156
|
+
} from "./index-vbsc8z8h.js";
|
|
156
157
|
import {
|
|
157
158
|
createVirtualizer2,
|
|
158
159
|
VirtualList2,
|
|
@@ -198,7 +199,7 @@ import {
|
|
|
198
199
|
Tooltip3,
|
|
199
200
|
SwitchThumb2,
|
|
200
201
|
Switch2
|
|
201
|
-
} from "./index-
|
|
202
|
+
} from "./index-kykzcs7p.js";
|
|
202
203
|
import {
|
|
203
204
|
ERROR_CODE_TO_STATUS2,
|
|
204
205
|
STATUS_TO_ERROR_CODE2,
|
|
@@ -459,6 +460,7 @@ export {
|
|
|
459
460
|
useCommandContext2 as useCommandContext,
|
|
460
461
|
useContext2 as useContext,
|
|
461
462
|
useDialogContext2 as useDialogContext,
|
|
463
|
+
useDismiss2 as useDismiss,
|
|
462
464
|
useDropdownMenuContext2 as useDropdownMenuContext,
|
|
463
465
|
useLocation2 as useLocation,
|
|
464
466
|
useNavigate2 as useNavigate,
|
package/dist/router/index.js
CHANGED
package/dist/router/router.d.ts
CHANGED
package/dist/ui/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { Dialog, DialogClose, DialogContent, type DialogContentProps, DialogDesc
|
|
|
6
6
|
export { DropdownMenu, DropdownMenuContent, DropdownMenuItem, type DropdownMenuItemProps, DropdownMenuPortal, type DropdownMenuProps, DropdownMenuSeparator, DropdownMenuTrigger, useDropdownMenuContext } from "./dropdown-menu";
|
|
7
7
|
export { createIcon, type IconNode, type IconProps } from "./icon";
|
|
8
8
|
export { type BaseLayoutProps, Center, type CenterProps, Grid, type GridProps, Row, type RowProps, Spacer, type SpacerProps, Stack, type StackProps } from "./layout";
|
|
9
|
+
export { type UseDismissOptions, useDismiss } from "./overlay";
|
|
9
10
|
export { Popover, PopoverClose, PopoverContent, type PopoverContentProps, PopoverPortal, type PopoverProps, PopoverTrigger, usePopoverContext } from "./popover";
|
|
10
11
|
export { Switch, type SwitchProps, SwitchThumb, type SwitchThumbProps } from "./switch";
|
|
11
12
|
export { type CreateTabsOptions, createTabs, Tabs, TabsContent, type TabsContentProps, TabsList, type TabsProps, TabsTrigger, useTabsContext } from "./tabs";
|
package/dist/ui/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
BottomBarRoot2,
|
|
13
13
|
BottomBarSeparator2,
|
|
14
14
|
BottomBar2,
|
|
15
|
+
useDismiss2,
|
|
15
16
|
CommandDialog2,
|
|
16
17
|
useCommandContext2,
|
|
17
18
|
CommandInput2,
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
ToastDescription2,
|
|
55
56
|
ToastClose2,
|
|
56
57
|
Toast2
|
|
57
|
-
} from "../index-
|
|
58
|
+
} from "../index-vbsc8z8h.js";
|
|
58
59
|
import {
|
|
59
60
|
createId2,
|
|
60
61
|
createControllableSignal2,
|
|
@@ -73,7 +74,7 @@ import {
|
|
|
73
74
|
Tooltip3,
|
|
74
75
|
SwitchThumb2,
|
|
75
76
|
Switch2
|
|
76
|
-
} from "../index-
|
|
77
|
+
} from "../index-kykzcs7p.js";
|
|
77
78
|
import {
|
|
78
79
|
Stack2,
|
|
79
80
|
Row2,
|
|
@@ -158,6 +159,7 @@ export {
|
|
|
158
159
|
useBottomBarContext2 as useBottomBarContext,
|
|
159
160
|
useCommandContext2 as useCommandContext,
|
|
160
161
|
useDialogContext2 as useDialogContext,
|
|
162
|
+
useDismiss2 as useDismiss,
|
|
161
163
|
useDropdownMenuContext2 as useDropdownMenuContext,
|
|
162
164
|
usePopoverContext2 as usePopoverContext,
|
|
163
165
|
useTabsContext2 as useTabsContext,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface UseDismissOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Whether dismiss listeners are active.
|
|
4
|
+
* Defaults to true.
|
|
5
|
+
*/
|
|
6
|
+
enabled?: boolean | (() => boolean);
|
|
7
|
+
/**
|
|
8
|
+
* Callback fired when dismissal is requested (Escape, click outside, or close button).
|
|
9
|
+
*/
|
|
10
|
+
onDismiss: () => void;
|
|
11
|
+
/**
|
|
12
|
+
* Close on 'Escape' key press.
|
|
13
|
+
* Defaults to true.
|
|
14
|
+
*/
|
|
15
|
+
escapeKey?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Close on pointer interaction outside the target element(s).
|
|
18
|
+
* Defaults to true.
|
|
19
|
+
*/
|
|
20
|
+
outsidePointer?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Additional element(s) to exclude from outside click detection (e.g. trigger button).
|
|
23
|
+
*/
|
|
24
|
+
outsideIgnore?: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined | (() => Array<HTMLElement | null | undefined>);
|
|
25
|
+
/**
|
|
26
|
+
* Trap keyboard focus inside the container when Tab / Shift+Tab is pressed.
|
|
27
|
+
* Defaults to false.
|
|
28
|
+
*/
|
|
29
|
+
trapFocus?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Close selector for click events bubbling inside the container (e.g. '[data-dialog-close]').
|
|
32
|
+
*/
|
|
33
|
+
closeSelector?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Callback fired when an outside pointer interaction occurs.
|
|
36
|
+
*/
|
|
37
|
+
onOutsidePointer?: (event: MouseEvent | TouchEvent) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Callback fired when an Escape keydown occurs.
|
|
40
|
+
*/
|
|
41
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Headless hook to manage overlay dismissal:
|
|
45
|
+
* - Outside pointer dismissal (click outside)
|
|
46
|
+
* - Escape key dismissal
|
|
47
|
+
* - Keyboard focus trapping (Tab cycle)
|
|
48
|
+
* - Close button selector dismissal delegation
|
|
49
|
+
*/
|
|
50
|
+
export declare function useDismiss(element: () => HTMLElement | null | undefined, options: UseDismissOptions): {
|
|
51
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
52
|
+
handleClick: (e: MouseEvent) => void;
|
|
53
|
+
};
|
package/dist/ui/popover.d.ts
CHANGED
package/dist/ui/utils.d.ts
CHANGED
|
@@ -18,4 +18,4 @@ export declare function trapFocus(container: HTMLElement, event: KeyboardEvent):
|
|
|
18
18
|
/**
|
|
19
19
|
* Listen for click outside a target element
|
|
20
20
|
*/
|
|
21
|
-
export declare function onClickOutside(elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined, handler: (event: MouseEvent | TouchEvent) => void): () => void;
|
|
21
|
+
export declare function onClickOutside(elements: Array<HTMLElement | null | undefined> | HTMLElement | null | undefined | (() => Array<HTMLElement | null | undefined>), handler: (event: MouseEvent | TouchEvent) => void): () => void;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -405,11 +405,13 @@ export {
|
|
|
405
405
|
TooltipTrigger,
|
|
406
406
|
toast,
|
|
407
407
|
trapFocus,
|
|
408
|
+
type UseDismissOptions,
|
|
408
409
|
useAccordionContext,
|
|
409
410
|
useAccordionItemContext,
|
|
410
411
|
useBottomBarContext,
|
|
411
412
|
useCommandContext,
|
|
412
413
|
useDialogContext,
|
|
414
|
+
useDismiss,
|
|
413
415
|
useDropdownMenuContext,
|
|
414
416
|
usePopoverContext,
|
|
415
417
|
useTabsContext,
|
package/src/router/router.tsx
CHANGED
|
@@ -253,6 +253,7 @@ export function Routes(props: RoutesProps): Child {
|
|
|
253
253
|
export interface LinkProps {
|
|
254
254
|
href: string;
|
|
255
255
|
class?: string | (() => string);
|
|
256
|
+
className?: string | (() => string);
|
|
256
257
|
activeClass?: string;
|
|
257
258
|
replace?: boolean;
|
|
258
259
|
children: Child;
|
|
@@ -264,7 +265,16 @@ export interface LinkProps {
|
|
|
264
265
|
*/
|
|
265
266
|
export function Link(props: LinkProps): Child {
|
|
266
267
|
const router = useContext(RouterContext);
|
|
267
|
-
const {
|
|
268
|
+
const {
|
|
269
|
+
href,
|
|
270
|
+
class: classProp,
|
|
271
|
+
className: classNameProp,
|
|
272
|
+
activeClass = "active",
|
|
273
|
+
replace,
|
|
274
|
+
children,
|
|
275
|
+
...rest
|
|
276
|
+
} = props;
|
|
277
|
+
const rawClass = classProp ?? classNameProp;
|
|
268
278
|
|
|
269
279
|
const handleClick = (e: MouseEvent) => {
|
|
270
280
|
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) {
|
|
@@ -286,7 +296,7 @@ export function Link(props: LinkProps): Child {
|
|
|
286
296
|
});
|
|
287
297
|
|
|
288
298
|
const computedClass = computed(() => {
|
|
289
|
-
const baseCls = typeof
|
|
299
|
+
const baseCls = typeof rawClass === "function" ? rawClass() : (rawClass ?? "");
|
|
290
300
|
if (activeClass && isActive()) {
|
|
291
301
|
return baseCls ? `${baseCls} ${activeClass}` : activeClass;
|
|
292
302
|
}
|
|
@@ -2,6 +2,7 @@ import { Portal } from "../dom/control-flow";
|
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
3
|
import type { CommandProps } from "./command";
|
|
4
4
|
import { CommandRoot } from "./command";
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
5
6
|
import { createControllableSignal } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface CommandDialogProps extends CommandProps {
|
|
@@ -19,6 +20,15 @@ export function CommandDialog(props: CommandDialogProps): Child {
|
|
|
19
20
|
onChange: props.onOpenChange
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
const contentRef: { current: HTMLElement | null } = { current: null };
|
|
24
|
+
|
|
25
|
+
const dismiss = useDismiss(() => contentRef.current, {
|
|
26
|
+
enabled: () => open(),
|
|
27
|
+
onDismiss: () => setOpen(false),
|
|
28
|
+
escapeKey: true,
|
|
29
|
+
outsidePointer: false
|
|
30
|
+
});
|
|
31
|
+
|
|
22
32
|
const { overlayClass, contentClass, style, ...rest } = props;
|
|
23
33
|
|
|
24
34
|
return (
|
|
@@ -39,8 +49,15 @@ export function CommandDialog(props: CommandDialogProps): Child {
|
|
|
39
49
|
onClick={(e: MouseEvent) => {
|
|
40
50
|
if (e.target === e.currentTarget) setOpen(false);
|
|
41
51
|
}}
|
|
52
|
+
onKeyDown={dismiss.handleKeyDown}
|
|
42
53
|
>
|
|
43
|
-
<div
|
|
54
|
+
<div
|
|
55
|
+
ref={(el: HTMLElement | null) => {
|
|
56
|
+
contentRef.current = el;
|
|
57
|
+
}}
|
|
58
|
+
style={{ width: "100%", maxWidth: "560px" }}
|
|
59
|
+
class={contentClass}
|
|
60
|
+
>
|
|
44
61
|
<CommandRoot {...rest} />
|
|
45
62
|
</div>
|
|
46
63
|
</div>
|
package/src/ui/dialog.tsx
CHANGED
|
@@ -2,7 +2,8 @@ import { Portal } from "../dom/control-flow";
|
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
3
|
import type { ReadonlySignal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
|
-
import {
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
6
|
+
import { createControllableSignal, createId } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface DialogContextValue {
|
|
8
9
|
open: ReadonlySignal<boolean>;
|
|
@@ -207,25 +208,26 @@ export interface DialogContentProps {
|
|
|
207
208
|
export function DialogContent(props: DialogContentProps): Child {
|
|
208
209
|
const ctx = useContext(DialogContext);
|
|
209
210
|
|
|
211
|
+
const dismiss = useDismiss(() => ctx?.contentRef.current, {
|
|
212
|
+
enabled: () => ctx?.open() ?? false,
|
|
213
|
+
onDismiss: () => ctx?.setOpen(false),
|
|
214
|
+
escapeKey: true,
|
|
215
|
+
trapFocus: true,
|
|
216
|
+
outsidePointer: false, // backdrop clicks are handled by DialogOverlay
|
|
217
|
+
closeSelector: "[data-dialog-close]",
|
|
218
|
+
onEscapeKeyDown: (e) => {
|
|
219
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
210
223
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
211
224
|
if (props.onKeyDown) props.onKeyDown(e);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (e.key === "Escape") {
|
|
215
|
-
ctx?.setOpen(false);
|
|
216
|
-
e.stopPropagation();
|
|
217
|
-
} else if (e.key === "Tab" && ctx?.contentRef.current) {
|
|
218
|
-
trapFocus(ctx.contentRef.current, e);
|
|
219
|
-
}
|
|
225
|
+
dismiss.handleKeyDown(e);
|
|
220
226
|
};
|
|
221
227
|
|
|
222
228
|
const handleClick = (e: MouseEvent) => {
|
|
223
229
|
if (props.onClick) props.onClick(e);
|
|
224
|
-
|
|
225
|
-
const target = e.target as HTMLElement | null;
|
|
226
|
-
if (target?.closest("[data-dialog-close]")) {
|
|
227
|
-
ctx?.setOpen(false);
|
|
228
|
-
}
|
|
230
|
+
dismiss.handleClick(e);
|
|
229
231
|
};
|
|
230
232
|
|
|
231
233
|
const {
|
package/src/ui/dropdown-menu.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Portal } from "../dom/control-flow";
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
|
-
import {
|
|
3
|
+
import type { ReadonlySignal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
|
-
import {
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
6
|
+
import { createControllableSignal, createId } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface DropdownMenuContextValue {
|
|
8
9
|
open: ReadonlySignal<boolean>;
|
|
@@ -207,6 +208,19 @@ export interface DropdownMenuContentProps {
|
|
|
207
208
|
export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
|
|
208
209
|
const ctx = useContext(DropdownMenuContext);
|
|
209
210
|
|
|
211
|
+
const dismiss = useDismiss(() => ctx?.contentRef.current, {
|
|
212
|
+
enabled: () => ctx?.open() ?? false,
|
|
213
|
+
onDismiss: () => {
|
|
214
|
+
if (ctx) {
|
|
215
|
+
ctx.setOpen(false);
|
|
216
|
+
ctx.triggerRef.current?.focus();
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
escapeKey: true,
|
|
220
|
+
outsidePointer: true,
|
|
221
|
+
outsideIgnore: () => [ctx?.triggerRef.current]
|
|
222
|
+
});
|
|
223
|
+
|
|
210
224
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
211
225
|
if (props.onKeyDown) props.onKeyDown(e);
|
|
212
226
|
if (e.defaultPrevented) return;
|
|
@@ -233,10 +247,7 @@ export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
|
|
|
233
247
|
items[items.length - 1]?.focus();
|
|
234
248
|
e.preventDefault();
|
|
235
249
|
} else if (e.key === "Escape") {
|
|
236
|
-
|
|
237
|
-
ctx.setOpen(false);
|
|
238
|
-
ctx.triggerRef.current?.focus();
|
|
239
|
-
}
|
|
250
|
+
dismiss.handleKeyDown(e);
|
|
240
251
|
e.preventDefault();
|
|
241
252
|
} else if (e.key === "Tab") {
|
|
242
253
|
if (ctx) ctx.setOpen(false);
|
|
@@ -259,14 +270,6 @@ export function DropdownMenuContent(props: DropdownMenuContentProps): Child {
|
|
|
259
270
|
ref={(el: HTMLElement | null) => {
|
|
260
271
|
if (ctx) {
|
|
261
272
|
ctx.contentRef.current = el;
|
|
262
|
-
if (el) {
|
|
263
|
-
const cleanup = onClickOutside([ctx.triggerRef.current, el], () => {
|
|
264
|
-
if (ctx.open()) {
|
|
265
|
-
ctx.setOpen(false);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
onCleanup(cleanup);
|
|
269
|
-
}
|
|
270
273
|
}
|
|
271
274
|
}}
|
|
272
275
|
id={ctx?.menuId}
|
package/src/ui/index.ts
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { onCleanup } from "../signals";
|
|
2
|
+
import { onClickOutside, trapFocus } from "./utils";
|
|
3
|
+
|
|
4
|
+
export interface UseDismissOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether dismiss listeners are active.
|
|
7
|
+
* Defaults to true.
|
|
8
|
+
*/
|
|
9
|
+
enabled?: boolean | (() => boolean);
|
|
10
|
+
/**
|
|
11
|
+
* Callback fired when dismissal is requested (Escape, click outside, or close button).
|
|
12
|
+
*/
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Close on 'Escape' key press.
|
|
16
|
+
* Defaults to true.
|
|
17
|
+
*/
|
|
18
|
+
escapeKey?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Close on pointer interaction outside the target element(s).
|
|
21
|
+
* Defaults to true.
|
|
22
|
+
*/
|
|
23
|
+
outsidePointer?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Additional element(s) to exclude from outside click detection (e.g. trigger button).
|
|
26
|
+
*/
|
|
27
|
+
outsideIgnore?:
|
|
28
|
+
| Array<HTMLElement | null | undefined>
|
|
29
|
+
| HTMLElement
|
|
30
|
+
| null
|
|
31
|
+
| undefined
|
|
32
|
+
| (() => Array<HTMLElement | null | undefined>);
|
|
33
|
+
/**
|
|
34
|
+
* Trap keyboard focus inside the container when Tab / Shift+Tab is pressed.
|
|
35
|
+
* Defaults to false.
|
|
36
|
+
*/
|
|
37
|
+
trapFocus?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Close selector for click events bubbling inside the container (e.g. '[data-dialog-close]').
|
|
40
|
+
*/
|
|
41
|
+
closeSelector?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Callback fired when an outside pointer interaction occurs.
|
|
44
|
+
*/
|
|
45
|
+
onOutsidePointer?: (event: MouseEvent | TouchEvent) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Callback fired when an Escape keydown occurs.
|
|
48
|
+
*/
|
|
49
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Headless hook to manage overlay dismissal:
|
|
54
|
+
* - Outside pointer dismissal (click outside)
|
|
55
|
+
* - Escape key dismissal
|
|
56
|
+
* - Keyboard focus trapping (Tab cycle)
|
|
57
|
+
* - Close button selector dismissal delegation
|
|
58
|
+
*/
|
|
59
|
+
export function useDismiss(
|
|
60
|
+
element: () => HTMLElement | null | undefined,
|
|
61
|
+
options: UseDismissOptions
|
|
62
|
+
): {
|
|
63
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
64
|
+
handleClick: (e: MouseEvent) => void;
|
|
65
|
+
} {
|
|
66
|
+
const isEnabled = () => {
|
|
67
|
+
if (typeof options.enabled === "function") return options.enabled();
|
|
68
|
+
return options.enabled ?? true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
72
|
+
if (!isEnabled() || e.defaultPrevented) return;
|
|
73
|
+
|
|
74
|
+
if (e.key === "Escape" && (options.escapeKey ?? true)) {
|
|
75
|
+
if (options.onEscapeKeyDown) {
|
|
76
|
+
options.onEscapeKeyDown(e);
|
|
77
|
+
}
|
|
78
|
+
if (!e.defaultPrevented) {
|
|
79
|
+
options.onDismiss();
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
}
|
|
82
|
+
} else if (e.key === "Tab" && options.trapFocus) {
|
|
83
|
+
const el = element();
|
|
84
|
+
if (el) {
|
|
85
|
+
trapFocus(el, e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleClick = (e: MouseEvent) => {
|
|
91
|
+
if (!isEnabled() || e.defaultPrevented) return;
|
|
92
|
+
if (options.closeSelector) {
|
|
93
|
+
const target = e.target as HTMLElement | null;
|
|
94
|
+
if (target?.closest(options.closeSelector)) {
|
|
95
|
+
options.onDismiss();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Setup outside pointer listener
|
|
101
|
+
if (typeof document !== "undefined") {
|
|
102
|
+
const unbind = onClickOutside(
|
|
103
|
+
() => {
|
|
104
|
+
const el = element();
|
|
105
|
+
const rawIgnores =
|
|
106
|
+
typeof options.outsideIgnore === "function"
|
|
107
|
+
? options.outsideIgnore()
|
|
108
|
+
: options.outsideIgnore;
|
|
109
|
+
const ignores = Array.isArray(rawIgnores) ? rawIgnores : rawIgnores ? [rawIgnores] : [];
|
|
110
|
+
return [el, ...ignores];
|
|
111
|
+
},
|
|
112
|
+
(event) => {
|
|
113
|
+
if (!isEnabled()) return;
|
|
114
|
+
if (options.outsidePointer === false) return;
|
|
115
|
+
if (options.onOutsidePointer) {
|
|
116
|
+
options.onOutsidePointer(event);
|
|
117
|
+
}
|
|
118
|
+
if (!event.defaultPrevented) {
|
|
119
|
+
options.onDismiss();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
onCleanup(unbind);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
handleKeyDown,
|
|
128
|
+
handleClick
|
|
129
|
+
};
|
|
130
|
+
}
|
package/src/ui/popover.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Portal } from "../dom/control-flow";
|
|
2
2
|
import type { Child } from "../dom/render";
|
|
3
|
-
import {
|
|
3
|
+
import type { ReadonlySignal } from "../signals";
|
|
4
4
|
import { createContext, useContext } from "../signals/context";
|
|
5
|
-
import {
|
|
5
|
+
import { useDismiss } from "./overlay";
|
|
6
|
+
import { createControllableSignal, createId } from "./utils";
|
|
6
7
|
|
|
7
8
|
export interface PopoverContextValue {
|
|
8
9
|
open: ReadonlySignal<boolean>;
|
|
@@ -138,24 +139,29 @@ export interface PopoverContentProps {
|
|
|
138
139
|
export function PopoverContent(props: PopoverContentProps): Child {
|
|
139
140
|
const ctx = useContext(PopoverContext);
|
|
140
141
|
|
|
142
|
+
const dismiss = useDismiss(() => ctx?.contentRef.current, {
|
|
143
|
+
enabled: () => ctx?.open() ?? false,
|
|
144
|
+
onDismiss: () => ctx?.setOpen(false),
|
|
145
|
+
escapeKey: true,
|
|
146
|
+
outsidePointer: true,
|
|
147
|
+
outsideIgnore: () => [ctx?.triggerRef.current],
|
|
148
|
+
closeSelector: "[data-popover-close]",
|
|
149
|
+
onEscapeKeyDown: (e) => {
|
|
150
|
+
if (props.onKeyDown) props.onKeyDown(e);
|
|
151
|
+
if (!e.defaultPrevented) {
|
|
152
|
+
ctx?.triggerRef.current?.focus();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
141
157
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
142
158
|
if (props.onKeyDown) props.onKeyDown(e);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (e.key === "Escape" && ctx) {
|
|
146
|
-
ctx.setOpen(false);
|
|
147
|
-
ctx.triggerRef.current?.focus();
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
}
|
|
159
|
+
dismiss.handleKeyDown(e);
|
|
150
160
|
};
|
|
151
161
|
|
|
152
162
|
const handleClick = (e: MouseEvent) => {
|
|
153
163
|
if (props.onClick) props.onClick(e);
|
|
154
|
-
|
|
155
|
-
const target = e.target as HTMLElement | null;
|
|
156
|
-
if (target?.closest("[data-popover-close]")) {
|
|
157
|
-
ctx?.setOpen(false);
|
|
158
|
-
}
|
|
164
|
+
dismiss.handleClick(e);
|
|
159
165
|
};
|
|
160
166
|
|
|
161
167
|
const {
|
|
@@ -175,14 +181,6 @@ export function PopoverContent(props: PopoverContentProps): Child {
|
|
|
175
181
|
ref={(el: HTMLElement | null) => {
|
|
176
182
|
if (ctx) {
|
|
177
183
|
ctx.contentRef.current = el;
|
|
178
|
-
if (el) {
|
|
179
|
-
const cleanup = onClickOutside([ctx.triggerRef.current, el], () => {
|
|
180
|
-
if (ctx.open()) {
|
|
181
|
-
ctx.setOpen(false);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
onCleanup(cleanup);
|
|
185
|
-
}
|
|
186
184
|
}
|
|
187
185
|
}}
|
|
188
186
|
id={ctx?.contentId}
|
package/src/ui/utils.ts
CHANGED
|
@@ -79,14 +79,20 @@ export function trapFocus(container: HTMLElement, event: KeyboardEvent) {
|
|
|
79
79
|
* Listen for click outside a target element
|
|
80
80
|
*/
|
|
81
81
|
export function onClickOutside(
|
|
82
|
-
elements:
|
|
82
|
+
elements:
|
|
83
|
+
| Array<HTMLElement | null | undefined>
|
|
84
|
+
| HTMLElement
|
|
85
|
+
| null
|
|
86
|
+
| undefined
|
|
87
|
+
| (() => Array<HTMLElement | null | undefined>),
|
|
83
88
|
handler: (event: MouseEvent | TouchEvent) => void
|
|
84
89
|
): () => void {
|
|
85
90
|
const listener = (event: MouseEvent | TouchEvent) => {
|
|
86
91
|
const target = event.target as Node | null;
|
|
87
92
|
if (!target) return;
|
|
88
93
|
|
|
89
|
-
const
|
|
94
|
+
const raw = typeof elements === "function" ? elements() : elements;
|
|
95
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
90
96
|
const isInside = list.some((el) => el?.contains(target));
|
|
91
97
|
|
|
92
98
|
if (!isInside) {
|