@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,102 +1,281 @@
|
|
|
1
1
|
import { act, renderHook } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { useObject } from "../use-object";
|
|
4
4
|
|
|
5
|
-
describe("useObject
|
|
6
|
-
|
|
7
|
-
const initialValue = { count: 0 };
|
|
8
|
-
const { result } = renderHook(() => useObject(initialValue));
|
|
5
|
+
describe("useObject", () => {
|
|
6
|
+
// ─── initialization ───────────────────────────────────────────────────────
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
describe("initialization", () => {
|
|
9
|
+
it("returns [state, actions] tuple where state deep-equals initialValue", () => {
|
|
10
|
+
const init = { name: "Alice", age: 30 };
|
|
11
|
+
const { result } = renderHook(() => useObject(init));
|
|
12
|
+
|
|
13
|
+
expect(result.current[0]).toEqual(init);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("actions object exposes set, replace, reset, setKey", () => {
|
|
17
|
+
const { result } = renderHook(() => useObject({ x: 1 }));
|
|
18
|
+
const actions = result.current[1];
|
|
19
|
+
|
|
20
|
+
expect(typeof actions.set).toBe("function");
|
|
21
|
+
expect(typeof actions.replace).toBe("function");
|
|
22
|
+
expect(typeof actions.reset).toBe("function");
|
|
23
|
+
expect(typeof actions.setKey).toBe("function");
|
|
24
|
+
});
|
|
11
25
|
});
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
const initialValue = { count: 0 };
|
|
15
|
-
const { result } = renderHook(() => useObject<{}>(initialValue));
|
|
27
|
+
// ─── set (merge) ──────────────────────────────────────────────────────────
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
describe("set", () => {
|
|
30
|
+
it("set(partial) shallow-merges and preserves untouched keys", () => {
|
|
31
|
+
const { result } = renderHook(() =>
|
|
32
|
+
useObject({ name: "Alice", email: "a@a.com" }),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
act(() => result.current[1].set({ name: "Bob" }));
|
|
36
|
+
|
|
37
|
+
expect(result.current[0]).toEqual({ name: "Bob", email: "a@a.com" });
|
|
19
38
|
});
|
|
20
39
|
|
|
21
|
-
|
|
40
|
+
it("set(fn) functional updater receives prev and merges returned partial", () => {
|
|
41
|
+
const { result } = renderHook(() => useObject({ count: 3 }));
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
act(() => result.current[1].set((s) => ({ count: s.count + 1 })));
|
|
44
|
+
|
|
45
|
+
expect(result.current[0]).toEqual({ count: 4 });
|
|
25
46
|
});
|
|
26
47
|
|
|
27
|
-
|
|
48
|
+
it("set({}) empty partial produces a new state reference", () => {
|
|
49
|
+
const { result } = renderHook(() => useObject({ x: 1 }));
|
|
50
|
+
const before = result.current[0];
|
|
51
|
+
|
|
52
|
+
act(() => result.current[1].set({}));
|
|
53
|
+
|
|
54
|
+
expect(result.current[0]).not.toBe(before);
|
|
55
|
+
});
|
|
28
56
|
});
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
// ─── replace ──────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe("replace", () => {
|
|
61
|
+
it("replace(value) swaps the whole object and discards unmentioned keys", () => {
|
|
62
|
+
const { result } = renderHook(() => useObject({ a: 1, b: 2 }));
|
|
63
|
+
|
|
64
|
+
act(() =>
|
|
65
|
+
result.current[1].replace({ a: 99 } as { a: number; b: number }),
|
|
66
|
+
);
|
|
33
67
|
|
|
34
|
-
|
|
35
|
-
result.current[
|
|
68
|
+
expect(result.current[0]).toEqual({ a: 99 });
|
|
69
|
+
expect((result.current[0] as { b?: number }).b).toBeUndefined();
|
|
36
70
|
});
|
|
37
71
|
|
|
38
|
-
|
|
72
|
+
it("replace(fn) functional variant receives prev and fully replaces", () => {
|
|
73
|
+
const { result } = renderHook(() => useObject({ count: 5 }));
|
|
39
74
|
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
act(() => result.current[1].replace((s) => ({ count: s.count * 2 })));
|
|
76
|
+
|
|
77
|
+
expect(result.current[0]).toEqual({ count: 10 });
|
|
42
78
|
});
|
|
43
79
|
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
it("replace with identical values still produces a new state reference", () => {
|
|
81
|
+
const { result } = renderHook(() => useObject({ x: 1 }));
|
|
82
|
+
const before = result.current[0];
|
|
46
83
|
|
|
47
|
-
|
|
48
|
-
const initialValue = { count: 0 };
|
|
49
|
-
const { result } = renderHook(() => useObject(initialValue));
|
|
84
|
+
act(() => result.current[1].replace({ x: 1 }));
|
|
50
85
|
|
|
51
|
-
|
|
52
|
-
result.current[1]({ count: 1 });
|
|
86
|
+
expect(result.current[0]).not.toBe(before);
|
|
53
87
|
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── reset ────────────────────────────────────────────────────────────────
|
|
54
91
|
|
|
55
|
-
|
|
92
|
+
describe("reset", () => {
|
|
93
|
+
it("reset() restores mount-time value after mutation", () => {
|
|
94
|
+
const { result } = renderHook(() => useObject({ count: 0 }));
|
|
56
95
|
|
|
57
|
-
|
|
58
|
-
result.current[
|
|
96
|
+
act(() => result.current[1].set({ count: 5 }));
|
|
97
|
+
expect(result.current[0]).toEqual({ count: 5 });
|
|
98
|
+
|
|
99
|
+
act(() => result.current[1].reset());
|
|
100
|
+
|
|
101
|
+
expect(result.current[0]).toEqual({ count: 0 });
|
|
59
102
|
});
|
|
60
103
|
|
|
61
|
-
|
|
104
|
+
it("reset() ignores prop changes after mount (frozen ref)", () => {
|
|
105
|
+
const { result, rerender } = renderHook(
|
|
106
|
+
({ iv }: { iv: { count: number } }) => useObject(iv),
|
|
107
|
+
{ initialProps: { iv: { count: 0 } } },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Change the prop after mount
|
|
111
|
+
rerender({ iv: { count: 99 } });
|
|
112
|
+
act(() => result.current[1].set({ count: 5 }));
|
|
113
|
+
act(() => result.current[1].reset());
|
|
114
|
+
|
|
115
|
+
// Must restore the mount-time value, NOT the re-rendered prop
|
|
116
|
+
expect(result.current[0]).toEqual({ count: 0 });
|
|
117
|
+
});
|
|
62
118
|
});
|
|
63
119
|
|
|
64
|
-
|
|
65
|
-
|
|
120
|
+
// ─── setKey ───────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("setKey", () => {
|
|
123
|
+
it("setKey(key, value) updates one key and preserves the others", () => {
|
|
124
|
+
const { result } = renderHook(() =>
|
|
125
|
+
useObject({ name: "Alice", age: 30 }),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
act(() => result.current[1].setKey("age", 31));
|
|
129
|
+
|
|
130
|
+
expect(result.current[0]).toEqual({ name: "Alice", age: 31 });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("setKey only touches the specified key", () => {
|
|
134
|
+
const { result } = renderHook(() =>
|
|
135
|
+
useObject({ a: "x", b: "y", c: "z" }),
|
|
136
|
+
);
|
|
66
137
|
|
|
67
|
-
|
|
138
|
+
act(() => result.current[1].setKey("b", "updated"));
|
|
139
|
+
|
|
140
|
+
expect(result.current[0].a).toBe("x");
|
|
141
|
+
expect(result.current[0].b).toBe("updated");
|
|
142
|
+
expect(result.current[0].c).toBe("z");
|
|
143
|
+
});
|
|
68
144
|
});
|
|
69
145
|
|
|
70
|
-
|
|
71
|
-
const initialValue = { count: 0, name: "initial" };
|
|
72
|
-
const { result } = renderHook(() => useObject(initialValue));
|
|
146
|
+
// ─── action stability ─────────────────────────────────────────────────────
|
|
73
147
|
|
|
74
|
-
|
|
75
|
-
|
|
148
|
+
describe("action stability", () => {
|
|
149
|
+
it("actions object reference is stable across re-renders", () => {
|
|
150
|
+
const { result, rerender } = renderHook(() => useObject({ x: 1 }));
|
|
151
|
+
const before = result.current[1];
|
|
152
|
+
|
|
153
|
+
rerender();
|
|
154
|
+
|
|
155
|
+
expect(result.current[1]).toBe(before);
|
|
76
156
|
});
|
|
77
157
|
|
|
78
|
-
|
|
158
|
+
it("each individual action fn is stable across re-renders", () => {
|
|
159
|
+
const { result, rerender } = renderHook(() => useObject({ x: 1 }));
|
|
160
|
+
const { set, replace, reset, setKey } = result.current[1];
|
|
79
161
|
|
|
80
|
-
|
|
81
|
-
|
|
162
|
+
rerender();
|
|
163
|
+
|
|
164
|
+
expect(result.current[1].set).toBe(set);
|
|
165
|
+
expect(result.current[1].replace).toBe(replace);
|
|
166
|
+
expect(result.current[1].reset).toBe(reset);
|
|
167
|
+
expect(result.current[1].setKey).toBe(setKey);
|
|
82
168
|
});
|
|
83
169
|
|
|
84
|
-
|
|
170
|
+
it("changing onChange reference does not invalidate action refs", () => {
|
|
171
|
+
const { result, rerender } = renderHook(
|
|
172
|
+
({ onChange }: { onChange: (s: { x: number }) => void }) =>
|
|
173
|
+
useObject({ x: 1 }, { onChange }),
|
|
174
|
+
{ initialProps: { onChange: vi.fn() } },
|
|
175
|
+
);
|
|
176
|
+
const before = result.current[1];
|
|
177
|
+
|
|
178
|
+
rerender({ onChange: vi.fn() }); // new function reference
|
|
179
|
+
|
|
180
|
+
expect(result.current[1]).toBe(before);
|
|
181
|
+
});
|
|
85
182
|
});
|
|
86
183
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
184
|
+
// ─── onChange ─────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
describe("onChange", () => {
|
|
187
|
+
it("does NOT fire on initial mount", () => {
|
|
188
|
+
const onChange = vi.fn();
|
|
189
|
+
renderHook(() => useObject({ x: 1 }, { onChange }));
|
|
190
|
+
|
|
191
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("fires after set with the new state", () => {
|
|
195
|
+
const onChange = vi.fn();
|
|
196
|
+
const { result } = renderHook(() => useObject({ x: 1 }, { onChange }));
|
|
197
|
+
|
|
198
|
+
act(() => result.current[1].set({ x: 2 }));
|
|
199
|
+
|
|
200
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(onChange).toHaveBeenCalledWith({ x: 2 });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("fires after replace with the new state", () => {
|
|
205
|
+
const onChange = vi.fn();
|
|
206
|
+
const { result } = renderHook(() =>
|
|
207
|
+
useObject({ x: 1, y: 2 }, { onChange }),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
act(() =>
|
|
211
|
+
result.current[1].replace({ x: 99 } as { x: number; y: number }),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(onChange).toHaveBeenCalledWith({ x: 99 });
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("fires after reset with the mount-time state", () => {
|
|
219
|
+
const onChange = vi.fn();
|
|
220
|
+
const { result } = renderHook(() => useObject({ x: 1 }, { onChange }));
|
|
221
|
+
|
|
222
|
+
act(() => result.current[1].set({ x: 5 }));
|
|
223
|
+
onChange.mockClear();
|
|
224
|
+
|
|
225
|
+
act(() => result.current[1].reset());
|
|
226
|
+
|
|
227
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
228
|
+
expect(onChange).toHaveBeenCalledWith({ x: 1 });
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("fires after setKey with the new state", () => {
|
|
232
|
+
const onChange = vi.fn();
|
|
233
|
+
const { result } = renderHook(() =>
|
|
234
|
+
useObject({ name: "Alice", age: 30 }, { onChange }),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
act(() => result.current[1].setKey("age", 31));
|
|
238
|
+
|
|
239
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
240
|
+
expect(onChange).toHaveBeenCalledWith({ name: "Alice", age: 31 });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("new inline onChange reference between renders does not double-fire", () => {
|
|
244
|
+
const spy = vi.fn();
|
|
245
|
+
const { rerender } = renderHook(
|
|
246
|
+
({ onChange }: { onChange: (s: { x: number }) => void }) =>
|
|
247
|
+
useObject({ x: 1 }, { onChange }),
|
|
248
|
+
{ initialProps: { onChange: () => spy() } },
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Re-render with a fresh inline function — no state change
|
|
252
|
+
rerender({ onChange: () => spy() });
|
|
253
|
+
|
|
254
|
+
expect(spy).not.toHaveBeenCalled();
|
|
91
255
|
});
|
|
92
|
-
expect(result.current[0]).toEqual({ count: 1 });
|
|
93
256
|
});
|
|
94
257
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
258
|
+
// ─── edge cases ───────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
describe("edge cases", () => {
|
|
261
|
+
it("set({}) with empty partial is valid and produces a new reference", () => {
|
|
262
|
+
const { result } = renderHook(() => useObject({ a: 1 }));
|
|
263
|
+
const before = result.current[0];
|
|
264
|
+
|
|
265
|
+
act(() => result.current[1].set({}));
|
|
266
|
+
|
|
267
|
+
expect(result.current[0]).not.toBe(before);
|
|
268
|
+
expect(result.current[0]).toEqual({ a: 1 });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("replace with identical values still produces a new reference", () => {
|
|
272
|
+
const { result } = renderHook(() => useObject({ x: 42 }));
|
|
273
|
+
const before = result.current[0];
|
|
274
|
+
|
|
275
|
+
act(() => result.current[1].replace({ x: 42 }));
|
|
276
|
+
|
|
277
|
+
expect(result.current[0]).not.toBe(before);
|
|
278
|
+
expect(result.current[0]).toEqual({ x: 42 });
|
|
99
279
|
});
|
|
100
|
-
expect(result.current[0]).toEqual({ count: 1 });
|
|
101
280
|
});
|
|
102
281
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useLatestRef } from "../internal/use-latest-ref";
|
|
3
|
+
|
|
4
|
+
export interface UseObjectOptions<T> {
|
|
5
|
+
/** Called after every state change. NOT called on initial mount. */
|
|
6
|
+
onChange?: (state: T) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseObjectActions<T> {
|
|
10
|
+
/** Shallow-merge a partial (or functional updater returning a partial). */
|
|
11
|
+
set(partial: Partial<T> | ((state: T) => Partial<T>)): void;
|
|
12
|
+
/** Replace the entire object (no merge). Functional variant receives prev. */
|
|
13
|
+
replace(value: T | ((state: T) => T)): void;
|
|
14
|
+
/** Restore the mount-time value (frozen ref). */
|
|
15
|
+
reset(): void;
|
|
16
|
+
/** Update a single typed key. */
|
|
17
|
+
setKey<K extends keyof T>(key: K, value: T[K]): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useObject<T extends object>(
|
|
21
|
+
initialValue: T,
|
|
22
|
+
options?: UseObjectOptions<T>,
|
|
23
|
+
): readonly [T, UseObjectActions<T>] {
|
|
24
|
+
const [state, setState] = useState<T>(initialValue);
|
|
25
|
+
|
|
26
|
+
// Frozen on mount — reset always restores the original value.
|
|
27
|
+
const initialRef = useRef(initialValue);
|
|
28
|
+
// Always the latest handler — never a memo/effect dep.
|
|
29
|
+
const onChangeRef = useLatestRef(options?.onChange);
|
|
30
|
+
|
|
31
|
+
// Fire onChange on every change EXCEPT the initial mount.
|
|
32
|
+
const mountedRef = useRef(false);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!mountedRef.current) {
|
|
35
|
+
mountedRef.current = true;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
onChangeRef.current?.(state);
|
|
39
|
+
}, [state]);
|
|
40
|
+
|
|
41
|
+
const actions = useMemo<UseObjectActions<T>>(
|
|
42
|
+
() => ({
|
|
43
|
+
set: (partial) =>
|
|
44
|
+
setState((prev) => ({
|
|
45
|
+
...prev,
|
|
46
|
+
...(typeof partial === "function" ? partial(prev) : partial),
|
|
47
|
+
})),
|
|
48
|
+
replace: (value) =>
|
|
49
|
+
setState((prev) =>
|
|
50
|
+
typeof value === "function" ? (value as (s: T) => T)(prev) : value,
|
|
51
|
+
),
|
|
52
|
+
reset: () => setState(initialRef.current),
|
|
53
|
+
setKey: (key, value) => setState((prev) => ({ ...prev, [key]: value })),
|
|
54
|
+
}),
|
|
55
|
+
[setState],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return [state, actions] as const;
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./use-prevent-page-close";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Meta } from "@storybook/react-vite";
|
|
2
|
+
import { usePreventPageClose } from "./use-prevent-page-close";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Prompts the browser's native "Leave site?" confirmation when the user
|
|
6
|
+
* tries to close the tab or reload while `enabled` is `true`. Typical
|
|
7
|
+
* use: block accidental navigation away from a form with unsaved
|
|
8
|
+
* changes. Only covers full-page unload — SPA route changes need a
|
|
9
|
+
* router guard. The dialog text is controlled by the browser and cannot
|
|
10
|
+
* be customized.
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta = {
|
|
13
|
+
title: "hooks/usePreventPageClose",
|
|
14
|
+
tags: ["beta"],
|
|
15
|
+
args: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
},
|
|
18
|
+
argTypes: {
|
|
19
|
+
enabled: {
|
|
20
|
+
control: "boolean",
|
|
21
|
+
description:
|
|
22
|
+
"When true, closing or reloading the tab triggers the native confirmation dialog.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
export const Default = {
|
|
30
|
+
render: (args: { enabled: boolean }) => {
|
|
31
|
+
usePreventPageClose(args.enabled);
|
|
32
|
+
return (
|
|
33
|
+
<p className="text-sm">
|
|
34
|
+
Try to close or reload this tab — the browser will ask for confirmation
|
|
35
|
+
while <code>enabled</code> is on.
|
|
36
|
+
</p>
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { usePreventPageClose } from "./use-prevent-page-close";
|
|
4
|
+
|
|
5
|
+
describe("usePreventPageClose", () => {
|
|
6
|
+
let addSpy: ReturnType<typeof vi.spyOn>;
|
|
7
|
+
let removeSpy: ReturnType<typeof vi.spyOn>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
addSpy = vi.spyOn(window, "addEventListener");
|
|
11
|
+
removeSpy = vi.spyOn(window, "removeEventListener");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
addSpy.mockRestore();
|
|
16
|
+
removeSpy.mockRestore();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("registers a beforeunload listener when enabled", () => {
|
|
20
|
+
renderHook(() => usePreventPageClose(true));
|
|
21
|
+
expect(addSpy).toHaveBeenCalledWith("beforeunload", expect.any(Function));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("does NOT register a listener when disabled", () => {
|
|
25
|
+
renderHook(() => usePreventPageClose(false));
|
|
26
|
+
expect(addSpy).not.toHaveBeenCalledWith(
|
|
27
|
+
"beforeunload",
|
|
28
|
+
expect.any(Function),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("removes the listener on unmount", () => {
|
|
33
|
+
const { unmount } = renderHook(() => usePreventPageClose(true));
|
|
34
|
+
unmount();
|
|
35
|
+
expect(removeSpy).toHaveBeenCalledWith(
|
|
36
|
+
"beforeunload",
|
|
37
|
+
expect.any(Function),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("calls preventDefault inside the handler", () => {
|
|
42
|
+
renderHook(() => usePreventPageClose(true));
|
|
43
|
+
|
|
44
|
+
const handler = addSpy.mock.calls.find(
|
|
45
|
+
([event]: [string, EventListenerOrEventListenerObject]) =>
|
|
46
|
+
event === "beforeunload",
|
|
47
|
+
)?.[1] as EventListener;
|
|
48
|
+
|
|
49
|
+
const event = new Event("beforeunload");
|
|
50
|
+
const preventSpy = vi.spyOn(event, "preventDefault");
|
|
51
|
+
handler(event);
|
|
52
|
+
|
|
53
|
+
expect(preventSpy).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("registers the listener once enabled flips to true", () => {
|
|
57
|
+
const { rerender } = renderHook(
|
|
58
|
+
({ enabled }) => usePreventPageClose(enabled),
|
|
59
|
+
{
|
|
60
|
+
initialProps: { enabled: false },
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(addSpy).not.toHaveBeenCalledWith(
|
|
65
|
+
"beforeunload",
|
|
66
|
+
expect.any(Function),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
rerender({ enabled: true });
|
|
70
|
+
|
|
71
|
+
expect(addSpy).toHaveBeenCalledWith("beforeunload", expect.any(Function));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("tears down the listener when enabled flips back to false", () => {
|
|
75
|
+
const { rerender } = renderHook(
|
|
76
|
+
({ enabled }) => usePreventPageClose(enabled),
|
|
77
|
+
{
|
|
78
|
+
initialProps: { enabled: true },
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
rerender({ enabled: false });
|
|
83
|
+
|
|
84
|
+
expect(removeSpy).toHaveBeenCalledWith(
|
|
85
|
+
"beforeunload",
|
|
86
|
+
expect.any(Function),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { isBrowser } from "../internal";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shows the browser's native "Leave site?" confirmation when the user
|
|
6
|
+
* tries to close the tab or reload the page while `enabled` is `true`.
|
|
7
|
+
*
|
|
8
|
+
* Note: this only covers full-page unload (tab close / reload). It does
|
|
9
|
+
* NOT intercept SPA route changes — for that, combine with your router's
|
|
10
|
+
* navigation guard. The dialog text is controlled by the browser and
|
|
11
|
+
* cannot be customized.
|
|
12
|
+
*/
|
|
13
|
+
export function usePreventPageClose(enabled: boolean): void {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!isBrowser || !enabled) return;
|
|
16
|
+
|
|
17
|
+
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
// Required by Chrome to trigger the native dialog. The string is
|
|
20
|
+
// ignored by modern browsers, which show their own generic message.
|
|
21
|
+
event.returnValue = "";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
25
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
26
|
+
}, [enabled]);
|
|
27
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderHook } from "@testing-library/react";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { DOTS, useRangePagination } from "../use-range-pagination";
|
|
4
4
|
|
|
5
5
|
describe("usePagination hook", () => {
|
|
6
6
|
it("should be defined", () => {
|
|
@@ -2,6 +2,7 @@ import { useMemo } from "react";
|
|
|
2
2
|
|
|
3
3
|
const range = (start: number, end: number): number[] =>
|
|
4
4
|
Array.from({ length: end - start }, (_, i) => i + start);
|
|
5
|
+
|
|
5
6
|
import { usePagination, type usePaginationProps } from "../../hooks";
|
|
6
7
|
|
|
7
8
|
export const DOTS = "...";
|
|
@@ -70,4 +71,3 @@ export const useRangePagination = ({
|
|
|
70
71
|
paginationRange: paginationRange ?? [],
|
|
71
72
|
};
|
|
72
73
|
};
|
|
73
|
-
|