@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20
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/components/button/button.cjs.js +1 -1
- package/dist/components/button/button.es.js +19 -18
- package/dist/components/button/components/base-button.cjs.js +1 -1
- package/dist/components/button/components/base-button.es.js +20 -20
- package/dist/components/calendar/calendar.cjs.js +1 -1
- package/dist/components/calendar/calendar.es.js +1 -0
- package/dist/components/date-picker/date-input.cjs.js +1 -1
- package/dist/components/date-picker/date-input.es.js +92 -75
- package/dist/components/date-picker/date-picker.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.es.js +104 -95
- package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.utils.es.js +51 -43
- package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
- package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
- package/dist/components/menu/menu.es.js +1 -9
- package/dist/components/otp/otp.cjs.js +2 -0
- package/dist/components/otp/otp.es.js +93 -0
- package/dist/components/password/password.cjs.js +1 -1
- package/dist/components/password/password.es.js +2 -2
- package/dist/components/select/select.cjs.js +1 -1
- package/dist/components/select/select.es.js +68 -60
- package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
- package/dist/hooks/internal/is-apple-device.es.js +9 -0
- package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
- package/dist/hooks/internal/use-latest-ref.es.js +11 -0
- package/dist/hooks/use-array/use-array.cjs.js +1 -1
- package/dist/hooks/use-array/use-array.es.js +54 -42
- package/dist/hooks/use-async/use-async.cjs.js +1 -1
- package/dist/hooks/use-async/use-async.es.js +53 -20
- package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
- package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
- package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
- package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
- package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
- package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
- package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
- package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
- package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
- package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
- package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
- package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
- package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
- package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
- package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
- package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
- package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
- package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
- package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
- package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
- package/dist/hooks/use-hover/use-hover.es.js +32 -17
- package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
- package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
- package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
- package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
- package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
- package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
- package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
- package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
- package/dist/hooks/use-object/use-object.cjs.js +1 -1
- package/dist/hooks/use-object/use-object.es.js +26 -22
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
- package/dist/hooks/use-step/use-step.cjs.js +1 -1
- package/dist/hooks/use-step/use-step.es.js +25 -24
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +308 -300
- package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
- package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/hooks/index.d.ts +2 -2
- package/dist/src/hooks/internal/index.d.ts +2 -0
- package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
- package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
- package/dist/src/hooks/use-array/use-array.d.ts +24 -11
- package/dist/src/hooks/use-async/use-async.d.ts +16 -13
- package/dist/src/hooks/use-boolean/index.d.ts +1 -0
- package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
- package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
- package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
- package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
- package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
- package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
- package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
- package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
- package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
- package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
- package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
- package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
- package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
- package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
- package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
- package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
- package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
- package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
- package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
- package/dist/src/hooks/use-object/use-object.d.ts +15 -6
- package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
- package/dist/src/hooks/use-step/use-step.d.ts +18 -11
- package/dist/src/utils/form.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
- package/src/components/auto-complete/auto-complete.test.tsx +4 -14
- package/src/components/avatar/avatar.test.tsx +7 -12
- package/src/components/button/button.test.tsx +10 -15
- package/src/components/button/button.tsx +14 -9
- package/src/components/button/components/base-button.tsx +2 -4
- package/src/components/calendar/calendar.test.tsx +12 -19
- package/src/components/calendar/calendar.tsx +4 -0
- package/src/components/card/card.test.tsx +4 -6
- package/src/components/checkbox/checkbox.test.tsx +12 -8
- package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
- package/src/components/combobox/combobox.test.tsx +24 -21
- package/src/components/date-picker/date-input-form.test.tsx +77 -0
- package/src/components/date-picker/date-input.stories.tsx +30 -18
- package/src/components/date-picker/date-input.tsx +77 -44
- package/src/components/date-picker/date-picker.stories.tsx +31 -1
- package/src/components/date-picker/date-picker.test.tsx +3 -13
- package/src/components/date-picker/date-picker.tsx +35 -16
- package/src/components/date-picker/date-picker.utils.test.ts +32 -14
- package/src/components/date-picker/date-picker.utils.ts +33 -0
- package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
- package/src/components/date-picker/use-hidden-field-value.ts +23 -0
- package/src/components/dialog/dialog.test.tsx +10 -8
- package/src/components/dropzone/dropzone.test.tsx +11 -13
- package/src/components/empty/empty.test.tsx +4 -3
- package/src/components/field/field.test.tsx +12 -13
- package/src/components/form/form.stories.tsx +16 -1
- package/src/components/index.ts +1 -0
- package/src/components/label/label.test.tsx +3 -3
- package/src/components/menu/menu.tsx +1 -5
- package/src/components/number-input/number-input.test.tsx +6 -2
- package/src/components/password/password.test.tsx +20 -6
- package/src/components/password/password.tsx +2 -2
- package/src/components/popover/popover.test.tsx +4 -4
- package/src/components/progress/progress.test.tsx +7 -8
- package/src/components/radio-group/radio-group.test.tsx +17 -11
- package/src/components/select/select.test.tsx +10 -10
- package/src/components/select/select.tsx +9 -1
- package/src/components/stepper/stepper.stories.tsx +11 -15
- package/src/components/stepper/stepper.test.tsx +6 -4
- package/src/components/switch/switch.test.tsx +3 -3
- package/src/components/table/table.test.tsx +9 -3
- package/src/components/tabs/tabs.test.tsx +6 -2
- package/src/components/tag/tag.test.tsx +1 -3
- package/src/components/textarea/textarea.test.tsx +4 -1
- package/src/components/timeline/timeline.test.tsx +10 -5
- package/src/components/toast/toast.test.tsx +11 -14
- package/src/components/tooltip/tooltip.test.tsx +1 -5
- package/src/components/tree/tree.test.tsx +3 -1
- package/src/hooks/index.ts +2 -2
- package/src/hooks/internal/index.ts +2 -0
- package/src/hooks/internal/is-apple-device.test.ts +41 -0
- package/src/hooks/internal/is-apple-device.ts +33 -0
- package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
- package/src/hooks/internal/use-latest-ref.ts +21 -0
- package/src/hooks/use-array/use-array.stories.tsx +435 -64
- package/src/hooks/use-array/use-array.test.tsx +398 -15
- package/src/hooks/use-array/use-array.ts +105 -66
- package/src/hooks/use-async/use-async.stories.tsx +255 -131
- package/src/hooks/use-async/use-async.test.ts +397 -0
- package/src/hooks/use-async/use-async.ts +117 -39
- package/src/hooks/use-boolean/index.ts +1 -0
- package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
- package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
- package/src/hooks/use-boolean/use-boolean.ts +50 -0
- package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
- package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
- package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
- package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
- package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
- package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
- package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
- package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
- package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
- package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
- package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
- package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
- package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
- package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
- package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
- package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
- package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
- package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
- package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
- package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
- package/src/hooks/use-hotkey/index.ts +9 -1
- package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
- package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
- package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
- package/src/hooks/use-hotkey/utils/index.ts +8 -3
- package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
- package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
- package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
- package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
- package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
- package/src/hooks/use-hover/use-hover.test.tsx +266 -26
- package/src/hooks/use-hover/use-hover.tsx +93 -28
- package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
- package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
- package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
- package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
- package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
- package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
- package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
- package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
- package/src/hooks/use-media-query/use-media-query.ts +39 -14
- package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
- package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
- package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
- package/src/hooks/use-mutation/use-mutation.ts +97 -0
- package/src/hooks/use-object/use-object.stories.tsx +310 -79
- package/src/hooks/use-object/use-object.test.tsx +235 -56
- package/src/hooks/use-object/use-object.ts +59 -0
- package/src/hooks/use-pagination/use-pagination.tsx +0 -1
- package/src/hooks/use-prevent-page-close/index.ts +1 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
- package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
- package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
- package/src/hooks/use-selection/use-selection.ts +0 -1
- package/src/hooks/use-step/use-step.stories.tsx +178 -65
- package/src/hooks/use-step/use-step.test.ts +178 -53
- package/src/hooks/use-step/use-step.ts +57 -49
- package/src/utils/form.test.tsx +13 -8
- package/src/utils/form.tsx +10 -0
- package/src/utils/functions/getFormData.test.ts +1 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
- package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
- package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
- package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
- package/dist/src/hooks/use-toggle/index.d.ts +0 -1
- package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
- package/src/hooks/use-async/use-async.test.tsx +0 -68
- package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
- package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
- package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
- package/src/hooks/use-mutation/use-mutation.tsx +0 -59
- package/src/hooks/use-object/use-object.tsx +0 -46
- package/src/hooks/use-prevent-close-window/index.ts +0 -1
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
- package/src/hooks/use-toggle/index.ts +0 -1
- package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
- package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
- package/src/hooks/use-toggle/use-toggle.ts +0 -14
- /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
- /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useBoolean } from "./use-boolean";
|
|
4
|
+
|
|
5
|
+
// ─── Shared UI helpers ─────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const JsonPanel = ({ value }: { value: unknown }) => (
|
|
8
|
+
<pre className="rounded-md bg-slate-950 p-3 text-xs text-white leading-relaxed overflow-auto max-h-48">
|
|
9
|
+
{JSON.stringify(value, null, 2)}
|
|
10
|
+
</pre>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const LogPanel = ({ entries }: { entries: string[] }) =>
|
|
14
|
+
entries.length > 0 ? (
|
|
15
|
+
<div className="rounded-md border bg-muted p-3 text-xs font-mono space-y-0.5">
|
|
16
|
+
{entries.map((entry, i) => (
|
|
17
|
+
<div key={i}>{entry}</div>
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
) : (
|
|
21
|
+
<div className="rounded-md border bg-muted p-3 text-xs font-mono text-muted-foreground">
|
|
22
|
+
No events yet — log is empty until the first mutation (proves onChange
|
|
23
|
+
does not fire on mount).
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const timestamp = () => new Date().toISOString().slice(11, 23);
|
|
28
|
+
|
|
29
|
+
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const meta: Meta = {
|
|
32
|
+
title: "hooks/useBoolean",
|
|
33
|
+
parameters: {
|
|
34
|
+
docs: {
|
|
35
|
+
description: {
|
|
36
|
+
component: `
|
|
37
|
+
\`useBoolean\` manages a boolean value with a stable set of typed actions.
|
|
38
|
+
|
|
39
|
+
**Key behaviors**
|
|
40
|
+
- All 4 action references (\`toggle\`, \`setTrue\`, \`setFalse\`, \`set\`) are **stable across renders** — built once in \`useMemo([setState])\`. Safe to pass as props or include in dependency arrays.
|
|
41
|
+
- \`toggle()\` flips the current value using a functional state updater — safe under React batching.
|
|
42
|
+
- \`setTrue()\` / \`setFalse()\` set an explicit value; if the value is already at the target, React **bails out** with no re-render and no \`onChange\` fire.
|
|
43
|
+
- \`set(value)\` sets the value explicitly — same bail-out semantics as \`setTrue\`/\`setFalse\`.
|
|
44
|
+
- \`onChange\` fires after every real state transition but **not on initial mount**. Callback is read through \`useLatestRef\` — safe to pass inline.
|
|
45
|
+
|
|
46
|
+
\`\`\`ts
|
|
47
|
+
const [value, { toggle, setTrue, setFalse, set }] = useBoolean(initialValue?, { onChange? });
|
|
48
|
+
\`\`\`
|
|
49
|
+
`.trim(),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
argTypes: {
|
|
54
|
+
initialValue: {
|
|
55
|
+
description:
|
|
56
|
+
"The initial boolean value. Defaults to `false` when omitted.",
|
|
57
|
+
table: {
|
|
58
|
+
category: "Hook options",
|
|
59
|
+
type: { summary: "boolean" },
|
|
60
|
+
defaultValue: { summary: "false" },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
onChange: {
|
|
64
|
+
description:
|
|
65
|
+
"Called after every state change. NOT called on initial mount. Callback is read through `useLatestRef` — safe to pass as an inline function.",
|
|
66
|
+
table: {
|
|
67
|
+
category: "Hook options",
|
|
68
|
+
type: { summary: "(value: boolean) => void" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default meta;
|
|
75
|
+
type Story = StoryObj;
|
|
76
|
+
|
|
77
|
+
// ─── Story 1 — Default ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export const Default: Story = {
|
|
80
|
+
render: () => {
|
|
81
|
+
const [value, { toggle, setTrue, setFalse, set }] = useBoolean(false);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex flex-col gap-4 max-w-sm">
|
|
85
|
+
<div
|
|
86
|
+
className={`flex items-center justify-center rounded-lg border px-6 py-4 text-sm font-semibold transition-colors ${
|
|
87
|
+
value
|
|
88
|
+
? "border-green-500 bg-green-50 text-green-700"
|
|
89
|
+
: "border-slate-300 bg-slate-50 text-slate-500"
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
<span
|
|
93
|
+
className={`mr-2 h-2.5 w-2.5 rounded-full ${value ? "bg-green-500" : "bg-slate-300"}`}
|
|
94
|
+
/>
|
|
95
|
+
{value ? "ON" : "OFF"}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="flex gap-2 flex-wrap">
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={toggle}
|
|
102
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
103
|
+
>
|
|
104
|
+
Toggle
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={setTrue}
|
|
109
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
110
|
+
>
|
|
111
|
+
Set True
|
|
112
|
+
</button>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={setFalse}
|
|
116
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
117
|
+
>
|
|
118
|
+
Set False
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => set(true)}
|
|
123
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
124
|
+
>
|
|
125
|
+
set(true)
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<JsonPanel value={value} />
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
parameters: {
|
|
134
|
+
docs: {
|
|
135
|
+
description: {
|
|
136
|
+
story:
|
|
137
|
+
"Basic toggle with a visual ON/OFF indicator. The colored badge reflects the current state. All 4 actions are wired to buttons — try toggling and observe the state update in the JSON panel.",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ─── Story 2 — AllActions ─────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export const AllActions: Story = {
|
|
146
|
+
render: () => {
|
|
147
|
+
const [value, { toggle, setTrue, setFalse, set }] = useBoolean(false);
|
|
148
|
+
|
|
149
|
+
const actions = [
|
|
150
|
+
{
|
|
151
|
+
label: "toggle()",
|
|
152
|
+
desc: "Flips the current value (false → true → false)",
|
|
153
|
+
fn: toggle,
|
|
154
|
+
color: "border-blue-300 hover:bg-blue-50",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: "setTrue()",
|
|
158
|
+
desc: "Forces value to true — no-op if already true",
|
|
159
|
+
fn: setTrue,
|
|
160
|
+
color: "border-green-300 hover:bg-green-50",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
label: "setFalse()",
|
|
164
|
+
desc: "Forces value to false — no-op if already false",
|
|
165
|
+
fn: setFalse,
|
|
166
|
+
color: "border-red-300 hover:bg-red-50",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
label: "set(true)",
|
|
170
|
+
desc: "Explicit set — same bail-out semantics as setTrue",
|
|
171
|
+
fn: () => set(true),
|
|
172
|
+
color: "border-amber-300 hover:bg-amber-50",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<div className="flex flex-col gap-4 max-w-sm">
|
|
178
|
+
<div className="grid grid-cols-2 gap-2">
|
|
179
|
+
{actions.map(({ label, desc, fn, color }) => (
|
|
180
|
+
<button
|
|
181
|
+
key={label}
|
|
182
|
+
type="button"
|
|
183
|
+
onClick={fn}
|
|
184
|
+
className={`rounded-md border p-3 text-left transition-colors ${color}`}
|
|
185
|
+
>
|
|
186
|
+
<p className="text-sm font-mono font-medium">{label}</p>
|
|
187
|
+
<p className="text-xs text-muted-foreground mt-0.5">{desc}</p>
|
|
188
|
+
</button>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div
|
|
193
|
+
className={`rounded-lg border px-4 py-3 text-center text-lg font-bold transition-colors ${
|
|
194
|
+
value
|
|
195
|
+
? "border-green-400 bg-green-50 text-green-700"
|
|
196
|
+
: "border-slate-300 bg-slate-50 text-slate-500"
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{String(value)}
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<JsonPanel value={value} />
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
parameters: {
|
|
207
|
+
docs: {
|
|
208
|
+
description: {
|
|
209
|
+
story:
|
|
210
|
+
"Each action is presented as a labeled card with its description. `toggle()` always flips. `setTrue()` and `setFalse()` are idempotent — clicking them repeatedly when the value is already at the target causes no re-render. `set(value)` gives explicit control.",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// ─── Story 3 — WithOnChange ────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
export const WithOnChange: Story = {
|
|
219
|
+
render: () => {
|
|
220
|
+
const [log, setLog] = useState<string[]>([]);
|
|
221
|
+
|
|
222
|
+
const addLog = (value: boolean) =>
|
|
223
|
+
setLog((prev) => [
|
|
224
|
+
`[${timestamp()}] onChange → ${String(value)}`,
|
|
225
|
+
...prev,
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const [value, { toggle, setTrue, setFalse, set }] = useBoolean(false, {
|
|
229
|
+
onChange: addLog,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<div className="flex flex-col gap-4 max-w-sm">
|
|
234
|
+
<p className="text-xs text-muted-foreground">
|
|
235
|
+
The log below starts empty — proving <code>onChange</code> does not
|
|
236
|
+
fire on initial mount. No-op calls (e.g. <code>setTrue()</code> when
|
|
237
|
+
already <code>true</code>) also produce no log entry.
|
|
238
|
+
</p>
|
|
239
|
+
|
|
240
|
+
<div className="flex gap-2 flex-wrap">
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={toggle}
|
|
244
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
245
|
+
>
|
|
246
|
+
Toggle
|
|
247
|
+
</button>
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
onClick={setTrue}
|
|
251
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
252
|
+
>
|
|
253
|
+
setTrue
|
|
254
|
+
</button>
|
|
255
|
+
<button
|
|
256
|
+
type="button"
|
|
257
|
+
onClick={setFalse}
|
|
258
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
259
|
+
>
|
|
260
|
+
setFalse
|
|
261
|
+
</button>
|
|
262
|
+
<button
|
|
263
|
+
type="button"
|
|
264
|
+
onClick={() => set(!value)}
|
|
265
|
+
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent transition-colors"
|
|
266
|
+
>
|
|
267
|
+
set(!value)
|
|
268
|
+
</button>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onClick={() => setLog([])}
|
|
272
|
+
className="rounded-md border px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent transition-colors"
|
|
273
|
+
>
|
|
274
|
+
Clear log
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<JsonPanel value={value} />
|
|
279
|
+
<LogPanel entries={log} />
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
parameters: {
|
|
284
|
+
docs: {
|
|
285
|
+
description: {
|
|
286
|
+
story:
|
|
287
|
+
"Wires `onChange` to a visible log panel. The log is empty on mount (no initial fire). No-op actions — like `setTrue()` when already `true` — produce no log entry because React bails out of the commit and the `useEffect([state])` does not re-run.",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// ─── Story 4 — RealWorld ──────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
export const RealWorld: Story = {
|
|
296
|
+
render: () => {
|
|
297
|
+
const [showPassword, { toggle }] = useBoolean(false);
|
|
298
|
+
const [inputValue, setInputValue] = useState("");
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<div className="flex flex-col gap-4 max-w-sm">
|
|
302
|
+
<p className="text-sm text-muted-foreground">
|
|
303
|
+
Password field with visibility toggle — mirrors the{" "}
|
|
304
|
+
<code>Password</code> component usage of <code>useBoolean</code>.
|
|
305
|
+
</p>
|
|
306
|
+
|
|
307
|
+
<div className="relative inline-flex h-9 w-full items-center rounded-md border border-input bg-background text-sm transition-shadow focus-within:border-ring">
|
|
308
|
+
<input
|
|
309
|
+
type={showPassword ? "text" : "password"}
|
|
310
|
+
value={inputValue}
|
|
311
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
312
|
+
placeholder="Enter password…"
|
|
313
|
+
className="min-w-0 flex-1 bg-transparent pl-3 outline-none placeholder:text-muted-foreground"
|
|
314
|
+
/>
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
onClick={toggle}
|
|
318
|
+
className="flex shrink-0 items-center px-2.5 text-muted-foreground hover:text-foreground transition-colors"
|
|
319
|
+
aria-label={showPassword ? "Hide password" : "Show password"}
|
|
320
|
+
>
|
|
321
|
+
{showPassword ? (
|
|
322
|
+
<svg
|
|
323
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
324
|
+
width="16"
|
|
325
|
+
height="16"
|
|
326
|
+
viewBox="0 0 24 24"
|
|
327
|
+
fill="none"
|
|
328
|
+
stroke="currentColor"
|
|
329
|
+
strokeWidth="2"
|
|
330
|
+
strokeLinecap="round"
|
|
331
|
+
strokeLinejoin="round"
|
|
332
|
+
>
|
|
333
|
+
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" />
|
|
334
|
+
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" />
|
|
335
|
+
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" />
|
|
336
|
+
<line x1="2" x2="22" y1="2" y2="22" />
|
|
337
|
+
</svg>
|
|
338
|
+
) : (
|
|
339
|
+
<svg
|
|
340
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
341
|
+
width="16"
|
|
342
|
+
height="16"
|
|
343
|
+
viewBox="0 0 24 24"
|
|
344
|
+
fill="none"
|
|
345
|
+
stroke="currentColor"
|
|
346
|
+
strokeWidth="2"
|
|
347
|
+
strokeLinecap="round"
|
|
348
|
+
strokeLinejoin="round"
|
|
349
|
+
>
|
|
350
|
+
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
|
|
351
|
+
<circle cx="12" cy="12" r="3" />
|
|
352
|
+
</svg>
|
|
353
|
+
)}
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div
|
|
358
|
+
className={`rounded-md border px-3 py-2 text-xs font-mono transition-colors ${
|
|
359
|
+
showPassword
|
|
360
|
+
? "border-amber-300 bg-amber-50 text-amber-700"
|
|
361
|
+
: "border-slate-200 bg-slate-50 text-slate-500"
|
|
362
|
+
}`}
|
|
363
|
+
>
|
|
364
|
+
Input type: <strong>{showPassword ? "text" : "password"}</strong>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
parameters: {
|
|
370
|
+
docs: {
|
|
371
|
+
description: {
|
|
372
|
+
story:
|
|
373
|
+
"Realistic password visibility toggle — the exact pattern used inside the `Password` component. `useBoolean(false)` drives the input type between `password` and `text`. The eye icon and state indicator update on each toggle.",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { useBoolean } from "./use-boolean";
|
|
4
|
+
|
|
5
|
+
describe("useBoolean", () => {
|
|
6
|
+
// ─── initialization ─────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
describe("initialization", () => {
|
|
9
|
+
it("default value is false when no initialValue is provided", () => {
|
|
10
|
+
const { result } = renderHook(() => useBoolean());
|
|
11
|
+
expect(result.current[0]).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("initialValue true is honored", () => {
|
|
15
|
+
const { result } = renderHook(() => useBoolean(true));
|
|
16
|
+
expect(result.current[0]).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// ─── toggle ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("toggle", () => {
|
|
23
|
+
it("flips false → true", () => {
|
|
24
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
25
|
+
act(() => result.current[1].toggle());
|
|
26
|
+
expect(result.current[0]).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("double toggle returns to original value", () => {
|
|
30
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
31
|
+
act(() => {
|
|
32
|
+
result.current[1].toggle();
|
|
33
|
+
result.current[1].toggle();
|
|
34
|
+
});
|
|
35
|
+
expect(result.current[0]).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── setTrue ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe("setTrue", () => {
|
|
42
|
+
it("sets to true from false", () => {
|
|
43
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
44
|
+
act(() => result.current[1].setTrue());
|
|
45
|
+
expect(result.current[0]).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("repeated setTrue does not throw and value stays true", () => {
|
|
49
|
+
const { result } = renderHook(() => useBoolean(true));
|
|
50
|
+
expect(() => {
|
|
51
|
+
act(() => result.current[1].setTrue());
|
|
52
|
+
}).not.toThrow();
|
|
53
|
+
expect(result.current[0]).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── setFalse ────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe("setFalse", () => {
|
|
60
|
+
it("sets to false from true", () => {
|
|
61
|
+
const { result } = renderHook(() => useBoolean(true));
|
|
62
|
+
act(() => result.current[1].setFalse());
|
|
63
|
+
expect(result.current[0]).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("repeated setFalse does not throw and value stays false", () => {
|
|
67
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
68
|
+
expect(() => {
|
|
69
|
+
act(() => result.current[1].setFalse());
|
|
70
|
+
}).not.toThrow();
|
|
71
|
+
expect(result.current[0]).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── set ─────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe("set", () => {
|
|
78
|
+
it("set(true) from false", () => {
|
|
79
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
80
|
+
act(() => result.current[1].set(true));
|
|
81
|
+
expect(result.current[0]).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("set(false) from true", () => {
|
|
85
|
+
const { result } = renderHook(() => useBoolean(true));
|
|
86
|
+
act(() => result.current[1].set(false));
|
|
87
|
+
expect(result.current[0]).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("set to same value does not throw", () => {
|
|
91
|
+
const { result } = renderHook(() => useBoolean(true));
|
|
92
|
+
expect(() => {
|
|
93
|
+
act(() => result.current[1].set(true));
|
|
94
|
+
}).not.toThrow();
|
|
95
|
+
expect(result.current[0]).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─── action stability ─────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("action stability", () => {
|
|
102
|
+
it("actions object reference is stable after toggle (re-render)", () => {
|
|
103
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
104
|
+
const actionsBefore = result.current[1];
|
|
105
|
+
act(() => result.current[1].toggle());
|
|
106
|
+
expect(result.current[1]).toBe(actionsBefore);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("all 4 fn refs are stable across re-renders", () => {
|
|
110
|
+
const { result } = renderHook(() => useBoolean(false));
|
|
111
|
+
const { toggle, setTrue, setFalse, set } = result.current[1];
|
|
112
|
+
act(() => result.current[1].toggle());
|
|
113
|
+
expect(result.current[1].toggle).toBe(toggle);
|
|
114
|
+
expect(result.current[1].setTrue).toBe(setTrue);
|
|
115
|
+
expect(result.current[1].setFalse).toBe(setFalse);
|
|
116
|
+
expect(result.current[1].set).toBe(set);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── onChange ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("onChange", () => {
|
|
123
|
+
it("not fired on initial mount", () => {
|
|
124
|
+
const onChange = vi.fn();
|
|
125
|
+
renderHook(() => useBoolean(false, { onChange }));
|
|
126
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("fired after toggle with the new value", () => {
|
|
130
|
+
const onChange = vi.fn();
|
|
131
|
+
const { result } = renderHook(() => useBoolean(false, { onChange }));
|
|
132
|
+
act(() => result.current[1].toggle());
|
|
133
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(onChange).toHaveBeenCalledWith(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("fired after setTrue", () => {
|
|
138
|
+
const onChange = vi.fn();
|
|
139
|
+
const { result } = renderHook(() => useBoolean(false, { onChange }));
|
|
140
|
+
act(() => result.current[1].setTrue());
|
|
141
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(onChange).toHaveBeenCalledWith(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("not fired when setTrue is a no-op (already true)", () => {
|
|
146
|
+
const onChange = vi.fn();
|
|
147
|
+
const { result } = renderHook(() => useBoolean(true, { onChange }));
|
|
148
|
+
act(() => result.current[1].setTrue());
|
|
149
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("fired after set(false) from true", () => {
|
|
153
|
+
const onChange = vi.fn();
|
|
154
|
+
const { result } = renderHook(() => useBoolean(true, { onChange }));
|
|
155
|
+
act(() => result.current[1].set(false));
|
|
156
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(onChange).toHaveBeenCalledWith(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ─── edge cases ───────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
describe("edge cases", () => {
|
|
164
|
+
it("initialValue=true, toggle → false", () => {
|
|
165
|
+
const { result } = renderHook(() => useBoolean(true));
|
|
166
|
+
act(() => result.current[1].toggle());
|
|
167
|
+
expect(result.current[0]).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("set(true) when already true → onChange NOT called", () => {
|
|
171
|
+
const onChange = vi.fn();
|
|
172
|
+
const { result } = renderHook(() => useBoolean(true, { onChange }));
|
|
173
|
+
act(() => result.current[1].set(true));
|
|
174
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useLatestRef } from "../internal/use-latest-ref";
|
|
3
|
+
|
|
4
|
+
export interface UseBooleanOptions {
|
|
5
|
+
/** Called after every value change. NOT called on initial mount. */
|
|
6
|
+
onChange?: (value: boolean) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseBooleanActions {
|
|
10
|
+
/** Flip the current value. */
|
|
11
|
+
toggle(): void;
|
|
12
|
+
/** Force the value to true. */
|
|
13
|
+
setTrue(): void;
|
|
14
|
+
/** Force the value to false. */
|
|
15
|
+
setFalse(): void;
|
|
16
|
+
/** Set the value explicitly. */
|
|
17
|
+
set(value: boolean): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useBoolean(
|
|
21
|
+
initialValue?: boolean,
|
|
22
|
+
options?: UseBooleanOptions,
|
|
23
|
+
): readonly [boolean, UseBooleanActions] {
|
|
24
|
+
const [state, setState] = useState<boolean>(initialValue ?? false);
|
|
25
|
+
|
|
26
|
+
// Always the latest handler — never a memo/effect dep.
|
|
27
|
+
const onChangeRef = useLatestRef(options?.onChange);
|
|
28
|
+
|
|
29
|
+
// Fire onChange on every change EXCEPT the initial mount.
|
|
30
|
+
const mountedRef = useRef(false);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!mountedRef.current) {
|
|
33
|
+
mountedRef.current = true;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onChangeRef.current?.(state);
|
|
37
|
+
}, [state]);
|
|
38
|
+
|
|
39
|
+
const actions = useMemo<UseBooleanActions>(
|
|
40
|
+
() => ({
|
|
41
|
+
toggle: () => setState((prev) => !prev),
|
|
42
|
+
setTrue: () => setState(true),
|
|
43
|
+
setFalse: () => setState(false),
|
|
44
|
+
set: (value) => setState(value),
|
|
45
|
+
}),
|
|
46
|
+
[setState],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return [state, actions] as const;
|
|
50
|
+
}
|