@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
|
@@ -1,97 +1,210 @@
|
|
|
1
|
-
import { Meta } from "@storybook/react-vite";
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { Button } from "../../components";
|
|
3
3
|
import { cn } from "../../lib";
|
|
4
|
-
import { useStep } from "
|
|
4
|
+
import { useStep } from "./use-step";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Meta
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
11
|
+
* `useStep` manages a numeric step index with bounded navigation.
|
|
12
|
+
*
|
|
13
|
+
* It owns **position only** — consumers index their own data array with the
|
|
14
|
+
* returned `index`. Always starts at index 0. All action callbacks are
|
|
15
|
+
* referentially stable after mount.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const [index, { next, back, goTo, reset, canGoNext, canGoBack, isFirstStep, isLastStep, progress }] =
|
|
19
|
+
* useStep(4)
|
|
20
|
+
* ```
|
|
8
21
|
*/
|
|
9
22
|
const meta: Meta = {
|
|
10
|
-
title: "
|
|
23
|
+
title: "Hooks/useStep",
|
|
11
24
|
tags: ["autodocs"],
|
|
25
|
+
parameters: {
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component: `
|
|
29
|
+
\`useStep\` manages a numeric step index with bounded navigation.
|
|
30
|
+
|
|
31
|
+
It owns **position only** — consumers index their own data array with the returned \`index\`.
|
|
32
|
+
Always starts at index 0. All action callbacks (\`next\`, \`back\`, \`goTo\`, \`reset\`) are referentially stable after mount.
|
|
33
|
+
|
|
34
|
+
\`\`\`ts
|
|
35
|
+
const [index, { next, back, goTo, reset, canGoNext, canGoBack, isFirstStep, isLastStep, progress }] =
|
|
36
|
+
useStep(4)
|
|
37
|
+
\`\`\`
|
|
38
|
+
`.trim(),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
argTypes: {
|
|
43
|
+
count: {
|
|
44
|
+
control: { type: "number", min: 1, step: 1 },
|
|
45
|
+
description: "Total number of steps. Source of truth for boundaries.",
|
|
46
|
+
table: { defaultValue: { summary: "—" } },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
args: {
|
|
50
|
+
count: 4,
|
|
51
|
+
},
|
|
12
52
|
};
|
|
13
53
|
|
|
14
54
|
export default meta;
|
|
15
55
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
type Story = StoryObj<typeof meta>;
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Stories
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
// ── Default ───────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Basic uncontrolled usage. `useStep` starts at index 0. Back and Next buttons
|
|
66
|
+
* navigate through the steps; the current position is shown as "Step X of N".
|
|
67
|
+
*/
|
|
68
|
+
export const Default: Story = {
|
|
69
|
+
parameters: {
|
|
70
|
+
docs: {
|
|
71
|
+
description: {
|
|
72
|
+
story:
|
|
73
|
+
"Basic uncontrolled usage. The hook starts at index 0 and manages its own state. Click Back / Next to navigate.",
|
|
22
74
|
},
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
render: () => {
|
|
78
|
+
const [index, { next, back, isFirstStep, isLastStep }] = useStep(4);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex flex-col gap-4 w-72">
|
|
82
|
+
<div className="flex h-20 items-center justify-center rounded-lg border-2 border-dashed text-lg font-semibold">
|
|
83
|
+
Step {index + 1} of 4
|
|
84
|
+
</div>
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<Button variant="outline" onClick={back} disabled={isFirstStep}>
|
|
87
|
+
Back
|
|
88
|
+
</Button>
|
|
89
|
+
<Button onClick={next} disabled={isLastStep}>
|
|
90
|
+
Next
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ── WithProgress ──────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Shows the `progress` ratio (0–1) as a visual progress bar.
|
|
102
|
+
* `progress` equals `index / (count - 1)` and is 0 when `count <= 1`.
|
|
103
|
+
*/
|
|
104
|
+
export const WithProgress: Story = {
|
|
105
|
+
parameters: {
|
|
106
|
+
docs: {
|
|
107
|
+
description: {
|
|
108
|
+
story:
|
|
109
|
+
"`progress` is a ratio in `[0, 1]` equal to `index / (count - 1)`. Drive a progress bar width directly: `progress * 100 + '%'`.",
|
|
26
110
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
render: () => {
|
|
114
|
+
const [index, { next, back, progress, isFirstStep, isLastStep }] =
|
|
115
|
+
useStep(5);
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex flex-col gap-4 w-72">
|
|
119
|
+
{/* Progress bar */}
|
|
120
|
+
<div className="w-full h-2 rounded-full bg-muted overflow-hidden">
|
|
121
|
+
<div
|
|
122
|
+
className="h-full rounded-full bg-primary transition-all duration-300"
|
|
123
|
+
style={{ width: `${progress * 100}%` }}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
127
|
+
Step {index + 1} of 5 — progress: {progress.toFixed(2)}
|
|
128
|
+
</p>
|
|
129
|
+
<div className="flex gap-2">
|
|
130
|
+
<Button variant="outline" onClick={back} disabled={isFirstStep}>
|
|
131
|
+
Back
|
|
132
|
+
</Button>
|
|
133
|
+
<Button onClick={next} disabled={isLastStep}>
|
|
134
|
+
Next
|
|
135
|
+
</Button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// ── BoundaryBehavior ──────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Demonstrates `canGoNext` and `canGoBack` driving button disabled state, plus
|
|
146
|
+
* dot indicators calling `goTo(i)` to jump to a specific step directly.
|
|
147
|
+
*/
|
|
148
|
+
export const BoundaryBehavior: Story = {
|
|
149
|
+
parameters: {
|
|
150
|
+
docs: {
|
|
151
|
+
description: {
|
|
152
|
+
story:
|
|
153
|
+
"`canGoNext` and `canGoBack` are derived from `index` and `count`. Use them to disable navigation controls at the boundaries. Dot indicators call `goTo(i)` for direct jumps.",
|
|
30
154
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
goTo,
|
|
37
|
-
step,
|
|
38
|
-
isLastStep,
|
|
39
|
-
isFirstStep,
|
|
40
|
-
currentStepIndex,
|
|
41
|
-
} = useStep(data);
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
render: () => {
|
|
158
|
+
const count = 5;
|
|
159
|
+
const [index, { next, back, goTo, canGoNext, canGoBack }] = useStep(count);
|
|
42
160
|
|
|
43
161
|
return (
|
|
44
|
-
<div>
|
|
45
|
-
<div className="flex
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
162
|
+
<div className="flex flex-col gap-4 w-72">
|
|
163
|
+
<div className="flex h-20 items-center justify-center rounded-lg border-2 border-dashed text-lg font-semibold">
|
|
164
|
+
Step {index + 1} of {count}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Dot indicators */}
|
|
168
|
+
<div className="flex justify-center gap-2">
|
|
169
|
+
{Array.from({ length: count }, (_, i) => (
|
|
170
|
+
<button
|
|
171
|
+
key={i}
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={() => goTo(i)}
|
|
49
174
|
className={cn(
|
|
50
|
-
"
|
|
51
|
-
|
|
175
|
+
"h-3 w-3 rounded-full border-2 transition-colors",
|
|
176
|
+
i === index
|
|
177
|
+
? "bg-primary border-primary"
|
|
178
|
+
: "bg-transparent border-muted-foreground hover:border-primary",
|
|
52
179
|
)}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</div>
|
|
180
|
+
aria-label={`Go to step ${i + 1}`}
|
|
181
|
+
/>
|
|
56
182
|
))}
|
|
57
183
|
</div>
|
|
58
184
|
|
|
59
|
-
<div className="
|
|
60
|
-
<Button onClick={back} disabled={
|
|
185
|
+
<div className="flex gap-2">
|
|
186
|
+
<Button variant="outline" onClick={back} disabled={!canGoBack}>
|
|
61
187
|
Back
|
|
62
188
|
</Button>
|
|
63
|
-
<Button onClick={next} disabled={
|
|
189
|
+
<Button onClick={next} disabled={!canGoNext}>
|
|
64
190
|
Next
|
|
65
191
|
</Button>
|
|
66
|
-
<Button onClick={() => goTo(0)}>Go to first step</Button>
|
|
67
|
-
<Button onClick={() => goTo(1)}>Go to second step</Button>
|
|
68
|
-
<Button onClick={() => goTo(2)}>Go to third step</Button>
|
|
69
192
|
</div>
|
|
70
193
|
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<code className="block">
|
|
77
|
-
Current step:{" "}
|
|
78
|
-
<span className="text-blue-300">
|
|
79
|
-
{JSON.stringify(step, null, 2)}
|
|
80
|
-
</span>
|
|
81
|
-
</code>
|
|
82
|
-
<code className="block">
|
|
83
|
-
isFirstStep:{" "}
|
|
84
|
-
<span className={isFirstStep ? "text-green-500" : "text-red-500"}>
|
|
85
|
-
{JSON.stringify(isFirstStep)}
|
|
194
|
+
<div className="rounded-md bg-muted/40 px-3 py-2 text-xs font-mono space-y-1">
|
|
195
|
+
<p>
|
|
196
|
+
canGoBack:{" "}
|
|
197
|
+
<span className={canGoBack ? "text-green-600" : "text-red-500"}>
|
|
198
|
+
{String(canGoBack)}
|
|
86
199
|
</span>
|
|
87
|
-
</
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
<span className={
|
|
91
|
-
{
|
|
200
|
+
</p>
|
|
201
|
+
<p>
|
|
202
|
+
canGoNext:{" "}
|
|
203
|
+
<span className={canGoNext ? "text-green-600" : "text-red-500"}>
|
|
204
|
+
{String(canGoNext)}
|
|
92
205
|
</span>
|
|
93
|
-
</
|
|
94
|
-
</
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
95
208
|
</div>
|
|
96
209
|
);
|
|
97
210
|
},
|
|
@@ -1,73 +1,198 @@
|
|
|
1
1
|
import { act, renderHook } from "@testing-library/react";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { useStep } from "
|
|
4
|
-
|
|
5
|
-
describe("useStep
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
import { useStep } from "../use-step";
|
|
4
|
+
|
|
5
|
+
describe("useStep", () => {
|
|
6
|
+
// ── 1. Default start ──────────────────────────────────────────────────────
|
|
7
|
+
describe("default start", () => {
|
|
8
|
+
it("starts at index 0, isFirstStep true, isLastStep false", () => {
|
|
9
|
+
const { result } = renderHook(() => useStep(5));
|
|
10
|
+
const [index, { isFirstStep, isLastStep }] = result.current;
|
|
11
|
+
expect(index).toBe(0);
|
|
12
|
+
expect(isFirstStep).toBe(true);
|
|
13
|
+
expect(isLastStep).toBe(false);
|
|
14
|
+
});
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
// ── 2. next() ─────────────────────────────────────────────────────────────
|
|
18
|
+
describe("next()", () => {
|
|
19
|
+
it("increments index when not at last step", async () => {
|
|
20
|
+
const { result } = renderHook(() => useStep(3));
|
|
21
|
+
await act(async () => {
|
|
22
|
+
result.current[1].next();
|
|
23
|
+
});
|
|
24
|
+
expect(result.current[0]).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is a no-op at the last step", async () => {
|
|
28
|
+
const { result } = renderHook(() => useStep(1));
|
|
29
|
+
await act(async () => {
|
|
30
|
+
result.current[1].next();
|
|
31
|
+
});
|
|
32
|
+
expect(result.current[0]).toBe(0);
|
|
33
|
+
});
|
|
23
34
|
});
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
// ── 3. back() ─────────────────────────────────────────────────────────────
|
|
37
|
+
describe("back()", () => {
|
|
38
|
+
it("decrements index when not at first step", async () => {
|
|
39
|
+
const { result } = renderHook(() => useStep(3));
|
|
40
|
+
await act(async () => {
|
|
41
|
+
result.current[1].goTo(1);
|
|
42
|
+
});
|
|
43
|
+
await act(async () => {
|
|
44
|
+
result.current[1].back();
|
|
45
|
+
});
|
|
46
|
+
expect(result.current[0]).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("is a no-op at the first step", async () => {
|
|
50
|
+
const { result } = renderHook(() => useStep(3));
|
|
51
|
+
await act(async () => {
|
|
52
|
+
result.current[1].back();
|
|
53
|
+
});
|
|
54
|
+
expect(result.current[0]).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── 4. goTo() ─────────────────────────────────────────────────────────────
|
|
59
|
+
describe("goTo()", () => {
|
|
60
|
+
it("jumps to a valid index", async () => {
|
|
61
|
+
const { result } = renderHook(() => useStep(5));
|
|
62
|
+
await act(async () => {
|
|
63
|
+
result.current[1].goTo(3);
|
|
64
|
+
});
|
|
65
|
+
expect(result.current[0]).toBe(3);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("clamps negative target to 0", async () => {
|
|
69
|
+
const { result } = renderHook(() => useStep(5));
|
|
70
|
+
await act(async () => {
|
|
71
|
+
result.current[1].goTo(-2);
|
|
72
|
+
});
|
|
73
|
+
expect(result.current[0]).toBe(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("clamps over-bound target to count - 1", async () => {
|
|
77
|
+
const { result } = renderHook(() => useStep(5));
|
|
78
|
+
await act(async () => {
|
|
79
|
+
result.current[1].goTo(99);
|
|
80
|
+
});
|
|
81
|
+
expect(result.current[0]).toBe(4);
|
|
82
|
+
});
|
|
31
83
|
});
|
|
32
84
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
85
|
+
// ── 5. reset() ────────────────────────────────────────────────────────────
|
|
86
|
+
describe("reset()", () => {
|
|
87
|
+
it("returns to index 0", async () => {
|
|
88
|
+
const { result } = renderHook(() => useStep(5));
|
|
89
|
+
await act(async () => {
|
|
90
|
+
result.current[1].goTo(3);
|
|
91
|
+
});
|
|
92
|
+
await act(async () => {
|
|
93
|
+
result.current[1].reset();
|
|
94
|
+
});
|
|
95
|
+
expect(result.current[0]).toBe(0);
|
|
96
|
+
});
|
|
39
97
|
});
|
|
40
98
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
99
|
+
// ── 6. canGoNext / canGoBack ──────────────────────────────────────────────
|
|
100
|
+
describe("canGoNext / canGoBack", () => {
|
|
101
|
+
it("canGoNext is false and canGoBack is true at the last step", async () => {
|
|
102
|
+
const { result } = renderHook(() => useStep(3));
|
|
103
|
+
await act(async () => {
|
|
104
|
+
result.current[1].goTo(2);
|
|
105
|
+
});
|
|
106
|
+
const [, { canGoNext, canGoBack }] = result.current;
|
|
107
|
+
expect(canGoNext).toBe(false);
|
|
108
|
+
expect(canGoBack).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("canGoBack is false and canGoNext is true at the first step", () => {
|
|
112
|
+
const { result } = renderHook(() => useStep(3));
|
|
113
|
+
const [, { canGoBack, canGoNext }] = result.current;
|
|
114
|
+
expect(canGoBack).toBe(false);
|
|
115
|
+
expect(canGoNext).toBe(true);
|
|
116
|
+
});
|
|
45
117
|
});
|
|
46
118
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
119
|
+
// ── 7. progress ───────────────────────────────────────────────────────────
|
|
120
|
+
describe("progress", () => {
|
|
121
|
+
it("is 0.5 at the mid-step of a 5-step sequence", async () => {
|
|
122
|
+
const { result } = renderHook(() => useStep(5));
|
|
123
|
+
await act(async () => {
|
|
124
|
+
result.current[1].goTo(2);
|
|
125
|
+
});
|
|
126
|
+
expect(result.current[1].progress).toBe(0.5);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("is 0 at the first step", () => {
|
|
130
|
+
const { result } = renderHook(() => useStep(5));
|
|
131
|
+
expect(result.current[1].progress).toBe(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("is 1 at the last step", async () => {
|
|
135
|
+
const { result } = renderHook(() => useStep(5));
|
|
136
|
+
await act(async () => {
|
|
137
|
+
result.current[1].goTo(4);
|
|
138
|
+
});
|
|
139
|
+
expect(result.current[1].progress).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("is 0 when count is 1", () => {
|
|
143
|
+
const { result } = renderHook(() => useStep(1));
|
|
144
|
+
expect(result.current[1].progress).toBe(0);
|
|
145
|
+
});
|
|
52
146
|
});
|
|
53
147
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
148
|
+
// ── 8. isFirstStep / isLastStep ───────────────────────────────────────────
|
|
149
|
+
describe("isFirstStep / isLastStep", () => {
|
|
150
|
+
it("isLastStep is true and isFirstStep is false at the last step", async () => {
|
|
151
|
+
const { result } = renderHook(() => useStep(3));
|
|
152
|
+
await act(async () => {
|
|
153
|
+
result.current[1].goTo(2);
|
|
154
|
+
});
|
|
155
|
+
const [, { isLastStep, isFirstStep }] = result.current;
|
|
156
|
+
expect(isLastStep).toBe(true);
|
|
157
|
+
expect(isFirstStep).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("isFirstStep and isLastStep are both true when count is 1", () => {
|
|
161
|
+
const { result } = renderHook(() => useStep(1));
|
|
162
|
+
const [, { isFirstStep, isLastStep }] = result.current;
|
|
163
|
+
expect(isFirstStep).toBe(true);
|
|
164
|
+
expect(isLastStep).toBe(true);
|
|
165
|
+
});
|
|
58
166
|
});
|
|
59
167
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
168
|
+
// ── 9. Action reference stability ─────────────────────────────────────────
|
|
169
|
+
describe("action reference stability", () => {
|
|
170
|
+
it("next, back, goTo, and reset are the same references across re-renders", async () => {
|
|
171
|
+
const { result, rerender } = renderHook(() => useStep(3));
|
|
172
|
+
const [, { next, back, goTo, reset }] = result.current;
|
|
173
|
+
|
|
174
|
+
rerender();
|
|
175
|
+
|
|
176
|
+
expect(result.current[1].next).toBe(next);
|
|
177
|
+
expect(result.current[1].back).toBe(back);
|
|
178
|
+
expect(result.current[1].goTo).toBe(goTo);
|
|
179
|
+
expect(result.current[1].reset).toBe(reset);
|
|
180
|
+
});
|
|
64
181
|
});
|
|
65
182
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
183
|
+
// ── 10. Cleanup on unmount ────────────────────────────────────────────────
|
|
184
|
+
describe("cleanup on unmount", () => {
|
|
185
|
+
it("does not throw after unmount when calling actions", async () => {
|
|
186
|
+
const { result, unmount } = renderHook(() => useStep(3));
|
|
187
|
+
unmount();
|
|
188
|
+
await act(async () => {
|
|
189
|
+
try {
|
|
190
|
+
result.current[1].next();
|
|
191
|
+
} catch {
|
|
192
|
+
// suppress unmount-related errors
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// no assertion needed — test passes if no error is thrown
|
|
196
|
+
});
|
|
72
197
|
});
|
|
73
198
|
});
|
|
@@ -1,55 +1,63 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
2
|
|
|
3
|
-
interface
|
|
4
|
-
|
|
5
|
-
step: T;
|
|
6
|
-
steps: T[];
|
|
7
|
-
isFirstStep: boolean;
|
|
8
|
-
isLastStep: boolean;
|
|
9
|
-
goTo: (i: number) => void;
|
|
3
|
+
export interface UseStepActions {
|
|
4
|
+
/** Advance one step. No-op at the last step. Stable reference. */
|
|
10
5
|
next: () => void;
|
|
6
|
+
/** Go back one step. No-op at the first step. Stable reference. */
|
|
11
7
|
back: () => void;
|
|
12
|
-
|
|
8
|
+
/** Jump to an index, clamped to [0, count - 1]. Stable reference. */
|
|
9
|
+
goTo: (index: number) => void;
|
|
10
|
+
/** Return to index 0. Stable reference. */
|
|
11
|
+
reset: () => void;
|
|
12
|
+
/** True when there is a next step to advance to. */
|
|
13
|
+
canGoNext: boolean;
|
|
14
|
+
/** True when there is a previous step to go back to. */
|
|
15
|
+
canGoBack: boolean;
|
|
16
|
+
/** True when the current index is the first step. */
|
|
17
|
+
isFirstStep: boolean;
|
|
18
|
+
/** True when the current index is the last step. */
|
|
19
|
+
isLastStep: boolean;
|
|
20
|
+
/** Ratio of progress in [0, 1]. 0 when count <= 1. */
|
|
21
|
+
progress: number;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
24
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
25
|
+
Math.min(Math.max(value, min), max);
|
|
26
|
+
|
|
27
|
+
export function useStep(count: number): [number, UseStepActions] {
|
|
28
|
+
const [index, setIndex] = useState(0);
|
|
29
|
+
const lastIndex = Math.max(count - 1, 0);
|
|
30
|
+
const currentIndex = clamp(index, 0, lastIndex);
|
|
31
|
+
|
|
32
|
+
const next = useCallback(
|
|
33
|
+
() => setIndex((i) => Math.min(i + 1, lastIndex)),
|
|
34
|
+
[lastIndex],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const back = useCallback(() => setIndex((i) => Math.max(i - 1, 0)), []);
|
|
38
|
+
|
|
39
|
+
const goTo = useCallback(
|
|
40
|
+
(target: number) => setIndex(clamp(target, 0, lastIndex)),
|
|
41
|
+
[lastIndex],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const reset = useCallback(() => setIndex(0), []);
|
|
45
|
+
|
|
46
|
+
const isFirstStep = currentIndex <= 0;
|
|
47
|
+
const isLastStep = currentIndex >= lastIndex;
|
|
48
|
+
|
|
49
|
+
return [
|
|
50
|
+
currentIndex,
|
|
51
|
+
{
|
|
52
|
+
next,
|
|
53
|
+
back,
|
|
54
|
+
goTo,
|
|
55
|
+
reset,
|
|
56
|
+
canGoNext: !isLastStep,
|
|
57
|
+
canGoBack: !isFirstStep,
|
|
58
|
+
isFirstStep,
|
|
59
|
+
isLastStep,
|
|
60
|
+
progress: count <= 1 ? 0 : currentIndex / (count - 1),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
54
63
|
}
|
|
55
|
-
|