@nikala-ui/hooks 0.8.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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @nikala-ui/hooks
2
+
3
+ Reactive SolidJS state primitives and custom hooks for **Nikala UI**.
4
+
5
+ Honoring the iconic Georgian painter **Niko Pirosmani (Nikala)**.
6
+
7
+ Official Documentation & Interactive Demos: [nikala.magradze.dev](https://nikala.magradze.dev)
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ This package provides reusable, fine-grained reactive primitives designed natively for SolidJS applications. It simplifies managing complex component state such as controlled and uncontrolled inputs, state synchronization, and reactive event callbacks.
14
+
15
+ ---
16
+
17
+ ## Core Features & Available Hooks
18
+
19
+ - **`createControllableSignal`** — SolidJS reactive primitive for managing state supporting both controlled and uncontrolled modes with a unified setter API.
20
+ - **`createClickOutside`** — SolidJS reactive primitive for detecting user interactions outside specified element(s) or refs.
21
+ - **`createClipboard`** — SolidJS reactive primitive for copying text to clipboard with automatic status reset.
22
+ - **`createKeybindings` / `createEscapeKey`** — SolidJS reactive primitives for listening to keyboard shortcuts, key combinations, and Escape key presses.
23
+ - **`createLockScroll`** — SolidJS reactive primitive for locking body or container scrolling when overlays are active.
24
+ - **`createDisclosure`** — SolidJS reactive primitive for managing boolean open/close state with toggle, open, and close helpers.
25
+ - **`createMediaQuery` / `createBreakpoint`** — SolidJS reactive primitives for tracking CSS media queries and responsive Tailwind breakpoints.
26
+ - **`createDebounce` / `createThrottle`** — SolidJS reactive primitives for debouncing and throttling rate-limited function execution.
27
+ - **`createIntersectionObserver` / `createInView`** — SolidJS reactive primitives for detecting element viewport visibility and scroll animation triggers.
28
+ - **`createTimer` / `createCountdown`** — SolidJS reactive primitives for recurring interval ticks and formatted countdown timers.
29
+ - **`createResizeObserver` / `createElementSize`** — SolidJS reactive primitives for tracking element width and height dimensions dynamically.
30
+ - **`createWindowSize`** — SolidJS reactive primitive for tracking window viewport inner width and height.
31
+ - **`createScrollPosition`** — SolidJS reactive primitive for tracking scroll position, scroll direction, and top/bottom container status.
32
+ - **`createFocusTrap`** — SolidJS reactive primitive for trapping keyboard focus inside target container element for accessibility (WCAG).
33
+ - **`createMousePosition`** — SolidJS reactive primitive for tracking global and element-relative mouse pointer coordinates.
34
+ - **`createLongPress`** — SolidJS reactive primitive for detecting long press / hold touch and pointer interactions.
35
+ - **`createHover`** — SolidJS reactive primitive for tracking element hover state with entrance/exit delays.
36
+ - **`createLocalStorage` / `createSessionStorage`** — SolidJS reactive primitives for Web Storage state synchronization across tabs.
37
+ - **`createPrevious`** — SolidJS reactive primitive for tracking previous value of a signal accessor.
38
+ - **`createNetworkStatus` / `createOnline`** — SolidJS reactive primitives for tracking browser network connectivity and connection quality metrics.
39
+ - **`createColorMode`** — SolidJS reactive primitive for managing dark/light themes and system preferences.
40
+ - **`createForm`** — SolidJS reactive primitive for form state management, field validation, errors, and submission.
41
+ - **`createInputMask`** — SolidJS reactive primitive for input value masking (phone numbers, credit cards, dates).
42
+ - **`createIdle`** — SolidJS reactive primitive for detecting user inactivity with customizable timeout and activity events.
43
+ - **`createActiveElement`** — SolidJS reactive primitive for tracking the currently focused DOM element.
44
+ - **Native SolidJS Reactivity** — Zero-dependency, fine-grained reactivity built directly on SolidJS signals.
45
+ - **TypeScript First** — Fully typed options, getters, and return tuple interfaces.
46
+
47
+ ---
48
+
49
+ ## Installation & Usage
50
+
51
+ Install the hooks package via your preferred package manager:
52
+
53
+ ```bash
54
+ bun add @nikala-ui/hooks
55
+ # or
56
+ npm install @nikala-ui/hooks
57
+ ```
58
+
59
+ Import hooks directly into your SolidJS components:
60
+
61
+ ```tsx
62
+ import { createControllableSignal } from "@nikala-ui/hooks";
63
+
64
+ const [value, setValue] = createControllableSignal({
65
+ defaultValue: "Uncontrolled Initial State",
66
+ });
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Documentation & Links
72
+
73
+ - Repository: [github.com/nikala-ui/ui](https://github.com/nikala-ui/ui)
74
+ - Hooks Documentation: [nikala.magradze.dev/docs/hooks](https://nikala.magradze.dev/docs/hooks)
75
+
76
+ ## License
77
+
78
+ [MIT](https://github.com/nikala-ui/ui/blob/main/LICENSE)
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@nikala-ui/hooks",
3
+ "version": "0.8.0",
4
+ "description": "Reactive SolidJS primitives and primitives for Nikala UI",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "module": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/nikala-ui/ui.git",
18
+ "directory": "packages/hooks"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^26.1.1",
22
+ "typescript": "^5.7.2"
23
+ },
24
+ "peerDependencies": {
25
+ "solid-js": "^1.9.0"
26
+ }
27
+ }
@@ -0,0 +1,51 @@
1
+ import { createSignal, onMount, onCleanup, type Accessor } from "solid-js";
2
+ import { isServer } from "solid-js/web";
3
+
4
+ export interface CreateActiveElementReturn {
5
+ /** Accessor returning the currently focused DOM element, or null */
6
+ activeElement: Accessor<Element | null>;
7
+ /** Accessor returning true if any element (non-body) is focused */
8
+ hasFocus: Accessor<boolean>;
9
+ }
10
+
11
+ /**
12
+ * SolidJS reactive primitive for tracking the currently focused (active) DOM element.
13
+ * Listens to focus/blur events on the document and reactively updates `activeElement` and `hasFocus`.
14
+ */
15
+ export function createActiveElement(): CreateActiveElementReturn {
16
+ // SSR: return static defaults
17
+ if (isServer) {
18
+ const [activeElement] = createSignal<Element | null>(null);
19
+ const [hasFocus] = createSignal(false);
20
+ return { activeElement, hasFocus };
21
+ }
22
+
23
+ const [activeElement, setActiveElement] = createSignal<Element | null>(null);
24
+
25
+ const hasFocus = () => {
26
+ const el = activeElement();
27
+ return el !== null && el !== document.body;
28
+ };
29
+
30
+ const handleFocusChange = () => {
31
+ setActiveElement(document.activeElement);
32
+ };
33
+
34
+ onMount(() => {
35
+ // Set initial value
36
+ setActiveElement(document.activeElement);
37
+
38
+ document.addEventListener("focusin", handleFocusChange, true);
39
+ document.addEventListener("focusout", handleFocusChange, true);
40
+
41
+ onCleanup(() => {
42
+ document.removeEventListener("focusin", handleFocusChange, true);
43
+ document.removeEventListener("focusout", handleFocusChange, true);
44
+ });
45
+ });
46
+
47
+ return {
48
+ activeElement,
49
+ hasFocus,
50
+ };
51
+ }
@@ -0,0 +1,82 @@
1
+ import { onMount, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export interface CreateClickOutsideOptions {
4
+ /** Target HTML element or accessor returning the container element */
5
+ target:
6
+ | HTMLElement
7
+ | Accessor<HTMLElement | undefined>
8
+ | (HTMLElement | Accessor<HTMLElement | undefined>)[];
9
+ /** Callback fired when a click/pointer event occurs outside the target element(s) */
10
+ onInteractOutside: (event: MouseEvent | PointerEvent | TouchEvent) => void;
11
+ /** Whether the listener is active. Defaults to true. */
12
+ enabled?: boolean | Accessor<boolean>;
13
+ /** Optional element or accessor to ignore when checking outside clicks (e.g. trigger button) */
14
+ ignore?:
15
+ | HTMLElement
16
+ | Accessor<HTMLElement | undefined>
17
+ | (HTMLElement | Accessor<HTMLElement | undefined>)[];
18
+ }
19
+
20
+ /**
21
+ * SolidJS reactive primitive for detecting user interactions outside specified element(s).
22
+ *
23
+ * @param options Configuration options including target element(s), callback, and optional ignore elements.
24
+ */
25
+ export function createClickOutside(options: CreateClickOutsideOptions): void {
26
+ const isEnabled = () => {
27
+ if (typeof options.enabled === "function") {
28
+ return options.enabled();
29
+ }
30
+ return options.enabled ?? true;
31
+ };
32
+
33
+ const getElement = (
34
+ el: HTMLElement | Accessor<HTMLElement | undefined> | undefined
35
+ ): HTMLElement | undefined => {
36
+ if (!el) return undefined;
37
+ if (typeof el === "function") {
38
+ return (el as Accessor<HTMLElement | undefined>)();
39
+ }
40
+ return el;
41
+ };
42
+
43
+ const getElements = (
44
+ targets:
45
+ | HTMLElement
46
+ | Accessor<HTMLElement | undefined>
47
+ | (HTMLElement | Accessor<HTMLElement | undefined>)[]
48
+ | undefined
49
+ ): HTMLElement[] => {
50
+ if (!targets) return [];
51
+ const list = Array.isArray(targets) ? targets : [targets];
52
+ return list.map(getElement).filter((e): e is HTMLElement => e !== undefined);
53
+ };
54
+
55
+ const handlePointerDown = (event: MouseEvent | PointerEvent | TouchEvent) => {
56
+ if (!isEnabled()) return;
57
+
58
+ const targetNode = event.target as Node | null;
59
+ if (!targetNode) return;
60
+
61
+ const mainElements = getElements(options.target);
62
+ if (mainElements.length === 0) return;
63
+
64
+ const isInsideTarget = mainElements.some((el) => el.contains(targetNode));
65
+ if (isInsideTarget) return;
66
+
67
+ const ignoreElements = getElements(options.ignore);
68
+ const isInsideIgnore = ignoreElements.some((el) => el.contains(targetNode));
69
+ if (isInsideIgnore) return;
70
+
71
+ options.onInteractOutside(event);
72
+ };
73
+
74
+ onMount(() => {
75
+ if (typeof window === "undefined") return;
76
+
77
+ window.addEventListener("pointerdown", handlePointerDown, true);
78
+ onCleanup(() => {
79
+ window.removeEventListener("pointerdown", handlePointerDown, true);
80
+ });
81
+ });
82
+ }
@@ -0,0 +1,69 @@
1
+ import { createSignal, onCleanup, type Accessor } from "solid-js";
2
+
3
+ export interface CreateClipboardOptions {
4
+ /** Time in milliseconds to maintain the copied state before resetting. Defaults to 2000ms. */
5
+ timeout?: number;
6
+ }
7
+
8
+ export interface CreateClipboardReturn {
9
+ /** Signal accessor indicating if content was recently copied */
10
+ copied: Accessor<boolean>;
11
+ /** Function to copy text to clipboard */
12
+ copy: (text: string) => Promise<boolean>;
13
+ /** Error state if clipboard write fails */
14
+ error: Accessor<Error | undefined>;
15
+ }
16
+
17
+ /**
18
+ * SolidJS reactive primitive for copying text to clipboard with automatic reset state.
19
+ *
20
+ * @param options Configuration options including copied status reset timeout duration.
21
+ */
22
+ export function createClipboard(options: CreateClipboardOptions = {}): CreateClipboardReturn {
23
+ const timeoutDuration = options.timeout ?? 2000;
24
+ const [copied, setCopied] = createSignal(false);
25
+ const [error, setError] = createSignal<Error | undefined>(undefined);
26
+ let timer: ReturnType<typeof setTimeout> | undefined;
27
+
28
+ const clearTimer = () => {
29
+ if (timer) {
30
+ clearTimeout(timer);
31
+ timer = undefined;
32
+ }
33
+ };
34
+
35
+ const copy = async (text: string): Promise<boolean> => {
36
+ clearTimer();
37
+ setError(undefined);
38
+
39
+ try {
40
+ if (typeof window !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
41
+ await navigator.clipboard.writeText(text);
42
+ setCopied(true);
43
+
44
+ timer = setTimeout(() => {
45
+ setCopied(false);
46
+ }, timeoutDuration);
47
+
48
+ return true;
49
+ }
50
+
51
+ throw new Error("Clipboard API not supported");
52
+ } catch (err) {
53
+ const copyError = err instanceof Error ? err : new Error(String(err));
54
+ setError(copyError);
55
+ setCopied(false);
56
+ return false;
57
+ }
58
+ };
59
+
60
+ onCleanup(() => {
61
+ clearTimer();
62
+ });
63
+
64
+ return {
65
+ copied,
66
+ copy,
67
+ error,
68
+ };
69
+ }
@@ -0,0 +1,115 @@
1
+ import { createEffect, createSignal, onCleanup, onMount, type Accessor } from "solid-js";
2
+
3
+ export type ColorMode = "light" | "dark" | "system";
4
+
5
+ export interface CreateColorModeOptions {
6
+ /** Initial color mode fallback. Defaults to 'system'. */
7
+ initialValue?: ColorMode;
8
+ /** LocalStorage key for persisting color mode state. Defaults to 'nikala-color-mode'. */
9
+ storageKey?: string;
10
+ /** HTML attribute to apply dark mode class on document.documentElement. Defaults to 'class'. */
11
+ attribute?: string;
12
+ }
13
+
14
+ export interface CreateColorModeReturn {
15
+ /** Accessor for current active color mode ('light', 'dark', 'system') */
16
+ mode: Accessor<ColorMode>;
17
+ /** Function to update current color mode */
18
+ setMode: (mode: ColorMode) => void;
19
+ /** Function to toggle color mode between 'light' and 'dark' */
20
+ toggleColorMode: () => void;
21
+ /** Accessor indicating if current resolved mode is dark */
22
+ isDark: Accessor<boolean>;
23
+ }
24
+
25
+ /**
26
+ * SolidJS reactive primitive for managing dark/light color mode themes and system preferences.
27
+ *
28
+ * @param options Configuration options including storage key and initial mode.
29
+ */
30
+ export function createColorMode(
31
+ options: CreateColorModeOptions = {}
32
+ ): CreateColorModeReturn {
33
+ const initialMode = options.initialValue ?? "system";
34
+ const storageKey = options.storageKey ?? "nikala-color-mode";
35
+ const attribute = options.attribute ?? "class";
36
+
37
+ const [mode, setModeSignal] = createSignal<ColorMode>(initialMode);
38
+ const [systemDark, setSystemDark] = createSignal(false);
39
+
40
+ const isDark = () => {
41
+ const currentMode = mode();
42
+ if (currentMode === "system") return systemDark();
43
+ return currentMode === "dark";
44
+ };
45
+
46
+ const applyTheme = (dark: boolean) => {
47
+ if (typeof document === "undefined") return;
48
+ const root = document.documentElement;
49
+ if (attribute === "class") {
50
+ if (dark) root.classList.add("dark");
51
+ else root.classList.remove("dark");
52
+ } else {
53
+ root.setAttribute(attribute, dark ? "dark" : "light");
54
+ }
55
+ };
56
+
57
+ const setMode = (newMode: ColorMode) => {
58
+ setModeSignal(newMode);
59
+ if (typeof window !== "undefined") {
60
+ try {
61
+ localStorage.setItem(storageKey, newMode);
62
+ } catch (e) {
63
+ console.warn(`[nikala-ui/hooks] Error setting color mode storage:`, e);
64
+ }
65
+ }
66
+ };
67
+
68
+ const toggleColorMode = () => {
69
+ const next = isDark() ? "light" : "dark";
70
+ setMode(next);
71
+ };
72
+
73
+ createEffect(() => {
74
+ applyTheme(isDark());
75
+ });
76
+
77
+ onMount(() => {
78
+ if (typeof window === "undefined") return;
79
+
80
+ // Check system preference
81
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
82
+ setSystemDark(mediaQuery.matches);
83
+
84
+ const handleMediaChange = (e: MediaQueryListEvent) => {
85
+ setSystemDark(e.matches);
86
+ };
87
+
88
+ if (mediaQuery.addEventListener) {
89
+ mediaQuery.addEventListener("change", handleMediaChange);
90
+ }
91
+
92
+ // Check localStorage
93
+ try {
94
+ const stored = localStorage.getItem(storageKey) as ColorMode | null;
95
+ if (stored && ["light", "dark", "system"].includes(stored)) {
96
+ setModeSignal(stored);
97
+ }
98
+ } catch (e) {
99
+ console.warn(`[nikala-ui/hooks] Error reading color mode storage:`, e);
100
+ }
101
+
102
+ onCleanup(() => {
103
+ if (mediaQuery.removeEventListener) {
104
+ mediaQuery.removeEventListener("change", handleMediaChange);
105
+ }
106
+ });
107
+ });
108
+
109
+ return {
110
+ mode,
111
+ setMode,
112
+ toggleColorMode,
113
+ isDark,
114
+ };
115
+ }
@@ -0,0 +1,56 @@
1
+ import { createSignal, type Accessor } from "solid-js";
2
+
3
+ export interface CreateControllableSignalOptions<T> {
4
+ /** Controlled value accessor or raw value */
5
+ value?: Accessor<T | undefined> | T;
6
+ /** Uncontrolled default initial value */
7
+ defaultValue?: T;
8
+ /** Callback fired whenever the value changes */
9
+ onChange?: (value: T) => void;
10
+ }
11
+
12
+ /**
13
+ * SolidJS reactive primitive for managing state supporting both controlled and uncontrolled modes.
14
+ *
15
+ * @param options Configuration options for value, defaultValue, and onChange callback.
16
+ * @returns A tuple containing [valueAccessor, setValueFunction].
17
+ */
18
+ export function createControllableSignal<T>(
19
+ options: CreateControllableSignalOptions<T>
20
+ ): [Accessor<T | undefined>, (nextValue: T | ((prev: T | undefined) => T)) => void] {
21
+ const [internalValue, setInternalValue] = createSignal<T | undefined>(
22
+ options.defaultValue
23
+ );
24
+
25
+ const getValue = (): T | undefined => {
26
+ if (typeof options.value === "function") {
27
+ return (options.value as Accessor<T | undefined>)();
28
+ }
29
+ return options.value;
30
+ };
31
+
32
+ const isControlled = () => getValue() !== undefined;
33
+
34
+ const value = () => {
35
+ const controlledVal = getValue();
36
+ return controlledVal !== undefined ? controlledVal : internalValue();
37
+ };
38
+
39
+ const setValue = (nextValue: T | ((prev: T | undefined) => T)) => {
40
+ const current = value();
41
+ const resolvedNext =
42
+ typeof nextValue === "function"
43
+ ? (nextValue as (prev: T | undefined) => T)(current)
44
+ : nextValue;
45
+
46
+ if (!isControlled()) {
47
+ setInternalValue(resolvedNext as any);
48
+ }
49
+
50
+ if (typeof options.onChange === "function") {
51
+ options.onChange(resolvedNext);
52
+ }
53
+ };
54
+
55
+ return [value, setValue];
56
+ }
@@ -0,0 +1,90 @@
1
+ import { onCleanup, type Accessor } from "solid-js";
2
+
3
+ export interface DebounceThrottleReturn<T extends (...args: any[]) => any> {
4
+ (...args: Parameters<T>): void;
5
+ clear: () => void;
6
+ }
7
+
8
+ /**
9
+ * SolidJS reactive primitive for debouncing function execution.
10
+ *
11
+ * @param fn Function to debounce.
12
+ * @param delay Delay in milliseconds (number or accessor).
13
+ */
14
+ export function createDebounce<T extends (...args: any[]) => any>(
15
+ fn: T,
16
+ delay: number | Accessor<number>
17
+ ): DebounceThrottleReturn<T> {
18
+ const getDelay = (): number => (typeof delay === "function" ? delay() : delay);
19
+ let timer: ReturnType<typeof setTimeout> | undefined;
20
+
21
+ const clear = () => {
22
+ if (timer) {
23
+ clearTimeout(timer);
24
+ timer = undefined;
25
+ }
26
+ };
27
+
28
+ const debounced = (...args: Parameters<T>) => {
29
+ clear();
30
+ timer = setTimeout(() => {
31
+ fn(...args);
32
+ }, getDelay());
33
+ };
34
+
35
+ debounced.clear = clear;
36
+
37
+ onCleanup(() => {
38
+ clear();
39
+ });
40
+
41
+ return debounced;
42
+ }
43
+
44
+ /**
45
+ * SolidJS reactive primitive for throttling function execution.
46
+ *
47
+ * @param fn Function to throttle.
48
+ * @param delay Interval in milliseconds (number or accessor).
49
+ */
50
+ export function createThrottle<T extends (...args: any[]) => any>(
51
+ fn: T,
52
+ delay: number | Accessor<number>
53
+ ): DebounceThrottleReturn<T> {
54
+ const getDelay = (): number => (typeof delay === "function" ? delay() : delay);
55
+ let lastCall = 0;
56
+ let timer: ReturnType<typeof setTimeout> | undefined;
57
+
58
+ const clear = () => {
59
+ if (timer) {
60
+ clearTimeout(timer);
61
+ timer = undefined;
62
+ }
63
+ };
64
+
65
+ const throttled = (...args: Parameters<T>) => {
66
+ const now = Date.now();
67
+ const interval = getDelay();
68
+ const remaining = interval - (now - lastCall);
69
+
70
+ if (remaining <= 0) {
71
+ clear();
72
+ lastCall = now;
73
+ fn(...args);
74
+ } else if (!timer) {
75
+ timer = setTimeout(() => {
76
+ lastCall = Date.now();
77
+ timer = undefined;
78
+ fn(...args);
79
+ }, remaining);
80
+ }
81
+ };
82
+
83
+ throttled.clear = clear;
84
+
85
+ onCleanup(() => {
86
+ clear();
87
+ });
88
+
89
+ return throttled;
90
+ }
@@ -0,0 +1,82 @@
1
+ import { createSignal, type Accessor } from "solid-js";
2
+
3
+ export interface CreateDisclosureOptions {
4
+ /** Uncontrolled initial open state. Defaults to false. */
5
+ defaultIsOpen?: boolean;
6
+ /** Controlled open state accessor */
7
+ isOpen?: boolean | Accessor<boolean | undefined>;
8
+ /** Callback triggered when state transitions to open */
9
+ onOpen?: () => void;
10
+ /** Callback triggered when state transitions to closed */
11
+ onClose?: () => void;
12
+ /** Callback triggered whenever open state changes */
13
+ onChange?: (isOpen: boolean) => void;
14
+ }
15
+
16
+ export interface CreateDisclosureReturn {
17
+ /** Signal accessor indicating if disclosure is open */
18
+ isOpen: Accessor<boolean>;
19
+ /** Function to open the disclosure */
20
+ open: () => void;
21
+ /** Function to close the disclosure */
22
+ close: () => void;
23
+ /** Function to toggle the disclosure open state */
24
+ toggle: () => void;
25
+ /** Setter function to programmatically set open state */
26
+ setOpen: (open: boolean) => void;
27
+ }
28
+
29
+ /**
30
+ * SolidJS reactive primitive for managing boolean disclosure (open/close) state with controlled and uncontrolled support.
31
+ *
32
+ * @param options Configuration options including initial state and callbacks.
33
+ */
34
+ export function createDisclosure(options: CreateDisclosureOptions = {}): CreateDisclosureReturn {
35
+ const [internalOpen, setInternalOpen] = createSignal<boolean>(
36
+ options.defaultIsOpen ?? false
37
+ );
38
+
39
+ const isControlled = () => {
40
+ if (typeof options.isOpen === "function") {
41
+ return options.isOpen() !== undefined;
42
+ }
43
+ return options.isOpen !== undefined;
44
+ };
45
+
46
+ const isOpen = (): boolean => {
47
+ if (typeof options.isOpen === "function") {
48
+ const val = options.isOpen();
49
+ return val !== undefined ? val : internalOpen();
50
+ }
51
+ return options.isOpen !== undefined ? options.isOpen : internalOpen();
52
+ };
53
+
54
+ const setOpenState = (nextState: boolean) => {
55
+ const currentState = isOpen();
56
+ if (currentState === nextState) return;
57
+
58
+ if (!isControlled()) {
59
+ setInternalOpen(nextState);
60
+ }
61
+
62
+ options.onChange?.(nextState);
63
+
64
+ if (nextState) {
65
+ options.onOpen?.();
66
+ } else {
67
+ options.onClose?.();
68
+ }
69
+ };
70
+
71
+ const open = () => setOpenState(true);
72
+ const close = () => setOpenState(false);
73
+ const toggle = () => setOpenState(!isOpen());
74
+
75
+ return {
76
+ isOpen,
77
+ open,
78
+ close,
79
+ toggle,
80
+ setOpen: setOpenState,
81
+ };
82
+ }