@stevederico/skateboard-ui 4.1.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 +23 -0
- 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 +28 -8
- package/dist/ui/dialog.js +18 -17
- package/dist/ui/drawer.js +28 -16
- package/dist/ui/dropdown-menu.js +44 -14
- package/dist/ui/menubar.js +35 -14
- package/dist/ui/popover.js +24 -5
- package/dist/ui/select.d.ts +1 -1
- package/dist/ui/select.js +27 -6
- package/dist/ui/sheet.js +27 -16
- package/dist/ui/slider.d.ts +2 -1
- package/dist/ui/slider.js +11 -6
- package/dist/ui/tooltip.js +27 -1
- 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/package.json +1 -1
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/tooltip.js
CHANGED
|
@@ -68,7 +68,7 @@ function TooltipTrigger({ asChild = false, render, nativeButton: _nativeButton,
|
|
|
68
68
|
}, ...props, children: slotChild }));
|
|
69
69
|
}
|
|
70
70
|
function TooltipContent({ className, style, ref, side = "top", sideOffset = 4, align = "center", alignOffset = 0, children, ...props }) {
|
|
71
|
-
const { open, contentId, triggerRef } = useTooltip();
|
|
71
|
+
const { open, contentId, triggerRef, hide } = useTooltip();
|
|
72
72
|
const [mounted, presenceRef] = usePresence(open);
|
|
73
73
|
const { floatingRef, pos } = useFloating(triggerRef, mounted, {
|
|
74
74
|
side,
|
|
@@ -76,6 +76,32 @@ function TooltipContent({ className, style, ref, side = "top", sideOffset = 4, a
|
|
|
76
76
|
sideOffset,
|
|
77
77
|
alignOffset,
|
|
78
78
|
});
|
|
79
|
+
// Touch dismissal: on touch devices a tooltip can open via tap/long-press
|
|
80
|
+
// (which focuses the trigger) with no pointer-leave/blur to close it again.
|
|
81
|
+
// While open, close it when the user points outside both the trigger and the
|
|
82
|
+
// content, and on scroll — so it can never get stuck on screen. Capture phase
|
|
83
|
+
// ensures we observe the pointerdown even if inner handlers stop propagation.
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
if (!open)
|
|
86
|
+
return;
|
|
87
|
+
const onPointerDown = (e) => {
|
|
88
|
+
const target = e.target;
|
|
89
|
+
if (!(target instanceof Node))
|
|
90
|
+
return;
|
|
91
|
+
const trigger = triggerRef.current;
|
|
92
|
+
const content = presenceRef.current;
|
|
93
|
+
if (trigger?.contains(target) || content?.contains(target))
|
|
94
|
+
return;
|
|
95
|
+
hide();
|
|
96
|
+
};
|
|
97
|
+
const onScroll = () => hide();
|
|
98
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
99
|
+
window.addEventListener("scroll", onScroll, true);
|
|
100
|
+
return () => {
|
|
101
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
102
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
103
|
+
};
|
|
104
|
+
}, [open, hide, triggerRef, presenceRef]);
|
|
79
105
|
if (!mounted)
|
|
80
106
|
return null;
|
|
81
107
|
return (_jsx(Portal, { children: _jsx("div", { ref: mergeRefs(floatingRef, presenceRef, ref), id: contentId, role: "tooltip", "data-slot": "tooltip-content", "data-state": open ? "open" : "closed", "data-open": open ? "" : undefined, "data-closed": open ? undefined : "", "data-side": pos?.side ?? side, style: {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface DialogLabelling {
|
|
2
|
+
titleId: string;
|
|
3
|
+
descriptionId: string;
|
|
4
|
+
/** Call from the title element's effect; returns the unregister cleanup. */
|
|
5
|
+
registerTitle: () => () => void;
|
|
6
|
+
/** Call from the description element's effect; returns the unregister cleanup. */
|
|
7
|
+
registerDescription: () => () => void;
|
|
8
|
+
/** `aria-labelledby` value — undefined when no title is rendered, so the
|
|
9
|
+
* attribute is omitted instead of pointing at a missing element. */
|
|
10
|
+
labelledBy: string | undefined;
|
|
11
|
+
/** `aria-describedby` value — undefined when no description is rendered. */
|
|
12
|
+
describedBy: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Tracks whether a title/description part is actually rendered inside an overlay
|
|
16
|
+
* so the container can wire `aria-labelledby`/`aria-describedby` only when the
|
|
17
|
+
* referenced element exists. An unconditional IDREF to an absent node is an
|
|
18
|
+
* accessibility defect (dangling reference) that assistive tech may announce as
|
|
19
|
+
* an empty or broken label. Reference-counted so it is robust to remounting.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useDialogLabelling(): DialogLabelling;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Tracks whether a title/description part is actually rendered inside an overlay
|
|
5
|
+
* so the container can wire `aria-labelledby`/`aria-describedby` only when the
|
|
6
|
+
* referenced element exists. An unconditional IDREF to an absent node is an
|
|
7
|
+
* accessibility defect (dangling reference) that assistive tech may announce as
|
|
8
|
+
* an empty or broken label. Reference-counted so it is robust to remounting.
|
|
9
|
+
*/
|
|
10
|
+
export function useDialogLabelling() {
|
|
11
|
+
const titleId = React.useId();
|
|
12
|
+
const descriptionId = React.useId();
|
|
13
|
+
const titleCount = React.useRef(0);
|
|
14
|
+
const descriptionCount = React.useRef(0);
|
|
15
|
+
const [hasTitle, setHasTitle] = React.useState(false);
|
|
16
|
+
const [hasDescription, setHasDescription] = React.useState(false);
|
|
17
|
+
const registerTitle = React.useCallback(() => {
|
|
18
|
+
titleCount.current += 1;
|
|
19
|
+
setHasTitle(true);
|
|
20
|
+
return () => {
|
|
21
|
+
titleCount.current -= 1;
|
|
22
|
+
if (titleCount.current === 0)
|
|
23
|
+
setHasTitle(false);
|
|
24
|
+
};
|
|
25
|
+
}, []);
|
|
26
|
+
const registerDescription = React.useCallback(() => {
|
|
27
|
+
descriptionCount.current += 1;
|
|
28
|
+
setHasDescription(true);
|
|
29
|
+
return () => {
|
|
30
|
+
descriptionCount.current -= 1;
|
|
31
|
+
if (descriptionCount.current === 0)
|
|
32
|
+
setHasDescription(false);
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
return {
|
|
36
|
+
titleId,
|
|
37
|
+
descriptionId,
|
|
38
|
+
registerTitle,
|
|
39
|
+
registerDescription,
|
|
40
|
+
labelledBy: hasTitle ? titleId : undefined,
|
|
41
|
+
describedBy: hasDescription ? descriptionId : undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Guard against synthetic `pointermove` events that fire when content scrolls
|
|
4
|
+
* under a stationary pointer (e.g. keyboard arrow navigation scrolls the focused
|
|
5
|
+
* item into view). Those events report the SAME client coordinates as the last,
|
|
6
|
+
* so pointer-driven focus would otherwise yank focus off the keyboard target.
|
|
7
|
+
*
|
|
8
|
+
* Returns a predicate that is true only when the pointer's client position
|
|
9
|
+
* actually changed since the previous call — wire it at the top of a menu's
|
|
10
|
+
* `onPointerMove` and bail when it returns false.
|
|
11
|
+
*/
|
|
12
|
+
export declare function usePointerMoved(): (event: React.PointerEvent) => boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Guard against synthetic `pointermove` events that fire when content scrolls
|
|
5
|
+
* under a stationary pointer (e.g. keyboard arrow navigation scrolls the focused
|
|
6
|
+
* item into view). Those events report the SAME client coordinates as the last,
|
|
7
|
+
* so pointer-driven focus would otherwise yank focus off the keyboard target.
|
|
8
|
+
*
|
|
9
|
+
* Returns a predicate that is true only when the pointer's client position
|
|
10
|
+
* actually changed since the previous call — wire it at the top of a menu's
|
|
11
|
+
* `onPointerMove` and bail when it returns false.
|
|
12
|
+
*/
|
|
13
|
+
export function usePointerMoved() {
|
|
14
|
+
const last = React.useRef(null);
|
|
15
|
+
return React.useCallback((event) => {
|
|
16
|
+
const prev = last.current;
|
|
17
|
+
const moved = !prev || prev.x !== event.clientX || prev.y !== event.clientY;
|
|
18
|
+
last.current = { x: event.clientX, y: event.clientY };
|
|
19
|
+
return moved;
|
|
20
|
+
}, []);
|
|
21
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock background scroll while `active` is true, reentrant across any number of
|
|
3
|
+
* simultaneously-open overlays. The original `body` overflow is captured on the
|
|
4
|
+
* first lock and restored only when the last lock releases.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useScrollLock(active: boolean): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
// Module-level reference count so stacked overlays share one lock: the first to
|
|
4
|
+
// open saves and replaces `body` overflow, and only the last to close restores
|
|
5
|
+
// it. Without this, an inner overlay's cleanup would unlock scroll while an
|
|
6
|
+
// outer overlay is still open (or vice-versa).
|
|
7
|
+
let lockCount = 0;
|
|
8
|
+
let previousOverflow = "";
|
|
9
|
+
/**
|
|
10
|
+
* Lock background scroll while `active` is true, reentrant across any number of
|
|
11
|
+
* simultaneously-open overlays. The original `body` overflow is captured on the
|
|
12
|
+
* first lock and restored only when the last lock releases.
|
|
13
|
+
*/
|
|
14
|
+
export function useScrollLock(active) {
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (!active)
|
|
17
|
+
return;
|
|
18
|
+
if (lockCount === 0) {
|
|
19
|
+
previousOverflow = document.body.style.overflow;
|
|
20
|
+
document.body.style.overflow = "hidden";
|
|
21
|
+
}
|
|
22
|
+
lockCount++;
|
|
23
|
+
return () => {
|
|
24
|
+
lockCount--;
|
|
25
|
+
if (lockCount === 0)
|
|
26
|
+
document.body.style.overflow = previousOverflow;
|
|
27
|
+
};
|
|
28
|
+
}, [active]);
|
|
29
|
+
}
|