@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.
@@ -0,0 +1,4 @@
1
+ export { useCheckbox, type UseCheckboxProps } from "./use-checkbox";
2
+ export { useSelect, type UseSelectProps } from "./use-select";
3
+ export { useRadioGroup, type UseRadioGroupProps } from "./use-radio-group";
4
+ export { useOutsideClick } from "./use-outside-click";
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,3 @@
1
+ export declare function useOutsideClick(handler: (event: MouseEvent | TouchEvent) => void, enabled?: boolean): {
2
+ addRef: (ref: React.RefObject<HTMLElement | null>) => void;
3
+ };
@@ -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
+ }