@ladder-ui/primitives 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +130 -0
- package/dist/index.mjs +125 -0
- package/dist/use-checkbox.d.ts +27 -0
- package/dist/use-outside-click.d.ts +3 -0
- package/dist/use-radio-group.d.ts +13 -0
- package/dist/use-select.d.ts +18 -0
- package/package.json +42 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
function useCheckbox(props) {
|
|
6
|
+
const { ref, indeterminate = false, status = "default", disabled, "aria-invalid": ariaInvalid, } = props;
|
|
7
|
+
const internalRef = react.useRef(null);
|
|
8
|
+
react.useEffect(() => {
|
|
9
|
+
if (internalRef.current) {
|
|
10
|
+
internalRef.current.indeterminate = indeterminate;
|
|
11
|
+
}
|
|
12
|
+
}, [indeterminate]);
|
|
13
|
+
function setRef(node) {
|
|
14
|
+
internalRef.current = node;
|
|
15
|
+
if (typeof ref === "function") {
|
|
16
|
+
ref(node);
|
|
17
|
+
}
|
|
18
|
+
else if (ref) {
|
|
19
|
+
ref.current = node;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const isInvalid = ariaInvalid ?? (status === "error" ? true : undefined);
|
|
23
|
+
return {
|
|
24
|
+
wrapperProps: {
|
|
25
|
+
"data-slot": "checkbox",
|
|
26
|
+
"data-disabled": disabled ? "true" : undefined,
|
|
27
|
+
"data-status": status !== "default" ? status : undefined,
|
|
28
|
+
},
|
|
29
|
+
inputProps: {
|
|
30
|
+
ref: setRef,
|
|
31
|
+
type: "checkbox",
|
|
32
|
+
disabled,
|
|
33
|
+
"aria-invalid": isInvalid,
|
|
34
|
+
"aria-checked": indeterminate ? "mixed" : undefined, // Useful for AT
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function useSelect(props) {
|
|
40
|
+
const { value: controlledValue, defaultValue, onValueChange, open: controlledOpen, onOpenChange, disabled, } = props;
|
|
41
|
+
const [internalOpen, setInternalOpen] = react.useState(false);
|
|
42
|
+
const [internalValue, setInternalValue] = react.useState(defaultValue);
|
|
43
|
+
const isOpenControlled = controlledOpen !== undefined;
|
|
44
|
+
const isValueControlled = controlledValue !== undefined;
|
|
45
|
+
const open = isOpenControlled ? controlledOpen : internalOpen;
|
|
46
|
+
const value = isValueControlled ? controlledValue : internalValue;
|
|
47
|
+
const triggerId = react.useId();
|
|
48
|
+
const contentId = react.useId();
|
|
49
|
+
const triggerRef = react.useRef(null);
|
|
50
|
+
const setOpen = react.useCallback((next) => {
|
|
51
|
+
if (disabled)
|
|
52
|
+
return;
|
|
53
|
+
if (!isOpenControlled)
|
|
54
|
+
setInternalOpen(next);
|
|
55
|
+
onOpenChange?.(next);
|
|
56
|
+
}, [disabled, isOpenControlled, onOpenChange]);
|
|
57
|
+
const handleValueChange = react.useCallback((newValue) => {
|
|
58
|
+
if (disabled)
|
|
59
|
+
return;
|
|
60
|
+
if (!isValueControlled)
|
|
61
|
+
setInternalValue(newValue);
|
|
62
|
+
onValueChange?.(newValue);
|
|
63
|
+
setOpen(false);
|
|
64
|
+
}, [disabled, isValueControlled, onValueChange, setOpen]);
|
|
65
|
+
return {
|
|
66
|
+
open,
|
|
67
|
+
setOpen,
|
|
68
|
+
value,
|
|
69
|
+
onValueChange: handleValueChange,
|
|
70
|
+
triggerId,
|
|
71
|
+
contentId,
|
|
72
|
+
triggerRef,
|
|
73
|
+
disabled,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function useRadioGroup(props = {}) {
|
|
78
|
+
const { value: controlledValue, defaultValue, onValueChange, name: propName, disabled, } = props;
|
|
79
|
+
const [internalValue, setInternalValue] = react.useState(defaultValue);
|
|
80
|
+
const generatedName = react.useId();
|
|
81
|
+
const isValueControlled = controlledValue !== undefined;
|
|
82
|
+
const value = isValueControlled ? controlledValue : internalValue;
|
|
83
|
+
const name = propName ?? generatedName;
|
|
84
|
+
const handleValueChange = react.useCallback((newValue) => {
|
|
85
|
+
if (disabled)
|
|
86
|
+
return;
|
|
87
|
+
if (!isValueControlled)
|
|
88
|
+
setInternalValue(newValue);
|
|
89
|
+
onValueChange?.(newValue);
|
|
90
|
+
}, [disabled, isValueControlled, onValueChange]);
|
|
91
|
+
return {
|
|
92
|
+
value,
|
|
93
|
+
onValueChange: handleValueChange,
|
|
94
|
+
name,
|
|
95
|
+
disabled,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function useOutsideClick(handler, enabled = true) {
|
|
100
|
+
const refs = react.useRef([]);
|
|
101
|
+
react.useEffect(() => {
|
|
102
|
+
if (!enabled)
|
|
103
|
+
return;
|
|
104
|
+
const listener = (event) => {
|
|
105
|
+
const target = event.target;
|
|
106
|
+
// Check if click was inside any of the tracked refs
|
|
107
|
+
const isInside = refs.current.some((ref) => ref.current && ref.current.contains(target));
|
|
108
|
+
if (!isInside) {
|
|
109
|
+
handler(event);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener("mousedown", listener);
|
|
113
|
+
document.addEventListener("touchstart", listener);
|
|
114
|
+
return () => {
|
|
115
|
+
document.removeEventListener("mousedown", listener);
|
|
116
|
+
document.removeEventListener("touchstart", listener);
|
|
117
|
+
};
|
|
118
|
+
}, [handler, enabled]);
|
|
119
|
+
const addRef = (ref) => {
|
|
120
|
+
if (!refs.current.includes(ref)) {
|
|
121
|
+
refs.current.push(ref);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return { addRef };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
exports.useCheckbox = useCheckbox;
|
|
128
|
+
exports.useOutsideClick = useOutsideClick;
|
|
129
|
+
exports.useRadioGroup = useRadioGroup;
|
|
130
|
+
exports.useSelect = useSelect;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useRef, useEffect, useState, useId, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
function useCheckbox(props) {
|
|
4
|
+
const { ref, indeterminate = false, status = "default", disabled, "aria-invalid": ariaInvalid, } = props;
|
|
5
|
+
const internalRef = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (internalRef.current) {
|
|
8
|
+
internalRef.current.indeterminate = indeterminate;
|
|
9
|
+
}
|
|
10
|
+
}, [indeterminate]);
|
|
11
|
+
function setRef(node) {
|
|
12
|
+
internalRef.current = node;
|
|
13
|
+
if (typeof ref === "function") {
|
|
14
|
+
ref(node);
|
|
15
|
+
}
|
|
16
|
+
else if (ref) {
|
|
17
|
+
ref.current = node;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const isInvalid = ariaInvalid ?? (status === "error" ? true : undefined);
|
|
21
|
+
return {
|
|
22
|
+
wrapperProps: {
|
|
23
|
+
"data-slot": "checkbox",
|
|
24
|
+
"data-disabled": disabled ? "true" : undefined,
|
|
25
|
+
"data-status": status !== "default" ? status : undefined,
|
|
26
|
+
},
|
|
27
|
+
inputProps: {
|
|
28
|
+
ref: setRef,
|
|
29
|
+
type: "checkbox",
|
|
30
|
+
disabled,
|
|
31
|
+
"aria-invalid": isInvalid,
|
|
32
|
+
"aria-checked": indeterminate ? "mixed" : undefined, // Useful for AT
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function useSelect(props) {
|
|
38
|
+
const { value: controlledValue, defaultValue, onValueChange, open: controlledOpen, onOpenChange, disabled, } = props;
|
|
39
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
40
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
41
|
+
const isOpenControlled = controlledOpen !== undefined;
|
|
42
|
+
const isValueControlled = controlledValue !== undefined;
|
|
43
|
+
const open = isOpenControlled ? controlledOpen : internalOpen;
|
|
44
|
+
const value = isValueControlled ? controlledValue : internalValue;
|
|
45
|
+
const triggerId = useId();
|
|
46
|
+
const contentId = useId();
|
|
47
|
+
const triggerRef = useRef(null);
|
|
48
|
+
const setOpen = useCallback((next) => {
|
|
49
|
+
if (disabled)
|
|
50
|
+
return;
|
|
51
|
+
if (!isOpenControlled)
|
|
52
|
+
setInternalOpen(next);
|
|
53
|
+
onOpenChange?.(next);
|
|
54
|
+
}, [disabled, isOpenControlled, onOpenChange]);
|
|
55
|
+
const handleValueChange = useCallback((newValue) => {
|
|
56
|
+
if (disabled)
|
|
57
|
+
return;
|
|
58
|
+
if (!isValueControlled)
|
|
59
|
+
setInternalValue(newValue);
|
|
60
|
+
onValueChange?.(newValue);
|
|
61
|
+
setOpen(false);
|
|
62
|
+
}, [disabled, isValueControlled, onValueChange, setOpen]);
|
|
63
|
+
return {
|
|
64
|
+
open,
|
|
65
|
+
setOpen,
|
|
66
|
+
value,
|
|
67
|
+
onValueChange: handleValueChange,
|
|
68
|
+
triggerId,
|
|
69
|
+
contentId,
|
|
70
|
+
triggerRef,
|
|
71
|
+
disabled,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function useRadioGroup(props = {}) {
|
|
76
|
+
const { value: controlledValue, defaultValue, onValueChange, name: propName, disabled, } = props;
|
|
77
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
78
|
+
const generatedName = useId();
|
|
79
|
+
const isValueControlled = controlledValue !== undefined;
|
|
80
|
+
const value = isValueControlled ? controlledValue : internalValue;
|
|
81
|
+
const name = propName ?? generatedName;
|
|
82
|
+
const handleValueChange = useCallback((newValue) => {
|
|
83
|
+
if (disabled)
|
|
84
|
+
return;
|
|
85
|
+
if (!isValueControlled)
|
|
86
|
+
setInternalValue(newValue);
|
|
87
|
+
onValueChange?.(newValue);
|
|
88
|
+
}, [disabled, isValueControlled, onValueChange]);
|
|
89
|
+
return {
|
|
90
|
+
value,
|
|
91
|
+
onValueChange: handleValueChange,
|
|
92
|
+
name,
|
|
93
|
+
disabled,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function useOutsideClick(handler, enabled = true) {
|
|
98
|
+
const refs = useRef([]);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!enabled)
|
|
101
|
+
return;
|
|
102
|
+
const listener = (event) => {
|
|
103
|
+
const target = event.target;
|
|
104
|
+
// Check if click was inside any of the tracked refs
|
|
105
|
+
const isInside = refs.current.some((ref) => ref.current && ref.current.contains(target));
|
|
106
|
+
if (!isInside) {
|
|
107
|
+
handler(event);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
document.addEventListener("mousedown", listener);
|
|
111
|
+
document.addEventListener("touchstart", listener);
|
|
112
|
+
return () => {
|
|
113
|
+
document.removeEventListener("mousedown", listener);
|
|
114
|
+
document.removeEventListener("touchstart", listener);
|
|
115
|
+
};
|
|
116
|
+
}, [handler, enabled]);
|
|
117
|
+
const addRef = (ref) => {
|
|
118
|
+
if (!refs.current.includes(ref)) {
|
|
119
|
+
refs.current.push(ref);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return { addRef };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { useCheckbox, useOutsideClick, useRadioGroup, useSelect };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Ref } from "react";
|
|
2
|
+
export interface UseCheckboxProps {
|
|
3
|
+
/** React ref to forward to the native input element. */
|
|
4
|
+
ref?: Ref<HTMLInputElement>;
|
|
5
|
+
/** Whether the checkbox is in an indeterminate state. */
|
|
6
|
+
indeterminate?: boolean;
|
|
7
|
+
/** Visual validation state. "error" will set aria-invalid="true". */
|
|
8
|
+
status?: "default" | "error";
|
|
9
|
+
/** Native disabled state. */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** Explicit aria-invalid, overrides status="error" */
|
|
12
|
+
"aria-invalid"?: boolean | "false" | "true" | "grammar" | "spelling";
|
|
13
|
+
}
|
|
14
|
+
export declare function useCheckbox(props: UseCheckboxProps): {
|
|
15
|
+
wrapperProps: {
|
|
16
|
+
"data-slot": string;
|
|
17
|
+
"data-disabled": string | undefined;
|
|
18
|
+
"data-status": "error" | undefined;
|
|
19
|
+
};
|
|
20
|
+
inputProps: {
|
|
21
|
+
ref: (node: HTMLInputElement | null) => void;
|
|
22
|
+
type: string;
|
|
23
|
+
disabled: boolean | undefined;
|
|
24
|
+
"aria-invalid": boolean | "false" | "true" | "grammar" | "spelling" | undefined;
|
|
25
|
+
"aria-checked": "mixed" | undefined;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface UseRadioGroupProps {
|
|
2
|
+
value?: string;
|
|
3
|
+
defaultValue?: string;
|
|
4
|
+
onValueChange?: (value: string) => void;
|
|
5
|
+
name?: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function useRadioGroup(props?: UseRadioGroupProps): {
|
|
9
|
+
value: string | undefined;
|
|
10
|
+
onValueChange: (newValue: string) => void;
|
|
11
|
+
name: string;
|
|
12
|
+
disabled: boolean | undefined;
|
|
13
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface UseSelectProps {
|
|
2
|
+
value?: string;
|
|
3
|
+
defaultValue?: string;
|
|
4
|
+
onValueChange?: (value: string) => void;
|
|
5
|
+
open?: boolean;
|
|
6
|
+
onOpenChange?: (open: boolean) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function useSelect(props: UseSelectProps): {
|
|
10
|
+
open: boolean;
|
|
11
|
+
setOpen: (next: boolean) => void;
|
|
12
|
+
value: string | undefined;
|
|
13
|
+
onValueChange: (newValue: string) => void;
|
|
14
|
+
triggerId: string;
|
|
15
|
+
contentId: string;
|
|
16
|
+
triggerRef: import("react").RefObject<HTMLButtonElement | null>;
|
|
17
|
+
disabled: boolean | undefined;
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ladder-ui/primitives",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Pure headless logic and accessibility primitives for Ladder UI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
21
|
+
"@types/react": "^19.0.0",
|
|
22
|
+
"rollup": "^4.59.0",
|
|
23
|
+
"tslib": "^2.6.2",
|
|
24
|
+
"typescript": "^5.3.3",
|
|
25
|
+
"@ladder-ui/core": "0.4.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@ladder-ui/core": ">=0.0.0",
|
|
29
|
+
"react": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/ivelaval/ladder-ui.git",
|
|
34
|
+
"directory": "packages/primitives"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "pnpm clean && rollup -c",
|
|
38
|
+
"dev": "rollup -c -w",
|
|
39
|
+
"type-check": "tsc --noEmit",
|
|
40
|
+
"clean": "rm -rf dist"
|
|
41
|
+
}
|
|
42
|
+
}
|