@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
|
@@ -11,12 +11,26 @@ describe("useDebouncedValue", () => {
|
|
|
11
11
|
vi.useRealTimers();
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// --- Return shape (v2: tuple) ---
|
|
15
|
+
|
|
16
|
+
it("returns a tuple [debounced, controls]", () => {
|
|
17
|
+
const { result } = renderHook(() => useDebouncedValue("init", 300));
|
|
18
|
+
const [debounced, controls] = result.current;
|
|
19
|
+
expect(debounced).toBe("init");
|
|
20
|
+
expect(typeof controls.cancel).toBe("function");
|
|
21
|
+
expect(typeof controls.flush).toBe("function");
|
|
22
|
+
expect(typeof controls.isPending).toBe("boolean");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("debounced is initial value at mount", () => {
|
|
15
26
|
const { result } = renderHook(() => useDebouncedValue("init", 300));
|
|
16
|
-
|
|
27
|
+
const [debounced] = result.current;
|
|
28
|
+
expect(debounced).toBe("init");
|
|
17
29
|
});
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
// --- Debounce behavior (using tuple destructure) ---
|
|
32
|
+
|
|
33
|
+
it("debounced does not update before delay elapses", () => {
|
|
20
34
|
const { result, rerender } = renderHook(
|
|
21
35
|
({ value }) => useDebouncedValue(value, 300),
|
|
22
36
|
{ initialProps: { value: "a" } },
|
|
@@ -27,10 +41,11 @@ describe("useDebouncedValue", () => {
|
|
|
27
41
|
vi.advanceTimersByTime(299);
|
|
28
42
|
});
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
const [debounced] = result.current;
|
|
45
|
+
expect(debounced).toBe("a");
|
|
31
46
|
});
|
|
32
47
|
|
|
33
|
-
it("updates after delay elapses", () => {
|
|
48
|
+
it("debounced updates after delay elapses", () => {
|
|
34
49
|
const { result, rerender } = renderHook(
|
|
35
50
|
({ value }) => useDebouncedValue(value, 300),
|
|
36
51
|
{ initialProps: { value: "a" } },
|
|
@@ -41,7 +56,8 @@ describe("useDebouncedValue", () => {
|
|
|
41
56
|
vi.advanceTimersByTime(300);
|
|
42
57
|
});
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
const [debounced] = result.current;
|
|
60
|
+
expect(debounced).toBe("b");
|
|
45
61
|
});
|
|
46
62
|
|
|
47
63
|
it("cancels pending update when value changes again before delay", () => {
|
|
@@ -60,13 +76,13 @@ describe("useDebouncedValue", () => {
|
|
|
60
76
|
vi.advanceTimersByTime(200);
|
|
61
77
|
});
|
|
62
78
|
|
|
63
|
-
expect(result.current).toBe("a");
|
|
79
|
+
expect(result.current[0]).toBe("a");
|
|
64
80
|
|
|
65
81
|
act(() => {
|
|
66
82
|
vi.advanceTimersByTime(100);
|
|
67
83
|
});
|
|
68
84
|
|
|
69
|
-
expect(result.current).toBe("c");
|
|
85
|
+
expect(result.current[0]).toBe("c");
|
|
70
86
|
});
|
|
71
87
|
|
|
72
88
|
it("works with non-string values", () => {
|
|
@@ -80,10 +96,89 @@ describe("useDebouncedValue", () => {
|
|
|
80
96
|
vi.advanceTimersByTime(100);
|
|
81
97
|
});
|
|
82
98
|
|
|
83
|
-
expect(result.current).toBe(42);
|
|
99
|
+
expect(result.current[0]).toBe(42);
|
|
84
100
|
});
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
// --- isPending ---
|
|
103
|
+
|
|
104
|
+
it("isPending is true while pending, false after update", () => {
|
|
105
|
+
const { result, rerender } = renderHook(
|
|
106
|
+
({ value }) => useDebouncedValue(value, 300),
|
|
107
|
+
{ initialProps: { value: "a" } },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
rerender({ value: "b" });
|
|
111
|
+
|
|
112
|
+
expect(result.current[1].isPending).toBe(true);
|
|
113
|
+
|
|
114
|
+
act(() => {
|
|
115
|
+
vi.advanceTimersByTime(300);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.current[1].isPending).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// --- cancel ---
|
|
122
|
+
|
|
123
|
+
it("cancel() stops pending value update", () => {
|
|
124
|
+
const { result, rerender } = renderHook(
|
|
125
|
+
({ value }) => useDebouncedValue(value, 300),
|
|
126
|
+
{ initialProps: { value: "a" } },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
rerender({ value: "b" });
|
|
130
|
+
|
|
131
|
+
act(() => {
|
|
132
|
+
result.current[1].cancel();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.current[1].isPending).toBe(false);
|
|
136
|
+
|
|
137
|
+
act(() => {
|
|
138
|
+
vi.advanceTimersByTime(300);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// value should still be "a" — update was cancelled
|
|
142
|
+
expect(result.current[0]).toBe("a");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// --- flush ---
|
|
146
|
+
|
|
147
|
+
it("flush() forces immediate value update", () => {
|
|
148
|
+
const { result, rerender } = renderHook(
|
|
149
|
+
({ value }) => useDebouncedValue(value, 300),
|
|
150
|
+
{ initialProps: { value: "a" } },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
rerender({ value: "b" });
|
|
154
|
+
|
|
155
|
+
act(() => {
|
|
156
|
+
result.current[1].flush();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.current[0]).toBe("b");
|
|
160
|
+
expect(result.current[1].isPending).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// --- Stable references ---
|
|
164
|
+
|
|
165
|
+
it("cancel and flush forwarded from inner hook — stable references", () => {
|
|
166
|
+
const { result, rerender } = renderHook(
|
|
167
|
+
({ value }) => useDebouncedValue(value, 300),
|
|
168
|
+
{ initialProps: { value: "a" } },
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const { cancel: c1, flush: f1 } = result.current[1];
|
|
172
|
+
rerender({ value: "a" });
|
|
173
|
+
const { cancel: c2, flush: f2 } = result.current[1];
|
|
174
|
+
|
|
175
|
+
expect(c2).toBe(c1);
|
|
176
|
+
expect(f2).toBe(f1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- Unmount cleanup ---
|
|
180
|
+
|
|
181
|
+
it("unmount cancels pending timer (no setState after unmount)", () => {
|
|
87
182
|
const { rerender, unmount } = renderHook(
|
|
88
183
|
({ value }) => useDebouncedValue(value, 300),
|
|
89
184
|
{ initialProps: { value: "a" } },
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import type { DebouncedControls } from "../use-debounce-callback/use-debounced-callback";
|
|
3
|
+
import { useDebouncedCallback } from "../use-debounce-callback/use-debounced-callback";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Returns the debounced version of `value` together with a controls object.
|
|
7
|
+
*
|
|
8
|
+
* Delegates entirely to `useDebouncedCallback` — there is no duplicated timer
|
|
9
|
+
* logic in this hook.
|
|
10
|
+
*
|
|
11
|
+
* @returns `[debouncedValue, { cancel, flush, isPending }]`
|
|
12
|
+
*/
|
|
13
|
+
export function useDebouncedValue<T>(
|
|
14
|
+
value: T,
|
|
15
|
+
delay: number,
|
|
16
|
+
): readonly [T, DebouncedControls] {
|
|
17
|
+
const [debounced, setDebounced] = useState<T>(value);
|
|
18
|
+
const [schedule, controls] = useDebouncedCallback(setDebounced, delay);
|
|
5
19
|
|
|
6
20
|
useEffect(() => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}, delay);
|
|
21
|
+
schedule(value);
|
|
22
|
+
}, [value, schedule]);
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
}, [value, delay]);
|
|
13
|
-
|
|
14
|
-
return debouncedValue;
|
|
24
|
+
return [debounced, controls] as const;
|
|
15
25
|
}
|
|
16
|
-
|
|
@@ -1,38 +1,329 @@
|
|
|
1
|
-
import { Meta } from "@storybook/react-vite";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type { Meta } from "@storybook/react-vite";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { Button } from "../../components/button";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
4
5
|
import { useDisclosure } from "../use-disclosure";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Manages a boolean open/closed state for UI patterns like modals, drawers,
|
|
9
|
+
* dropdowns, and accordions.
|
|
10
|
+
*
|
|
11
|
+
* Returns a tuple `[opened, actions]` where `actions` is `{ open, close, toggle, setOpen }`.
|
|
12
|
+
* All actions are referentially stable after mount — safe to pass as props to memoized
|
|
13
|
+
* children or use as `useEffect` / `useCallback` dependencies.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const [opened, { open, close, toggle, setOpen }] = useDisclosure()
|
|
17
|
+
* const [opened, { open, close }] = useDisclosure({ defaultOpen: true })
|
|
18
|
+
* const [opened, { toggle }] = useDisclosure({ onOpenChange: (next) => track(next) })
|
|
19
|
+
* ```
|
|
8
20
|
*/
|
|
9
21
|
const meta: Meta = {
|
|
10
22
|
title: "hooks/useDisclosure",
|
|
23
|
+
parameters: { layout: "centered" },
|
|
11
24
|
};
|
|
12
25
|
|
|
13
26
|
export default meta;
|
|
14
27
|
|
|
28
|
+
// ── Default (Modal pattern) ───────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The most common use case: a trigger opens an overlay, and `close()` dismisses it.
|
|
32
|
+
* Actions (`open`, `close`, `toggle`, `setOpen`) are referentially stable —
|
|
33
|
+
* safe to pass as props without breaking child memoization.
|
|
34
|
+
*/
|
|
15
35
|
export const Default = {
|
|
36
|
+
name: "Default (Modal)",
|
|
37
|
+
render: () => {
|
|
38
|
+
const [opened, { open, close }] = useDisclosure();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex min-h-64 items-start justify-center pt-8">
|
|
42
|
+
<Button onClick={open}>Open modal</Button>
|
|
43
|
+
|
|
44
|
+
{opened && (
|
|
45
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
46
|
+
<div
|
|
47
|
+
className="absolute inset-0 bg-black/40"
|
|
48
|
+
onClick={close}
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
/>
|
|
51
|
+
<div className="relative z-10 w-full max-w-md rounded-xl bg-background p-6 shadow-2xl">
|
|
52
|
+
<h2 className="mb-1 text-lg font-semibold">Confirm action</h2>
|
|
53
|
+
<p className="mb-6 text-sm text-muted-foreground">
|
|
54
|
+
This is a modal controlled by <code>useDisclosure</code>. Click
|
|
55
|
+
outside or use the buttons below to dismiss.
|
|
56
|
+
</p>
|
|
57
|
+
<div className="flex justify-end gap-2">
|
|
58
|
+
<Button variant="outline" onClick={close}>
|
|
59
|
+
Cancel
|
|
60
|
+
</Button>
|
|
61
|
+
<Button onClick={close}>Confirm</Button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ── Drawer ────────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Same hook, different UI — a slide-in side panel.
|
|
75
|
+
* Demonstrates that `useDisclosure` is agnostic to what it controls.
|
|
76
|
+
*/
|
|
77
|
+
export const Drawer = {
|
|
78
|
+
render: () => {
|
|
79
|
+
const [opened, { open, close }] = useDisclosure();
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex min-h-64 items-start justify-center pt-8">
|
|
83
|
+
<Button onClick={open}>Open drawer</Button>
|
|
84
|
+
|
|
85
|
+
{opened && (
|
|
86
|
+
<div className="fixed inset-0 z-50 flex justify-end">
|
|
87
|
+
<div
|
|
88
|
+
className="absolute inset-0 bg-black/40"
|
|
89
|
+
onClick={close}
|
|
90
|
+
aria-hidden="true"
|
|
91
|
+
/>
|
|
92
|
+
<div className="relative z-10 flex h-full w-72 flex-col bg-background shadow-2xl">
|
|
93
|
+
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
94
|
+
<span className="font-semibold">Settings</span>
|
|
95
|
+
<Button variant="ghost" size="sm" onClick={close}>
|
|
96
|
+
✕
|
|
97
|
+
</Button>
|
|
98
|
+
</div>
|
|
99
|
+
<div className="flex-1 px-4 py-4">
|
|
100
|
+
<p className="text-sm text-muted-foreground">
|
|
101
|
+
Drawer content goes here. The hook doesn't care whether it's
|
|
102
|
+
controlling a modal, drawer, tooltip, or any other element.
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="border-t px-4 py-3">
|
|
106
|
+
<Button className="w-full" onClick={close}>
|
|
107
|
+
Close
|
|
108
|
+
</Button>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ── Accordion (multiple independent) ─────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Three independent `useDisclosure()` instances — each item manages its own state.
|
|
122
|
+
* Opening one does not affect the others.
|
|
123
|
+
*/
|
|
124
|
+
export const Accordion = {
|
|
125
|
+
render: () => {
|
|
126
|
+
const [aOpened, { toggle: aToggle }] = useDisclosure();
|
|
127
|
+
const [bOpened, { toggle: bToggle }] = useDisclosure();
|
|
128
|
+
const [cOpened, { toggle: cToggle }] = useDisclosure();
|
|
129
|
+
|
|
130
|
+
const items = [
|
|
131
|
+
{
|
|
132
|
+
id: "a",
|
|
133
|
+
label: "What does this hook return?",
|
|
134
|
+
opened: aOpened,
|
|
135
|
+
toggle: aToggle,
|
|
136
|
+
body: "A tuple [opened, { open, close, toggle, setOpen }]. The boolean is reactive; all actions are stable.",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "b",
|
|
140
|
+
label: "Does onOpenChange fire on no-ops?",
|
|
141
|
+
opened: bOpened,
|
|
142
|
+
toggle: bToggle,
|
|
143
|
+
body: "No. Calling open() when already open, or close() when already closed, is a silent no-op — no re-render, no callback.",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "c",
|
|
147
|
+
label: "Are the actions referentially stable?",
|
|
148
|
+
opened: cOpened,
|
|
149
|
+
toggle: cToggle,
|
|
150
|
+
body: "Yes. All four actions maintain identity for the component's lifetime, making them safe as useEffect dependencies.",
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className="w-full max-w-lg divide-y rounded-xl border">
|
|
156
|
+
{items.map(({ id, label, opened, toggle, body }) => (
|
|
157
|
+
<div key={id}>
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
className="flex w-full items-center justify-between px-4 py-3 text-sm font-medium transition-colors hover:bg-muted/50"
|
|
161
|
+
onClick={toggle}
|
|
162
|
+
>
|
|
163
|
+
{label}
|
|
164
|
+
<span
|
|
165
|
+
className={cn(
|
|
166
|
+
"text-muted-foreground transition-transform duration-200",
|
|
167
|
+
opened && "rotate-180",
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
▾
|
|
171
|
+
</span>
|
|
172
|
+
</button>
|
|
173
|
+
{opened && (
|
|
174
|
+
<p className="border-t bg-muted/20 px-4 py-3 text-sm text-muted-foreground">
|
|
175
|
+
{body}
|
|
176
|
+
</p>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// ── onOpenChange ──────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `onOpenChange` fires exactly once per real transition — silent on no-ops.
|
|
189
|
+
* Clicking "Open" twice logs only one entry. Clicking "Close" when already
|
|
190
|
+
* closed produces no log. The counter proves this visually.
|
|
191
|
+
*/
|
|
192
|
+
export const OnOpenChange = {
|
|
193
|
+
name: "With onOpenChange",
|
|
16
194
|
render: () => {
|
|
17
|
-
const [
|
|
195
|
+
const [log, setLog] = useState<Array<{ value: boolean; time: string }>>([]);
|
|
196
|
+
|
|
197
|
+
const [opened, { open, close, toggle }] = useDisclosure({
|
|
198
|
+
onOpenChange: (next) => {
|
|
199
|
+
setLog((prev) => [
|
|
200
|
+
{ value: next, time: new Date().toLocaleTimeString() },
|
|
201
|
+
...prev.slice(0, 5),
|
|
202
|
+
]);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
18
205
|
|
|
19
206
|
return (
|
|
20
|
-
<div>
|
|
207
|
+
<div className="flex w-80 flex-col gap-4">
|
|
21
208
|
<div
|
|
22
|
-
onClick={toggle}
|
|
23
209
|
className={cn(
|
|
24
|
-
"
|
|
25
|
-
|
|
210
|
+
"flex h-14 items-center justify-center rounded-lg border-2 border-dashed text-sm font-medium transition-all",
|
|
211
|
+
opened
|
|
212
|
+
? "border-primary bg-primary/5 text-primary"
|
|
213
|
+
: "border-border text-muted-foreground",
|
|
26
214
|
)}
|
|
27
215
|
>
|
|
28
|
-
|
|
216
|
+
{opened ? "Opened" : "Closed"}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="flex gap-2">
|
|
220
|
+
<Button size="sm" onClick={open} className="flex-1">
|
|
221
|
+
Open
|
|
222
|
+
</Button>
|
|
223
|
+
<Button
|
|
224
|
+
size="sm"
|
|
225
|
+
variant="outline"
|
|
226
|
+
onClick={close}
|
|
227
|
+
className="flex-1"
|
|
228
|
+
>
|
|
229
|
+
Close
|
|
230
|
+
</Button>
|
|
231
|
+
<Button
|
|
232
|
+
size="sm"
|
|
233
|
+
variant="outline"
|
|
234
|
+
onClick={toggle}
|
|
235
|
+
className="flex-1"
|
|
236
|
+
>
|
|
237
|
+
Toggle
|
|
238
|
+
</Button>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div className="min-h-24 rounded-lg border bg-muted/30 p-3">
|
|
242
|
+
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
|
243
|
+
Transition log (no-ops are silent):
|
|
244
|
+
</p>
|
|
245
|
+
{log.length === 0 ? (
|
|
246
|
+
<p className="text-xs italic text-muted-foreground">
|
|
247
|
+
No transitions yet.
|
|
248
|
+
</p>
|
|
249
|
+
) : (
|
|
250
|
+
<ul className="space-y-1">
|
|
251
|
+
{log.map((entry, i) => (
|
|
252
|
+
<li key={`${entry.time}-${i}`} className="font-mono text-xs">
|
|
253
|
+
<span
|
|
254
|
+
className={cn(
|
|
255
|
+
"font-semibold",
|
|
256
|
+
entry.value ? "text-emerald-600" : "text-rose-500",
|
|
257
|
+
)}
|
|
258
|
+
>
|
|
259
|
+
→ {entry.value ? "true" : "false"}
|
|
260
|
+
</span>
|
|
261
|
+
<span className="ml-2 text-muted-foreground">
|
|
262
|
+
{entry.time}
|
|
263
|
+
</span>
|
|
264
|
+
</li>
|
|
265
|
+
))}
|
|
266
|
+
</ul>
|
|
267
|
+
)}
|
|
29
268
|
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
30
273
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
274
|
+
// ── StableActions ─────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Action references never change after mount, even across many re-renders.
|
|
278
|
+
* Safe to use as `useEffect` dependencies or as stable props to memoized children.
|
|
279
|
+
*/
|
|
280
|
+
export const StableActions = {
|
|
281
|
+
render: () => {
|
|
282
|
+
const [, { open }] = useDisclosure();
|
|
283
|
+
const [renderCount, setRenderCount] = useState(0);
|
|
284
|
+
const initialOpenRef = useRef<(() => void) | null>(null);
|
|
285
|
+
|
|
286
|
+
if (initialOpenRef.current === null) {
|
|
287
|
+
initialOpenRef.current = open;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isStable = open === initialOpenRef.current;
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className="flex w-72 flex-col gap-4">
|
|
294
|
+
<div className="rounded-xl border bg-muted/20 p-4">
|
|
295
|
+
<p className="mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
296
|
+
Action identity
|
|
297
|
+
</p>
|
|
298
|
+
<div className="flex items-center justify-between rounded-lg border bg-background px-3 py-2">
|
|
299
|
+
<code className="text-sm">open</code>
|
|
300
|
+
<span
|
|
301
|
+
className={cn(
|
|
302
|
+
"text-sm font-semibold",
|
|
303
|
+
isStable ? "text-emerald-600" : "text-rose-500",
|
|
304
|
+
)}
|
|
305
|
+
>
|
|
306
|
+
{isStable ? "stable ✓" : "changed ✗"}
|
|
307
|
+
</span>
|
|
308
|
+
</div>
|
|
35
309
|
</div>
|
|
310
|
+
|
|
311
|
+
<div className="rounded-xl border bg-muted/20 p-4">
|
|
312
|
+
<p className="mb-3 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
313
|
+
Render count
|
|
314
|
+
</p>
|
|
315
|
+
<p className="text-2xl font-bold tabular-nums">{renderCount + 1}</p>
|
|
316
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
317
|
+
renders (this component)
|
|
318
|
+
</p>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<Button variant="outline" onClick={() => setRenderCount((c) => c + 1)}>
|
|
322
|
+
Force re-render
|
|
323
|
+
</Button>
|
|
324
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
325
|
+
Trigger re-renders — <code>open</code> identity stays stable.
|
|
326
|
+
</p>
|
|
36
327
|
</div>
|
|
37
328
|
);
|
|
38
329
|
},
|