@stevederico/skateboard-ui 4.0.0 → 4.3.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/CHANGELOG.md +36 -0
- package/dist/components/core/Command.js +33 -7
- package/dist/components/core/Context.js +6 -6
- package/dist/components/core/Utilities.js +18 -14
- package/dist/ui/alert-dialog.js +24 -16
- package/dist/ui/checkbox.d.ts +9 -6
- package/dist/ui/checkbox.js +21 -9
- package/dist/ui/context-menu.js +57 -10
- package/dist/ui/dialog.js +18 -17
- package/dist/ui/drawer.js +40 -19
- package/dist/ui/dropdown-menu.js +68 -14
- package/dist/ui/menubar.js +48 -14
- package/dist/ui/navigation-menu.d.ts +2 -2
- package/dist/ui/navigation-menu.js +80 -11
- package/dist/ui/popover.d.ts +1 -1
- package/dist/ui/popover.js +83 -6
- package/dist/ui/radio-group.d.ts +1 -1
- package/dist/ui/radio-group.js +17 -5
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.js +37 -6
- package/dist/ui/sheet.js +29 -18
- package/dist/ui/slider.d.ts +2 -1
- package/dist/ui/slider.js +11 -6
- package/dist/ui/tabs.d.ts +1 -1
- package/dist/ui/tabs.js +16 -2
- package/dist/ui/tooltip.js +27 -1
- package/dist/ui/use-floating.d.ts +4 -0
- package/dist/ui/use-floating.js +10 -2
- package/dist/ui/use-labelling.d.ts +21 -0
- package/dist/ui/use-labelling.js +43 -0
- package/dist/ui/use-pointer-moved.d.ts +12 -0
- package/dist/ui/use-pointer-moved.js +21 -0
- package/dist/ui/use-scroll-lock.d.ts +6 -0
- package/dist/ui/use-scroll-lock.js +29 -0
- package/dist/ui/use-typeahead.d.ts +19 -0
- package/dist/ui/use-typeahead.js +64 -0
- package/package.json +1 -1
|
@@ -63,15 +63,56 @@ function NavigationMenuList({ className, ...props }) {
|
|
|
63
63
|
}
|
|
64
64
|
function NavigationMenuItem({ className, ...props }) {
|
|
65
65
|
const value = React.useId();
|
|
66
|
+
// One id + content ref per item so multiple triggers/panels don't collide
|
|
67
|
+
// when wiring aria-controls and focusing into the right portaled panel.
|
|
68
|
+
const contentId = React.useId();
|
|
66
69
|
const triggerRef = React.useRef(null);
|
|
67
|
-
|
|
70
|
+
const contentRef = React.useRef(null);
|
|
71
|
+
return (_jsx(NavigationMenuItemContext.Provider, { value: { value, triggerRef, contentId, contentRef }, children: _jsx("li", { "data-slot": "navigation-menu-item", className: cn("relative", className), ...props }) }));
|
|
68
72
|
}
|
|
73
|
+
// Tabbable descendants, in DOM order, used to move focus into the open content
|
|
74
|
+
// panel. Kept local (not imported) so this file stays self-contained.
|
|
75
|
+
const FOCUSABLE_SELECTOR = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
69
76
|
const navigationMenuTriggerStyle = cva("cursor-pointer group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-open:bg-muted/50 data-open:hover:bg-muted data-open:focus:bg-muted");
|
|
70
|
-
function NavigationMenuTrigger({ className, children, onMouseEnter, onFocus, onClick, ...props }) {
|
|
77
|
+
function NavigationMenuTrigger({ className, children, onMouseEnter, onFocus, onClick, onKeyDown, ...props }) {
|
|
71
78
|
const { value, setValue, open } = useNavigationMenu();
|
|
72
|
-
const { value: itemValue, triggerRef } = useNavigationMenuItem();
|
|
79
|
+
const { value: itemValue, triggerRef, contentId, contentRef } = useNavigationMenuItem();
|
|
73
80
|
const isOpen = value === itemValue;
|
|
74
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Move keyboard focus into the open content panel so its links are
|
|
83
|
+
* reachable. The panel is portaled and opens on focus/hover, but without
|
|
84
|
+
* this a keyboard user can never enter it (WCAG 2.1.1 keyboard trap-out).
|
|
85
|
+
* ArrowDown (and Enter) open if needed, then focus the first focusable child.
|
|
86
|
+
*/
|
|
87
|
+
const handleKeyDown = (e) => {
|
|
88
|
+
onKeyDown?.(e);
|
|
89
|
+
if (e.defaultPrevented)
|
|
90
|
+
return;
|
|
91
|
+
if (e.key !== "ArrowDown" && e.key !== "Enter")
|
|
92
|
+
return;
|
|
93
|
+
// Enter still toggles via onClick; only intercept it to enter an open panel.
|
|
94
|
+
if (e.key === "Enter" && !isOpen)
|
|
95
|
+
return;
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
open(itemValue);
|
|
98
|
+
// The panel mounts this tick and is positioned a frame or two later (it
|
|
99
|
+
// starts visibility:hidden until useFloating measures it, and focus() on a
|
|
100
|
+
// hidden node is a no-op). Retry across a few frames until it is visible,
|
|
101
|
+
// bounded so a panel that never opens can't spin forever.
|
|
102
|
+
let frames = 0;
|
|
103
|
+
const focusFirst = () => {
|
|
104
|
+
const node = contentRef.current;
|
|
105
|
+
if (!node || getComputedStyle(node).visibility === "hidden") {
|
|
106
|
+
if (frames++ < 20)
|
|
107
|
+
requestAnimationFrame(focusFirst);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const first = node.querySelector(FOCUSABLE_SELECTOR);
|
|
111
|
+
(first ?? node).focus();
|
|
112
|
+
};
|
|
113
|
+
requestAnimationFrame(focusFirst);
|
|
114
|
+
};
|
|
115
|
+
return (_jsxs("button", { ref: triggerRef, type: "button", "data-slot": "navigation-menu-trigger", "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": isOpen ? contentId : undefined, "data-state": isOpen ? "open" : "closed", "data-popup-open": isOpen ? "" : undefined, "data-open": isOpen ? "" : undefined, className: cn(navigationMenuTriggerStyle(), "group", className), onMouseEnter: (e) => {
|
|
75
116
|
onMouseEnter?.(e);
|
|
76
117
|
if (!e.defaultPrevented)
|
|
77
118
|
open(itemValue);
|
|
@@ -83,11 +124,11 @@ function NavigationMenuTrigger({ className, children, onMouseEnter, onFocus, onC
|
|
|
83
124
|
onClick?.(e);
|
|
84
125
|
if (!e.defaultPrevented)
|
|
85
126
|
setValue(isOpen ? null : itemValue);
|
|
86
|
-
}, ...props, children: [children, " ", _jsx(ChevronDownIcon, { className: "relative top-px ml-1 size-3 transition duration-300 group-data-open:rotate-180", "aria-hidden": "true" })] }));
|
|
127
|
+
}, onKeyDown: handleKeyDown, ...props, children: [children, " ", _jsx(ChevronDownIcon, { className: "relative top-px ml-1 size-3 transition duration-300 group-data-open:rotate-180", "aria-hidden": "true" })] }));
|
|
87
128
|
}
|
|
88
|
-
function NavigationMenuContent({ className, children, onMouseEnter, ...props }) {
|
|
89
|
-
const { value, cancelClose, scheduleClose } = useNavigationMenu();
|
|
90
|
-
const { value: itemValue, triggerRef } = useNavigationMenuItem();
|
|
129
|
+
function NavigationMenuContent({ className, children, onMouseEnter, onKeyDown, ...props }) {
|
|
130
|
+
const { value, setValue, cancelClose, scheduleClose } = useNavigationMenu();
|
|
131
|
+
const { value: itemValue, triggerRef, contentId, contentRef } = useNavigationMenuItem();
|
|
91
132
|
const isOpen = value === itemValue;
|
|
92
133
|
const [mounted, presenceRef] = usePresence(isOpen);
|
|
93
134
|
const { floatingRef, pos } = useFloating(triggerRef, mounted, {
|
|
@@ -99,16 +140,44 @@ function NavigationMenuContent({ className, children, onMouseEnter, ...props })
|
|
|
99
140
|
const setRef = React.useCallback((node) => {
|
|
100
141
|
floatingRef.current = node;
|
|
101
142
|
presenceRef.current = node;
|
|
102
|
-
|
|
143
|
+
contentRef.current = node;
|
|
144
|
+
}, [floatingRef, presenceRef, contentRef]);
|
|
145
|
+
/**
|
|
146
|
+
* Restore focus to the trigger whenever the panel closes while focus is still
|
|
147
|
+
* inside it (root-level Escape, mouse-leave timeout, etc.). Without this the
|
|
148
|
+
* panel unmounts and keyboard focus is stranded on a detached node (a11y).
|
|
149
|
+
* If focus already moved elsewhere (e.g. an outside click), leave it alone.
|
|
150
|
+
*/
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
if (isOpen)
|
|
153
|
+
return;
|
|
154
|
+
const node = contentRef.current;
|
|
155
|
+
const trigger = triggerRef.current;
|
|
156
|
+
if (trigger && node && node.contains(document.activeElement))
|
|
157
|
+
trigger.focus();
|
|
158
|
+
}, [isOpen, contentRef, triggerRef]);
|
|
159
|
+
/**
|
|
160
|
+
* Escape closes the menu and returns focus to the trigger. The root already
|
|
161
|
+
* listens for Escape to clear the value, but only this panel knows to restore
|
|
162
|
+
* focus — otherwise keyboard focus would be stranded on a now-unmounted node.
|
|
163
|
+
*/
|
|
164
|
+
const handleKeyDown = (e) => {
|
|
165
|
+
onKeyDown?.(e);
|
|
166
|
+
if (e.defaultPrevented || e.key !== "Escape")
|
|
167
|
+
return;
|
|
168
|
+
e.preventDefault();
|
|
169
|
+
setValue(null);
|
|
170
|
+
triggerRef.current?.focus();
|
|
171
|
+
};
|
|
103
172
|
if (!mounted)
|
|
104
173
|
return null;
|
|
105
|
-
return (_jsx(Portal, { children: _jsx("div", { ref: setRef, "data-slot": "navigation-menu-content", "data-state": isOpen ? "open" : "closed", "data-open": isOpen ? "" : undefined, "data-closed": isOpen ? undefined : "", "data-side": pos?.side ?? "bottom", style: {
|
|
174
|
+
return (_jsx(Portal, { children: _jsx("div", { ref: setRef, id: contentId, role: "menu", tabIndex: -1, "data-slot": "navigation-menu-content", "data-state": isOpen ? "open" : "closed", "data-open": isOpen ? "" : undefined, "data-closed": isOpen ? undefined : "", "data-side": pos?.side ?? "bottom", style: {
|
|
106
175
|
position: "fixed",
|
|
107
176
|
left: pos?.x ?? 0,
|
|
108
177
|
top: pos?.y ?? 0,
|
|
109
178
|
visibility: pos ? "visible" : "hidden",
|
|
110
179
|
["--transform-origin"]: pos?.transformOrigin ?? "center",
|
|
111
|
-
}, onMouseEnter: (e) => {
|
|
180
|
+
}, onKeyDown: handleKeyDown, onMouseEnter: (e) => {
|
|
112
181
|
onMouseEnter?.(e);
|
|
113
182
|
cancelClose();
|
|
114
183
|
}, onMouseLeave: scheduleClose, className: cn("z-50 w-auto origin-(--transform-origin) rounded-md bg-popover p-2 text-popover-foreground shadow ring-1 ring-foreground/10 duration-200 outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", className), ...props, children: children }) }));
|
package/dist/ui/popover.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface PopoverContentProps extends React.ComponentProps<"div"> {
|
|
|
18
18
|
sideOffset?: number;
|
|
19
19
|
alignOffset?: number;
|
|
20
20
|
}
|
|
21
|
-
declare function PopoverContent({ className, style, ref, align, alignOffset, side, sideOffset, ...props }: PopoverContentProps): React.JSX.Element | null;
|
|
21
|
+
declare function PopoverContent({ className, style, ref, align, alignOffset, side, sideOffset, onKeyDown, ...props }: PopoverContentProps): React.JSX.Element | null;
|
|
22
22
|
declare function PopoverHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
23
23
|
declare function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">): React.JSX.Element;
|
|
24
24
|
declare function PopoverDescription({ className, ...props }: React.ComponentProps<"p">): React.JSX.Element;
|
package/dist/ui/popover.js
CHANGED
|
@@ -8,6 +8,7 @@ import { useFloating } from "./use-floating.js";
|
|
|
8
8
|
import { useDismiss } from "./use-dismiss.js";
|
|
9
9
|
import { usePresence } from "./use-presence.js";
|
|
10
10
|
import { useControllableState } from "./use-controllable-state.js";
|
|
11
|
+
import { useDialogLabelling } from "./use-labelling.js";
|
|
11
12
|
const PopoverContext = React.createContext(null);
|
|
12
13
|
function usePopover() {
|
|
13
14
|
const ctx = React.useContext(PopoverContext);
|
|
@@ -23,7 +24,19 @@ function Popover({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
23
24
|
});
|
|
24
25
|
const triggerRef = React.useRef(null);
|
|
25
26
|
const contentId = React.useId();
|
|
26
|
-
|
|
27
|
+
const labelling = useDialogLabelling();
|
|
28
|
+
return (_jsx(PopoverContext.Provider, { value: {
|
|
29
|
+
open: isOpen,
|
|
30
|
+
setOpen,
|
|
31
|
+
triggerRef,
|
|
32
|
+
contentId,
|
|
33
|
+
titleId: labelling.titleId,
|
|
34
|
+
descriptionId: labelling.descriptionId,
|
|
35
|
+
registerTitle: labelling.registerTitle,
|
|
36
|
+
registerDescription: labelling.registerDescription,
|
|
37
|
+
labelledBy: labelling.labelledBy,
|
|
38
|
+
describedBy: labelling.describedBy,
|
|
39
|
+
}, children: children }));
|
|
27
40
|
}
|
|
28
41
|
function PopoverTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
29
42
|
const { open, setOpen, triggerRef, contentId } = usePopover();
|
|
@@ -35,8 +48,10 @@ function PopoverTrigger({ asChild = false, render, nativeButton: _nativeButton,
|
|
|
35
48
|
setOpen(!open);
|
|
36
49
|
}, ...props, children: slotChild }));
|
|
37
50
|
}
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
// Tabbable descendants, in DOM order, for focus-in and the Tab trap.
|
|
52
|
+
const FOCUSABLE_SELECTOR = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
53
|
+
function PopoverContent({ className, style, ref, align = "center", alignOffset = 0, side = "bottom", sideOffset = 4, onKeyDown, ...props }) {
|
|
54
|
+
const { open, setOpen, triggerRef, contentId, labelledBy, describedBy } = usePopover();
|
|
40
55
|
const [mounted, presenceRef] = usePresence(open);
|
|
41
56
|
const { floatingRef, pos } = useFloating(triggerRef, mounted, {
|
|
42
57
|
side,
|
|
@@ -44,10 +59,66 @@ function PopoverContent({ className, style, ref, align = "center", alignOffset =
|
|
|
44
59
|
sideOffset,
|
|
45
60
|
alignOffset,
|
|
46
61
|
});
|
|
62
|
+
const contentRef = React.useRef(null);
|
|
47
63
|
useDismiss(open, () => setOpen(false), [triggerRef, floatingRef]);
|
|
64
|
+
// Move focus into the popover when it opens and restore it to the trigger on
|
|
65
|
+
// close — a role="dialog" must manage focus (native <dialog> does this for
|
|
66
|
+
// free; this portaled div does not).
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (!open)
|
|
69
|
+
return;
|
|
70
|
+
const trigger = triggerRef.current;
|
|
71
|
+
let raf = 0;
|
|
72
|
+
// Retry across frames until the content is mounted AND visible: presence
|
|
73
|
+
// mounts it a tick after `open` flips, and it stays visibility:hidden until
|
|
74
|
+
// useFloating positions it — focus() on a missing or hidden node is a silent
|
|
75
|
+
// no-op. The effect cleanup cancels this when the popover closes.
|
|
76
|
+
const focusIn = () => {
|
|
77
|
+
const node = contentRef.current;
|
|
78
|
+
if (!node || getComputedStyle(node).visibility === "hidden") {
|
|
79
|
+
raf = requestAnimationFrame(focusIn);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const first = node.querySelector(FOCUSABLE_SELECTOR);
|
|
83
|
+
(first ?? node).focus();
|
|
84
|
+
};
|
|
85
|
+
raf = requestAnimationFrame(focusIn);
|
|
86
|
+
return () => {
|
|
87
|
+
cancelAnimationFrame(raf);
|
|
88
|
+
// Only reclaim focus if it is still inside the popover (an outside click
|
|
89
|
+
// already moved it elsewhere — don't yank it back).
|
|
90
|
+
const node = contentRef.current;
|
|
91
|
+
if (trigger && node && node.contains(document.activeElement))
|
|
92
|
+
trigger.focus();
|
|
93
|
+
};
|
|
94
|
+
}, [open, triggerRef]);
|
|
48
95
|
if (!mounted)
|
|
49
96
|
return null;
|
|
50
|
-
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, ref), id: contentId, role: "dialog", "data-slot": "popover-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? side,
|
|
97
|
+
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, contentRef, ref), id: contentId, role: "dialog", "aria-labelledby": labelledBy, "aria-describedby": describedBy, tabIndex: -1, "data-slot": "popover-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? side, onKeyDown: (e) => {
|
|
98
|
+
onKeyDown?.(e);
|
|
99
|
+
if (e.defaultPrevented || e.key !== "Tab")
|
|
100
|
+
return;
|
|
101
|
+
const node = contentRef.current;
|
|
102
|
+
if (!node)
|
|
103
|
+
return;
|
|
104
|
+
const focusables = Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null || el === document.activeElement);
|
|
105
|
+
if (focusables.length === 0) {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
node.focus();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const first = focusables[0];
|
|
111
|
+
const last = focusables[focusables.length - 1];
|
|
112
|
+
const activeEl = document.activeElement;
|
|
113
|
+
if (e.shiftKey && (activeEl === first || activeEl === node)) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
last.focus();
|
|
116
|
+
}
|
|
117
|
+
else if (!e.shiftKey && activeEl === last) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
first.focus();
|
|
120
|
+
}
|
|
121
|
+
}, style: {
|
|
51
122
|
...style,
|
|
52
123
|
position: "fixed",
|
|
53
124
|
left: pos?.x ?? 0,
|
|
@@ -60,9 +131,15 @@ function PopoverHeader({ className, ...props }) {
|
|
|
60
131
|
return (_jsx("div", { "data-slot": "popover-header", className: cn("flex flex-col gap-1 text-sm", className), ...props }));
|
|
61
132
|
}
|
|
62
133
|
function PopoverTitle({ className, ...props }) {
|
|
63
|
-
|
|
134
|
+
// Register this title's id so PopoverContent can point aria-labelledby at it.
|
|
135
|
+
const { titleId, registerTitle } = usePopover();
|
|
136
|
+
React.useEffect(registerTitle, [registerTitle]);
|
|
137
|
+
return (_jsx("h2", { id: titleId, "data-slot": "popover-title", className: cn("font-medium", className), ...props }));
|
|
64
138
|
}
|
|
65
139
|
function PopoverDescription({ className, ...props }) {
|
|
66
|
-
|
|
140
|
+
// Register this description's id so PopoverContent can point aria-describedby at it.
|
|
141
|
+
const { descriptionId, registerDescription } = usePopover();
|
|
142
|
+
React.useEffect(registerDescription, [registerDescription]);
|
|
143
|
+
return (_jsx("p", { id: descriptionId, "data-slot": "popover-description", className: cn("text-muted-foreground", className), ...props }));
|
|
67
144
|
}
|
|
68
145
|
export { Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription, };
|
package/dist/ui/radio-group.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ declare function RadioGroup({ className, value, defaultValue, onValueChange, nam
|
|
|
13
13
|
export interface RadioGroupItemProps extends Omit<React.ComponentProps<"button">, "value"> {
|
|
14
14
|
value: string;
|
|
15
15
|
}
|
|
16
|
-
declare function RadioGroupItem({ className, value, onClick, disabled, ...props }: RadioGroupItemProps): React.JSX.Element;
|
|
16
|
+
declare function RadioGroupItem({ className, value, onClick, disabled, ref, ...props }: RadioGroupItemProps): React.JSX.Element;
|
|
17
17
|
export { RadioGroup, RadioGroupItem };
|
package/dist/ui/radio-group.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
|
+
import { mergeRefs } from "./slot.js";
|
|
5
6
|
import { useControllableState } from "./use-controllable-state.js";
|
|
6
7
|
const RadioGroupContext = React.createContext(null);
|
|
7
8
|
function useRadioGroup() {
|
|
@@ -39,13 +40,24 @@ function RadioGroup({ className, value, defaultValue, onValueChange, name, onKey
|
|
|
39
40
|
radios[next].click();
|
|
40
41
|
}, ...props }), name ? (_jsx("input", { type: "hidden", name: name, value: selected ?? "" })) : null] }));
|
|
41
42
|
}
|
|
42
|
-
function RadioGroupItem({ className, value, onClick, disabled, ...props }) {
|
|
43
|
+
function RadioGroupItem({ className, value, onClick, disabled, ref, ...props }) {
|
|
43
44
|
const { value: selected, setValue } = useRadioGroup();
|
|
44
45
|
const checked = selected === value;
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
const btnRef = React.useRef(null);
|
|
47
|
+
// Roving tab index: the checked radio is the single tab stop. When nothing is
|
|
48
|
+
// checked, only the first enabled radio is tabbable — making every radio
|
|
49
|
+
// tabbable would put multiple stops in Tab order, breaking the radiogroup
|
|
50
|
+
// pattern.
|
|
51
|
+
const [isFirstEnabled, setIsFirstEnabled] = React.useState(false);
|
|
52
|
+
React.useLayoutEffect(() => {
|
|
53
|
+
if (selected !== undefined)
|
|
54
|
+
return;
|
|
55
|
+
const el = btnRef.current;
|
|
56
|
+
const first = el?.parentElement?.querySelector('[role="radio"]:not([disabled]):not([data-disabled])');
|
|
57
|
+
setIsFirstEnabled(first === el);
|
|
58
|
+
});
|
|
59
|
+
const tabbable = checked || (selected === undefined && isFirstEnabled);
|
|
60
|
+
return (_jsx("button", { ref: mergeRefs(btnRef, ref), type: "button", role: "radio", "aria-checked": checked, disabled: disabled, tabIndex: tabbable ? 0 : -1, "data-slot": "radio-group-item", "data-state": checked ? "checked" : "unchecked", "data-checked": checked ? "" : undefined, className: cn("cursor-pointer group/radio-group-item peer relative flex aspect-square size-4 shrink-0 items-center justify-center rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary", className), onClick: (e) => {
|
|
49
61
|
onClick?.(e);
|
|
50
62
|
if (!e.defaultPrevented)
|
|
51
63
|
setValue(value);
|
package/dist/ui/select.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ declare function SelectGroup({ className, ...props }: React.ComponentProps<"div"
|
|
|
14
14
|
declare function SelectValue({ className, placeholder, ...props }: React.ComponentProps<"span"> & {
|
|
15
15
|
placeholder?: React.ReactNode;
|
|
16
16
|
}): React.JSX.Element;
|
|
17
|
-
declare function SelectTrigger({ className, size, children, onClick, onKeyDown, ...props }: React.ComponentProps<"button"> & {
|
|
17
|
+
declare function SelectTrigger({ className, size, children, id, onClick, onKeyDown, ...props }: React.ComponentProps<"button"> & {
|
|
18
18
|
size?: "sm" | "default";
|
|
19
19
|
}): React.JSX.Element;
|
|
20
20
|
export interface SelectContentProps extends React.ComponentProps<"div"> {
|
package/dist/ui/select.js
CHANGED
|
@@ -7,6 +7,8 @@ import { Portal } from "./portal.js";
|
|
|
7
7
|
import { useFloating } from "./use-floating.js";
|
|
8
8
|
import { usePresence } from "./use-presence.js";
|
|
9
9
|
import { useControllableState } from "./use-controllable-state.js";
|
|
10
|
+
import { useTypeahead } from "./use-typeahead.js";
|
|
11
|
+
import { usePointerMoved } from "./use-pointer-moved.js";
|
|
10
12
|
import { ChevronDownIcon, CheckIcon } from "../icons/index.js";
|
|
11
13
|
const SelectContext = React.createContext(null);
|
|
12
14
|
function useSelect() {
|
|
@@ -28,6 +30,7 @@ function Select({ value, defaultValue, onValueChange, open, defaultOpen = false,
|
|
|
28
30
|
});
|
|
29
31
|
const triggerRef = React.useRef(null);
|
|
30
32
|
const contentId = React.useId();
|
|
33
|
+
const triggerId = React.useId();
|
|
31
34
|
const [labels, setLabels] = React.useState({});
|
|
32
35
|
const registerLabel = React.useCallback((v, label) => setLabels((prev) => ({ ...prev, [v]: label })), []);
|
|
33
36
|
const unregisterLabel = React.useCallback((v) => setLabels((prev) => {
|
|
@@ -46,6 +49,7 @@ function Select({ value, defaultValue, onValueChange, open, defaultOpen = false,
|
|
|
46
49
|
setValue,
|
|
47
50
|
triggerRef,
|
|
48
51
|
contentId,
|
|
52
|
+
triggerId,
|
|
49
53
|
labels,
|
|
50
54
|
registerLabel,
|
|
51
55
|
unregisterLabel,
|
|
@@ -59,9 +63,13 @@ function SelectValue({ className, placeholder, ...props }) {
|
|
|
59
63
|
const label = value !== undefined ? labels[value] : undefined;
|
|
60
64
|
return (_jsx("span", { "data-slot": "select-value", "data-placeholder": value === undefined ? "" : undefined, className: cn("flex flex-1 text-left", className), ...props, children: label ?? placeholder ?? null }));
|
|
61
65
|
}
|
|
62
|
-
function SelectTrigger({ className, size = "default", children, onClick, onKeyDown, ...props }) {
|
|
63
|
-
const { open, setOpen, value, triggerRef, contentId } = useSelect();
|
|
64
|
-
|
|
66
|
+
function SelectTrigger({ className, size = "default", children, id, onClick, onKeyDown, ...props }) {
|
|
67
|
+
const { open, setOpen, value, triggerRef, contentId, triggerId } = useSelect();
|
|
68
|
+
// Give the trigger a stable id so the listbox can point back at it via
|
|
69
|
+
// aria-labelledby — naming the popup with its combobox. Honor a consumer id
|
|
70
|
+
// when supplied so an existing label association isn't clobbered.
|
|
71
|
+
const resolvedTriggerId = id ?? triggerId;
|
|
72
|
+
return (_jsxs("button", { ref: triggerRef, type: "button", id: resolvedTriggerId, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? contentId : undefined, "data-slot": "select-trigger", "data-size": size, "data-state": open ? "open" : "closed", "data-placeholder": value === undefined ? "" : undefined, className: cn("flex w-fit cursor-pointer items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onClick: (e) => {
|
|
65
73
|
onClick?.(e);
|
|
66
74
|
if (!e.defaultPrevented)
|
|
67
75
|
setOpen(!open);
|
|
@@ -79,7 +87,7 @@ function optionsOf(container) {
|
|
|
79
87
|
return Array.from(container.querySelectorAll('[role="option"]:not([data-disabled])'));
|
|
80
88
|
}
|
|
81
89
|
function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, onKeyDown, ...props }) {
|
|
82
|
-
const { open, setOpen, value, triggerRef, contentId } = useSelect();
|
|
90
|
+
const { open, setOpen, value, triggerRef, contentId, triggerId } = useSelect();
|
|
83
91
|
const [mounted, presenceRef] = usePresence(open);
|
|
84
92
|
const { floatingRef, pos } = useFloating(triggerRef, mounted, {
|
|
85
93
|
side,
|
|
@@ -88,6 +96,8 @@ function SelectContent({ className, children, side = "bottom", sideOffset = 4, a
|
|
|
88
96
|
alignOffset,
|
|
89
97
|
});
|
|
90
98
|
const containerRef = React.useRef(null);
|
|
99
|
+
const { onTypeaheadKeyDown } = useTypeahead();
|
|
100
|
+
const hasPointerMoved = usePointerMoved();
|
|
91
101
|
// Focus the selected (or first) option once the popup is actually visible.
|
|
92
102
|
const didFocus = React.useRef(false);
|
|
93
103
|
React.useEffect(() => {
|
|
@@ -134,14 +144,24 @@ function SelectContent({ className, children, side = "bottom", sideOffset = 4, a
|
|
|
134
144
|
return (_jsx("div", { hidden: true, "aria-hidden": true, "data-slot": "select-content-registry", children: children }));
|
|
135
145
|
}
|
|
136
146
|
const triggerWidth = triggerRef.current?.offsetWidth;
|
|
137
|
-
|
|
147
|
+
// Name the listbox after its combobox trigger for screen readers. Prefer the
|
|
148
|
+
// trigger's live DOM id (which reflects any consumer-supplied id) and fall
|
|
149
|
+
// back to the context-generated triggerId.
|
|
150
|
+
const labelledById = triggerRef.current?.id || triggerId;
|
|
151
|
+
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, containerRef), id: contentId, role: "listbox", "aria-labelledby": labelledById, tabIndex: -1, "data-slot": "select-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? side, style: {
|
|
138
152
|
position: "fixed",
|
|
139
153
|
left: pos?.x ?? 0,
|
|
140
154
|
top: pos?.y ?? 0,
|
|
141
155
|
minWidth: triggerWidth,
|
|
142
156
|
visibility: pos ? "visible" : "hidden",
|
|
143
157
|
["--transform-origin"]: pos?.transformOrigin ?? "center",
|
|
158
|
+
["--available-height"]: pos ? `${pos.availableHeight}px` : undefined,
|
|
144
159
|
}, onPointerMove: (e) => {
|
|
160
|
+
// Ignore synthetic pointermoves fired by keyboard arrow scrolling
|
|
161
|
+
// (same coords) — otherwise pointer focus yanks focus off the
|
|
162
|
+
// keyboard-navigated option right after it scrolls into view.
|
|
163
|
+
if (!hasPointerMoved(e))
|
|
164
|
+
return;
|
|
145
165
|
const opt = e.target.closest('[role="option"]:not([data-disabled])');
|
|
146
166
|
if (opt && opt !== document.activeElement)
|
|
147
167
|
opt.focus();
|
|
@@ -180,6 +200,13 @@ function SelectContent({ className, children, side = "bottom", sideOffset = 4, a
|
|
|
180
200
|
active.click();
|
|
181
201
|
}
|
|
182
202
|
}
|
|
203
|
+
else {
|
|
204
|
+
const match = onTypeaheadKeyDown(e, opts, idx);
|
|
205
|
+
if (match) {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
match.focus();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
183
210
|
}, className: cn("relative isolate z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props, children: children }) }));
|
|
184
211
|
}
|
|
185
212
|
function SelectLabel({ className, ...props }) {
|
|
@@ -195,7 +222,11 @@ function SelectItem({ className, children, value, disabled, onClick, ...props })
|
|
|
195
222
|
React.useEffect(() => {
|
|
196
223
|
registerLabel(value, children);
|
|
197
224
|
}, [value, children, registerLabel]);
|
|
198
|
-
return (_jsxs("div", { role: "option", tabIndex: -1, "aria-selected": selected, "aria-disabled": disabled || undefined, "data-slot": "select-item", "data-value": value, "data-disabled": disabled ? "" : undefined, className: cn(
|
|
225
|
+
return (_jsxs("div", { role: "option", tabIndex: -1, "aria-selected": selected, "aria-disabled": disabled || undefined, "data-slot": "select-item", "data-value": value, "data-disabled": disabled ? "" : undefined, className: cn(
|
|
226
|
+
// focus-visible ring gives keyboard users a clear focus indicator;
|
|
227
|
+
// focus:bg-accent alone is a weak signal and fails when accent contrast
|
|
228
|
+
// is low, so pair it with a visible ring for arrow-key navigation.
|
|
229
|
+
"relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), onClick: (e) => {
|
|
199
230
|
if (disabled)
|
|
200
231
|
return;
|
|
201
232
|
onClick?.(e);
|
package/dist/ui/sheet.js
CHANGED
|
@@ -3,10 +3,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../shadcn/lib/utils.js";
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
|
-
import { Slot, resolveRender } from "./slot.js";
|
|
6
|
+
import { Slot, mergeRefs, resolveRender } from "./slot.js";
|
|
7
7
|
import { XIcon } from "../icons/index.js";
|
|
8
8
|
import { useControllableState } from "./use-controllable-state.js";
|
|
9
9
|
import { usePresence } from "./use-presence.js";
|
|
10
|
+
import { useScrollLock } from "./use-scroll-lock.js";
|
|
11
|
+
import { useDialogLabelling } from "./use-labelling.js";
|
|
10
12
|
const SheetContext = React.createContext(null);
|
|
11
13
|
function useSheet() {
|
|
12
14
|
const ctx = React.useContext(SheetContext);
|
|
@@ -22,9 +24,20 @@ function Sheet({ open, defaultOpen = false, onOpenChange, children }) {
|
|
|
22
24
|
defaultValue: defaultOpen,
|
|
23
25
|
onChange: onOpenChange,
|
|
24
26
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Conditional labelling: only advertise aria-labelledby/aria-describedby when a
|
|
28
|
+
// Title/Description part is actually rendered, so the dialog never points at a
|
|
29
|
+
// missing element (a dangling IDREF that screen readers announce as broken).
|
|
30
|
+
const { titleId, descriptionId, registerTitle, registerDescription, labelledBy, describedBy } = useDialogLabelling();
|
|
31
|
+
return (_jsx(SheetContext.Provider, { value: {
|
|
32
|
+
open: isOpen,
|
|
33
|
+
setOpen,
|
|
34
|
+
titleId,
|
|
35
|
+
descriptionId,
|
|
36
|
+
registerTitle,
|
|
37
|
+
registerDescription,
|
|
38
|
+
labelledBy,
|
|
39
|
+
describedBy,
|
|
40
|
+
}, children: children }));
|
|
28
41
|
}
|
|
29
42
|
function SheetTrigger({ asChild = false, render, nativeButton: _nativeButton, children, onClick, ...props }) {
|
|
30
43
|
const { open, setOpen } = useSheet();
|
|
@@ -67,10 +80,10 @@ const SIDE_OUT = {
|
|
|
67
80
|
bottom: "slide-out-to-bottom-52",
|
|
68
81
|
};
|
|
69
82
|
function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }) {
|
|
70
|
-
const { open, setOpen,
|
|
83
|
+
const { open, setOpen, labelledBy, describedBy } = useSheet();
|
|
71
84
|
const ref = React.useRef(null);
|
|
72
85
|
const pointerDownOnBackdrop = React.useRef(false);
|
|
73
|
-
const [mounted] = usePresence(open);
|
|
86
|
+
const [mounted, presenceRef] = usePresence(open);
|
|
74
87
|
React.useEffect(() => {
|
|
75
88
|
const node = ref.current;
|
|
76
89
|
if (!node)
|
|
@@ -80,18 +93,12 @@ function SheetContent({ className, children, side = "right", showCloseButton = t
|
|
|
80
93
|
else if (!mounted && node.open)
|
|
81
94
|
node.close();
|
|
82
95
|
}, [mounted]);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const prev = document.body.style.overflow;
|
|
87
|
-
document.body.style.overflow = "hidden";
|
|
88
|
-
return () => {
|
|
89
|
-
document.body.style.overflow = prev;
|
|
90
|
-
};
|
|
91
|
-
}, [mounted]);
|
|
96
|
+
// Reentrant, reference-counted scroll lock so stacked overlays don't unlock the
|
|
97
|
+
// body while another is still open (a plain per-instance effect would).
|
|
98
|
+
useScrollLock(mounted);
|
|
92
99
|
if (!mounted)
|
|
93
100
|
return null;
|
|
94
|
-
return (_jsxs("dialog", { ...props, ref: ref, "aria-labelledby":
|
|
101
|
+
return (_jsxs("dialog", { ...props, ref: mergeRefs(ref, presenceRef), "aria-labelledby": labelledBy, "aria-describedby": describedBy, "data-slot": "sheet-content", "data-side": side, "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", onCancel: (e) => {
|
|
95
102
|
e.preventDefault();
|
|
96
103
|
setOpen(false);
|
|
97
104
|
}, onClose: () => setOpen(false), onPointerDown: (e) => {
|
|
@@ -111,11 +118,15 @@ function SheetFooter({ className, ...props }) {
|
|
|
111
118
|
return (_jsx("div", { "data-slot": "sheet-footer", className: cn("mt-auto flex flex-col gap-2 p-4", className), ...props }));
|
|
112
119
|
}
|
|
113
120
|
function SheetTitle({ className, ...props }) {
|
|
114
|
-
const { titleId } = useSheet();
|
|
121
|
+
const { titleId, registerTitle } = useSheet();
|
|
122
|
+
// Register so the dialog only sets aria-labelledby while this title is mounted.
|
|
123
|
+
React.useEffect(registerTitle, [registerTitle]);
|
|
115
124
|
return (_jsx("h2", { id: titleId, "data-slot": "sheet-title", className: cn("font-medium text-foreground", className), ...props }));
|
|
116
125
|
}
|
|
117
126
|
function SheetDescription({ className, ...props }) {
|
|
118
|
-
const { descriptionId } = useSheet();
|
|
127
|
+
const { descriptionId, registerDescription } = useSheet();
|
|
128
|
+
// Register so the dialog only sets aria-describedby while this description is mounted.
|
|
129
|
+
React.useEffect(registerDescription, [registerDescription]);
|
|
119
130
|
return (_jsx("p", { id: descriptionId, "data-slot": "sheet-description", className: cn("text-sm text-muted-foreground", className), ...props }));
|
|
120
131
|
}
|
|
121
132
|
export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, };
|
package/dist/ui/slider.d.ts
CHANGED
|
@@ -8,11 +8,12 @@ export interface SliderProps extends Omit<React.ComponentProps<"div">, "defaultV
|
|
|
8
8
|
step?: number;
|
|
9
9
|
orientation?: "horizontal" | "vertical";
|
|
10
10
|
disabled?: boolean;
|
|
11
|
+
name?: string;
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
14
|
* Self-contained slider with pointer drag and keyboard control. Supports one or
|
|
14
15
|
* more thumbs (the nearest thumb to the pointer is the one that moves). Track,
|
|
15
16
|
* range, and thumbs mirror the shadcn structure/classes.
|
|
16
17
|
*/
|
|
17
|
-
declare function Slider({ className, value, defaultValue, onValueChange, min, max, step, orientation, disabled, ...props }: SliderProps): React.JSX.Element;
|
|
18
|
+
declare function Slider({ className, value, defaultValue, onValueChange, min, max, step, orientation, disabled, name, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }: SliderProps): React.JSX.Element;
|
|
18
19
|
export { Slider };
|
package/dist/ui/slider.js
CHANGED
|
@@ -9,7 +9,10 @@ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
|
9
9
|
* more thumbs (the nearest thumb to the pointer is the one that moves). Track,
|
|
10
10
|
* range, and thumbs mirror the shadcn structure/classes.
|
|
11
11
|
*/
|
|
12
|
-
function Slider({ className, value, defaultValue, onValueChange, min = 0, max = 100, step = 1, orientation = "horizontal", disabled = false,
|
|
12
|
+
function Slider({ className, value, defaultValue, onValueChange, min = 0, max = 100, step = 1, orientation = "horizontal", disabled = false, name,
|
|
13
|
+
// Pull the accessible-name props off so they don't land on the outer wrapper
|
|
14
|
+
// div; the thumb (role="slider") is the element that needs them.
|
|
15
|
+
"aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }) {
|
|
13
16
|
const [values, setValues] = useControllableState({
|
|
14
17
|
value,
|
|
15
18
|
defaultValue: defaultValue ?? [min],
|
|
@@ -85,10 +88,12 @@ function Slider({ className, value, defaultValue, onValueChange, min = 0, max =
|
|
|
85
88
|
};
|
|
86
89
|
const rangeStart = values.length > 1 ? Math.min(...values.map(percent)) : 0;
|
|
87
90
|
const rangeEnd = Math.max(...values.map(percent));
|
|
88
|
-
return (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
return (_jsxs("div", { "data-slot": "slider", "data-orientation": orientation, "data-disabled": disabled ? "" : undefined, className: cn("data-horizontal:w-full data-vertical:h-full", className), ...props, children: [_jsxs("div", { "data-orientation": orientation, onPointerDown: onPointerDown, className: "relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col", children: [_jsx("div", { ref: trackRef, "data-slot": "slider-track", "data-orientation": orientation, className: "relative grow overflow-hidden rounded-full bg-muted select-none data-horizontal:h-1.5 data-horizontal:w-full data-vertical:h-full data-vertical:w-1.5", children: _jsx("div", { "data-slot": "slider-range", "data-orientation": orientation, className: "absolute bg-primary select-none data-horizontal:h-full data-vertical:w-full", style: vertical
|
|
92
|
+
? { bottom: `${rangeStart}%`, height: `${rangeEnd - rangeStart}%` }
|
|
93
|
+
: { left: `${rangeStart}%`, width: `${rangeEnd - rangeStart}%` } }) }), values.map((v, index) => (_jsx("span", { role: "slider", tabIndex: disabled ? -1 : 0, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": v, "aria-orientation": orientation, "aria-disabled": disabled || undefined, "data-slot": "slider-thumb", "data-disabled": disabled ? "" : undefined, onKeyDown: onThumbKeyDown(index), className: "absolute block size-4 shrink-0 -translate-x-1/2 cursor-pointer rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] select-none hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 data-vertical:translate-x-0 data-vertical:-translate-y-1/2", "data-orientation": orientation, style: vertical
|
|
94
|
+
? { bottom: `${percent(v)}%` }
|
|
95
|
+
: { left: `${percent(v)}%` } }, index)))] }), name
|
|
96
|
+
? values.map((v, i) => (_jsx("input", { type: "hidden", name: name, value: v }, i)))
|
|
97
|
+
: null] }));
|
|
93
98
|
}
|
|
94
99
|
export { Slider };
|
package/dist/ui/tabs.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ declare function TabsList({ className, variant, onKeyDown, ...props }: React.Com
|
|
|
21
21
|
export interface TabsTriggerProps extends Omit<React.ComponentProps<"button">, "value"> {
|
|
22
22
|
value: string;
|
|
23
23
|
}
|
|
24
|
-
declare function TabsTrigger({ className, value, onClick, ...props }: TabsTriggerProps): React.JSX.Element;
|
|
24
|
+
declare function TabsTrigger({ className, value, onClick, ref, ...props }: TabsTriggerProps): React.JSX.Element;
|
|
25
25
|
export interface TabsContentProps extends Omit<React.ComponentProps<"div">, "value"> {
|
|
26
26
|
value: string;
|
|
27
27
|
}
|