diff --git a/package.json b/package.json
index 5dbb602..7979c16 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
+ "test": "turbo run test",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"check-types": "turbo run check-types"
diff --git a/packages/tiny-react-hooks/src/index.ts b/packages/tiny-react-hooks/src/index.ts
index e69de29..d2b6108 100644
--- a/packages/tiny-react-hooks/src/index.ts
+++ b/packages/tiny-react-hooks/src/index.ts
@@ -0,0 +1 @@
+export * from './useDisclosure';
diff --git a/packages/tiny-react-hooks/src/useDisclosure/index.ts b/packages/tiny-react-hooks/src/useDisclosure/index.ts
new file mode 100644
index 0000000..e58ced5
--- /dev/null
+++ b/packages/tiny-react-hooks/src/useDisclosure/index.ts
@@ -0,0 +1 @@
+export * from './useDisclosure';
\ No newline at end of file
diff --git a/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.demo.tsx b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.demo.tsx
new file mode 100644
index 0000000..dbde126
--- /dev/null
+++ b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.demo.tsx
@@ -0,0 +1,21 @@
+import { useDisclosure } from './useDisclosure';
+
+export default function Component() {
+ const {
+ isOpen,
+ onClose,
+ onOpen,
+ onToggle,
+ } = useDisclosure();
+
+ return (
+ <>
+
+ Value is {isOpen.toString()}
+
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.md b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.md
new file mode 100644
index 0000000..289c2d4
--- /dev/null
+++ b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.md
@@ -0,0 +1,5 @@
+A simple hook to play with a boolean.
+Use cases:
+ - manage modal state
+ - manage popover state
+ - manage boolean value
diff --git a/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.test.ts b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.test.ts
new file mode 100644
index 0000000..ab7a4ee
--- /dev/null
+++ b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.test.ts
@@ -0,0 +1,105 @@
+import { act, renderHook } from '@testing-library/react';
+import { useDisclosure } from './useDisclosure';
+
+describe('useDisclosure()', () => {
+ it('should return correct value and methods', () => {
+ const { result } = renderHook(() => useDisclosure());
+
+ expect(result.current.isOpen).toBe(false);
+ expect(typeof result.current.isOpen).toBe('boolean');
+ expect(typeof result.current.onOpen).toBe('function');
+ expect(typeof result.current.onClose).toBe('function');
+ expect(typeof result.current.onToggle).toBe('function');
+ });
+
+
+ describe('with default value', () => {
+ describe('with correct default value as boolean', () => {
+ describe('should work with default value', () => {
+ it('should return isOpen with true', () => {
+ const { result } = renderHook(() => useDisclosure(true));
+ expect(result.current.isOpen).toBe(true);
+ });
+ it('should return isOpen with false', () => {
+ const { result } = renderHook(() => useDisclosure(false));
+ expect(result.current.isOpen).toBe(false);
+ });
+ it('should return isOpen with false when nothing is passed as argument', () => {
+ const { result } = renderHook(() => useDisclosure());
+ expect(result.current.isOpen).toBe(false);
+ });
+ });
+ });
+ describe('with incorrect default value type', () => {
+ it('should throw an error', () => {
+ const nonBoolean = '' as never;
+ vi.spyOn(console, 'error').mockImplementation(() => vi.fn());
+ expect(() => {
+ renderHook(() => useDisclosure(nonBoolean));
+ }).toThrowError('defaultValue must be a boolean value');
+ vi.resetAllMocks();
+ });
+ });
+ });
+
+ describe('onOpen', () => {
+ it('should set to true', () => {
+ const { result } = renderHook(() => useDisclosure(false));
+
+ act(() => {
+ result.current.onOpen();
+ });
+
+ expect(result.current.isOpen).toBe(true);
+ });
+ });
+
+ describe('onClose', () => {
+ it('should set to false', () => {
+ const { result } = renderHook(() => useDisclosure(true));
+
+ act(() => {
+ result.current.onClose();
+ });
+
+ expect(result.current.isOpen).toBe(false);
+ })
+ });
+
+ describe('onToggle', () => {
+ describe('toggle the value when false', () => {
+ it('should set to true', () => {
+ const { result } = renderHook(() => useDisclosure(false));
+ act(() => {
+ result.current.onToggle();
+ });
+
+ expect(result.current.isOpen).toBe(true);
+ });
+ });
+ describe('toggle the value when true', () => {
+ it('should set to false', () => {
+ const { result } = renderHook(() => useDisclosure(true));
+ act(() => {
+ result.current.onToggle();
+ });
+
+ expect(result.current.isOpen).toBe(false);
+ });
+ });
+ describe('toggle multiple time', () => {
+ it('should set to correct open state', () => {
+ const { result } = renderHook(() => useDisclosure(false));
+ act(() => {
+ result.current.onToggle(); // true
+ result.current.onToggle(); // false
+ result.current.onToggle(); // true
+ result.current.onToggle(); // false
+ result.current.onToggle(); // true -> result
+ });
+
+ expect(result.current.isOpen).toBe(true);
+ });
+ });
+ });
+});
diff --git a/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.ts b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.ts
new file mode 100644
index 0000000..1ff7267
--- /dev/null
+++ b/packages/tiny-react-hooks/src/useDisclosure/useDisclosure.ts
@@ -0,0 +1,43 @@
+import { useCallback, useState } from 'react';
+
+type UseDisclosureReturn = {
+ isOpen: boolean;
+ onOpen: () => void;
+ onClose: () => void;
+ onToggle: () => void;
+}
+
+/**
+ * Custom hook that handles boolean state with useful utility functions.
+ * @param {boolean} [defaultValue] - The initial value for the boolean state (default is `false`).
+ * @returns {UseDisclosureReturn} An object containing the boolean state value and utility functions to manipulate the state.
+ * @throws Will throw an error if `defaultValue` is an invalid boolean value.
+ * @public
+ * @example
+ * ```tsx
+ * const { isOpen, onOpen, onClose, onToggle } = UseDisclosureReturn(true);
+ * ```
+ */
+export function useDisclosure(defaultValue = false): UseDisclosureReturn {
+ if (typeof defaultValue !== 'boolean') throw new Error('defaultValue must be a boolean value');
+ const [isOpen, setOpen] = useState(defaultValue);
+
+ const onOpen = useCallback(() => {
+ setOpen(true);
+ }, []);
+
+ const onClose = useCallback(() => {
+ setOpen(false);
+ }, []);
+
+ const onToggle = useCallback(() => {
+ setOpen((isOpening) => !isOpening);
+ }, []);
+
+ return {
+ isOpen,
+ onOpen,
+ onClose,
+ onToggle,
+ };
+}
diff --git a/turbo.json b/turbo.json
index ebee3b2..0fb08a6 100644
--- a/turbo.json
+++ b/turbo.json
@@ -11,6 +11,10 @@
"dependsOn": [],
"cache": false
},
+ "test": {
+ "outputs": [],
+ "cache": false
+ },
"dev": {
"dependsOn": ["^build"],
"outputs": [],