From 169562852d4305c123aa7316cad5238e2bf575c6 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:23:54 +0700 Subject: [PATCH 1/8] feat: add useDisclosure implement --- .../src/useDisclosure/useDisclosure.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/tiny-react-hooks/src/useDisclosure/useDisclosure.ts 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, + }; +} From 51be643c2bc16650c99d083c2f01badcf1645218 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:24:54 +0700 Subject: [PATCH 2/8] test: add test cases for useDisclosure hook --- .../src/useDisclosure/useDisclosure.test.ts | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/tiny-react-hooks/src/useDisclosure/useDisclosure.test.ts 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); + }); + }); + }); +}); From ff5389a2b66cbf1e8a4653b1c9bbc2bd2cc24f76 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:25:36 +0700 Subject: [PATCH 3/8] feat: export hook --- packages/tiny-react-hooks/src/useDisclosure/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tiny-react-hooks/src/useDisclosure/index.tsx b/packages/tiny-react-hooks/src/useDisclosure/index.tsx index e69de29..e58ced5 100644 --- a/packages/tiny-react-hooks/src/useDisclosure/index.tsx +++ b/packages/tiny-react-hooks/src/useDisclosure/index.tsx @@ -0,0 +1 @@ +export * from './useDisclosure'; \ No newline at end of file From bc034d26f03e316ebf89d70c024d7e625db96fe6 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:25:47 +0700 Subject: [PATCH 4/8] docs: add docs for useDisclosure hook --- .../src/useDisclosure/useDisclosure.demo.tsx | 21 +++++++++++++++++++ .../src/useDisclosure/useDisclosure.md | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 packages/tiny-react-hooks/src/useDisclosure/useDisclosure.demo.tsx create mode 100644 packages/tiny-react-hooks/src/useDisclosure/useDisclosure.md 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 From e22b0f2d5826bdea827c2d6aa353b318264ec2b1 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:27:53 +0700 Subject: [PATCH 5/8] feat: export hook --- packages/tiny-react-hooks/src/index.ts | 1 + 1 file changed, 1 insertion(+) 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'; From bd7c1b60fd2e08cdc5ff4508e6882cbf2c035a57 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:40:23 +0700 Subject: [PATCH 6/8] fix: fix build fail --- packages/tiny-react-hooks/src/useDisclosure/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/tiny-react-hooks/src/useDisclosure/index.ts 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 From 83ffcb63b880fbe069d2289703d9a4822c606ea5 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:41:34 +0700 Subject: [PATCH 7/8] fix: fix build fail --- package.json | 1 + 1 file changed, 1 insertion(+) 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" From 9ff1c65c0a109dc9c8d34bbec40204c0af18f4f9 Mon Sep 17 00:00:00 2001 From: Tuan Anh Date: Wed, 16 Apr 2025 11:42:59 +0700 Subject: [PATCH 8/8] fix: add test to turbo --- turbo.json | 4 ++++ 1 file changed, 4 insertions(+) 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": [],