@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,56 +1,61 @@
|
|
|
1
|
-
import { Meta } from "@storybook/react-vite";
|
|
1
|
+
import type { Meta } from "@storybook/react-vite";
|
|
2
2
|
import { Trash } from "lucide-react";
|
|
3
3
|
import { Button, Input } from "../../components";
|
|
4
4
|
import { useLocalStorage } from "../use-local-storage";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param key - La clave del localStorage donde se almacenará el valor.
|
|
14
|
-
* @param initialValue - El valor inicial que se utilizará si no hay datos almacenados bajo la clave proporcionada.
|
|
15
|
-
* @returns Una tupla con el valor almacenado, una función para actualizarlo y otra para eliminarlo.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* const [name, setName, removeName] = useLocalStorage<string>("name", "John Doe");
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* const [count, setCount, removeCount] = useLocalStorage<number>("count", 0);
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* const [items, setItems, removeItems] = useLocalStorage<string[]>("items", ["apple", "banana"]);
|
|
7
|
+
* Persists a value in `window.localStorage` and stays in sync across
|
|
8
|
+
* every instance reading the same key (in the same tab and across tabs
|
|
9
|
+
* via the `storage` event). Returns `[value, setValue, remove]`. SSR-safe:
|
|
10
|
+
* during server render the value falls back to `initialValue` and nothing
|
|
11
|
+
* is written. Values are JSON-serialized by default; pass a `serializer`
|
|
12
|
+
* option to customize.
|
|
25
13
|
*/
|
|
26
14
|
const meta: Meta = {
|
|
27
15
|
title: "hooks/useLocalStorage",
|
|
16
|
+
tags: ["beta"],
|
|
28
17
|
};
|
|
29
18
|
|
|
30
19
|
export default meta;
|
|
31
20
|
|
|
32
21
|
export const Default = {
|
|
33
22
|
render: () => {
|
|
34
|
-
const [value, setValue, removeValue] = useLocalStorage<string>(
|
|
23
|
+
const [value, setValue, removeValue] = useLocalStorage<string>(
|
|
24
|
+
"demo:name",
|
|
25
|
+
"",
|
|
26
|
+
);
|
|
35
27
|
|
|
36
28
|
return (
|
|
37
29
|
<div className="space-y-2">
|
|
38
|
-
<Input value={value} onValueChange={setValue} />
|
|
30
|
+
<Input value={value} onValueChange={setValue} placeholder="Type…" />
|
|
39
31
|
<Button onClick={removeValue}>Clear</Button>
|
|
40
32
|
</div>
|
|
41
33
|
);
|
|
42
34
|
},
|
|
43
35
|
};
|
|
44
36
|
|
|
45
|
-
|
|
37
|
+
/**
|
|
38
|
+
* `setValue` accepts a functional updater that receives the previous
|
|
39
|
+
* value, matching the `useState` signature. Safe for rapid consecutive
|
|
40
|
+
* updates because the closure always reads the latest value.
|
|
41
|
+
*/
|
|
42
|
+
export const FunctionalUpdater = {
|
|
46
43
|
render: () => {
|
|
47
|
-
const [count, setCount, removeCount] = useLocalStorage<number>(
|
|
44
|
+
const [count, setCount, removeCount] = useLocalStorage<number>(
|
|
45
|
+
"demo:count",
|
|
46
|
+
0,
|
|
47
|
+
);
|
|
48
48
|
|
|
49
49
|
return (
|
|
50
50
|
<div className="space-y-2">
|
|
51
51
|
<p>Count: {count}</p>
|
|
52
52
|
<div className="flex gap-2">
|
|
53
|
-
<Button onClick={() => setCount(
|
|
53
|
+
<Button onClick={() => setCount((prev) => (prev ?? 0) + 1)}>
|
|
54
|
+
Increment
|
|
55
|
+
</Button>
|
|
56
|
+
<Button onClick={() => setCount((prev) => (prev ?? 0) - 1)}>
|
|
57
|
+
Decrement
|
|
58
|
+
</Button>
|
|
54
59
|
<Button onClick={removeCount}>Reset</Button>
|
|
55
60
|
</div>
|
|
56
61
|
</div>
|
|
@@ -58,22 +63,117 @@ export const WithNumber = {
|
|
|
58
63
|
},
|
|
59
64
|
};
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Two hooks reading the same key stay synchronized in the same tab.
|
|
68
|
+
* Updating from one component updates the other on the next render —
|
|
69
|
+
* no prop drilling required.
|
|
70
|
+
*/
|
|
71
|
+
export const CrossInstanceSync = {
|
|
72
|
+
render: () => {
|
|
73
|
+
const [a, setA] = useLocalStorage<string>("demo:shared", "");
|
|
74
|
+
const [b, setB] = useLocalStorage<string>("demo:shared", "");
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="space-y-4">
|
|
78
|
+
<div className="space-y-1">
|
|
79
|
+
<p className="text-muted-foreground text-sm">Instance A</p>
|
|
80
|
+
<Input value={a} onValueChange={setA} />
|
|
81
|
+
</div>
|
|
82
|
+
<div className="space-y-1">
|
|
83
|
+
<p className="text-muted-foreground text-sm">Instance B</p>
|
|
84
|
+
<Input value={b} onValueChange={setB} />
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Open this story in two browser tabs at the same URL and edit the
|
|
93
|
+
* input in one tab. The other tab updates immediately via the native
|
|
94
|
+
* `storage` event.
|
|
95
|
+
*/
|
|
96
|
+
export const CrossTabSync = {
|
|
97
|
+
render: () => {
|
|
98
|
+
const [value, setValue, removeValue] = useLocalStorage<string>(
|
|
99
|
+
"demo:cross-tab",
|
|
100
|
+
"",
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="space-y-2">
|
|
105
|
+
<Input
|
|
106
|
+
value={value}
|
|
107
|
+
onValueChange={setValue}
|
|
108
|
+
placeholder="Edit in one tab, watch the other"
|
|
109
|
+
/>
|
|
110
|
+
<Button onClick={removeValue}>Clear</Button>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Provide a custom `serializer` to control how values are written to
|
|
118
|
+
* and read from storage. Useful for non-JSON formats, date objects,
|
|
119
|
+
* or encrypted payloads.
|
|
120
|
+
*
|
|
121
|
+
* ```tsx
|
|
122
|
+
* const dateSerializer = {
|
|
123
|
+
* read: (raw: string) => new Date(raw),
|
|
124
|
+
* write: (value: Date) => value.toISOString(),
|
|
125
|
+
* };
|
|
126
|
+
*
|
|
127
|
+
* useLocalStorage<Date>("demo:date", new Date(), {
|
|
128
|
+
* serializer: dateSerializer,
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export const CustomSerializer = {
|
|
133
|
+
render: () => {
|
|
134
|
+
const upperCase = {
|
|
135
|
+
read: (raw: string) => raw.toUpperCase(),
|
|
136
|
+
write: (value: string) => value.toLowerCase(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const [value, setValue, remove] = useLocalStorage<string>(
|
|
140
|
+
"demo:upper",
|
|
141
|
+
"INITIAL",
|
|
142
|
+
{ serializer: upperCase },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Input value={value} onValueChange={setValue} />
|
|
148
|
+
<p className="text-muted-foreground text-sm">
|
|
149
|
+
Stored on disk: written lowercase, read uppercase.
|
|
150
|
+
</p>
|
|
151
|
+
<Button onClick={remove}>Clear</Button>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Works with any JSON-serializable shape. Reading a non-JSON value
|
|
159
|
+
* logs a console error and falls back to `initialValue`.
|
|
160
|
+
*/
|
|
161
|
+
export const ArrayValue = {
|
|
62
162
|
render: () => {
|
|
63
163
|
const [items, setItems, removeItems] = useLocalStorage<string[]>(
|
|
64
|
-
"oranges",
|
|
164
|
+
"demo:oranges",
|
|
65
165
|
["🍊"],
|
|
66
166
|
);
|
|
67
167
|
|
|
68
168
|
return (
|
|
69
169
|
<div className="space-y-2">
|
|
70
170
|
<ul>
|
|
71
|
-
{items
|
|
72
|
-
<li key={index}>{item}</li>
|
|
171
|
+
{items?.map((item, index) => (
|
|
172
|
+
<li key={`${item}-${index}`}>{item}</li>
|
|
73
173
|
))}
|
|
74
174
|
</ul>
|
|
75
175
|
<div className="flex gap-2">
|
|
76
|
-
<Button onClick={() => setItems([...
|
|
176
|
+
<Button onClick={() => setItems((prev) => [...(prev ?? []), "🍊"])}>
|
|
77
177
|
🍊 Add Orange
|
|
78
178
|
</Button>
|
|
79
179
|
<Button onClick={removeItems}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { act, renderHook } from "@testing-library/react";
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { useLocalStorage } from "../use-local-storage";
|
|
4
4
|
|
|
5
5
|
describe("useLocalStorage hook", () => {
|
|
@@ -9,11 +9,16 @@ describe("useLocalStorage hook", () => {
|
|
|
9
9
|
localStorage.clear();
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.unstubAllGlobals();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
12
17
|
it("should be defined", () => {
|
|
13
18
|
expect(useLocalStorage).toBeDefined();
|
|
14
19
|
});
|
|
15
20
|
|
|
16
|
-
it("
|
|
21
|
+
it("returns initialValue when localStorage is empty", () => {
|
|
17
22
|
const { result } = renderHook(() =>
|
|
18
23
|
useLocalStorage<string>(key, "initialValue"),
|
|
19
24
|
);
|
|
@@ -22,7 +27,12 @@ describe("useLocalStorage hook", () => {
|
|
|
22
27
|
expect(storedValue).toBe("initialValue");
|
|
23
28
|
});
|
|
24
29
|
|
|
25
|
-
it("
|
|
30
|
+
it("does NOT write initialValue to localStorage on mount", () => {
|
|
31
|
+
renderHook(() => useLocalStorage<string>(key, "initialValue"));
|
|
32
|
+
expect(localStorage.getItem(key)).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("reads an existing stored value from localStorage", () => {
|
|
26
36
|
localStorage.setItem(key, JSON.stringify("storedValue"));
|
|
27
37
|
|
|
28
38
|
const { result } = renderHook(() =>
|
|
@@ -33,14 +43,13 @@ describe("useLocalStorage hook", () => {
|
|
|
33
43
|
expect(storedValue).toBe("storedValue");
|
|
34
44
|
});
|
|
35
45
|
|
|
36
|
-
it("
|
|
46
|
+
it("setValue updates the value and writes to localStorage", () => {
|
|
37
47
|
const { result } = renderHook(() =>
|
|
38
48
|
useLocalStorage<string>(key, "initialValue"),
|
|
39
49
|
);
|
|
40
50
|
|
|
41
|
-
const [, setValue] = result.current;
|
|
42
|
-
|
|
43
51
|
act(() => {
|
|
52
|
+
const [, setValue] = result.current;
|
|
44
53
|
setValue("newValue");
|
|
45
54
|
});
|
|
46
55
|
|
|
@@ -49,54 +58,47 @@ describe("useLocalStorage hook", () => {
|
|
|
49
58
|
expect(localStorage.getItem(key)).toBe(JSON.stringify("newValue"));
|
|
50
59
|
});
|
|
51
60
|
|
|
52
|
-
it("
|
|
61
|
+
it("setValue accepts a functional updater", () => {
|
|
62
|
+
const { result } = renderHook(() => useLocalStorage<number>(key, 1));
|
|
63
|
+
|
|
64
|
+
act(() => {
|
|
65
|
+
const [, setValue] = result.current;
|
|
66
|
+
setValue((prev) => (prev ?? 0) + 5);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.current[0]).toBe(6);
|
|
70
|
+
expect(localStorage.getItem(key)).toBe("6");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("remove() deletes the key and falls back to initialValue", () => {
|
|
53
74
|
localStorage.setItem(key, JSON.stringify("storedValue"));
|
|
54
75
|
|
|
55
76
|
const { result } = renderHook(() => useLocalStorage<string>(key, ""));
|
|
56
77
|
|
|
57
|
-
const [, , remove] = result.current;
|
|
58
|
-
|
|
59
78
|
act(() => {
|
|
79
|
+
const [, , remove] = result.current;
|
|
60
80
|
remove();
|
|
61
81
|
});
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
expect(
|
|
83
|
+
expect(result.current[0]).toBe("");
|
|
84
|
+
expect(localStorage.getItem(key)).toBeNull();
|
|
65
85
|
});
|
|
66
86
|
|
|
67
|
-
it("
|
|
87
|
+
it("falls back to initialValue and logs when stored value is not JSON", () => {
|
|
68
88
|
const consoleErrorSpy = vi
|
|
69
89
|
.spyOn(console, "error")
|
|
70
|
-
.mockImplementation(() => {
|
|
71
|
-
console.log("error");
|
|
72
|
-
});
|
|
90
|
+
.mockImplementation(() => {});
|
|
73
91
|
localStorage.setItem(key, "non-JSON-string");
|
|
74
92
|
|
|
75
93
|
const { result } = renderHook(() =>
|
|
76
94
|
useLocalStorage<string>(key, "initialValue"),
|
|
77
95
|
);
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
expect(storedValue).toBe("initialValue");
|
|
97
|
+
expect(result.current[0]).toBe("initialValue");
|
|
81
98
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
82
|
-
|
|
83
|
-
consoleErrorSpy.mockRestore();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("calls removeItem when storedValue is undefined (no initial value)", () => {
|
|
87
|
-
const removeItemMock = vi.fn();
|
|
88
|
-
vi.stubGlobal("localStorage", {
|
|
89
|
-
getItem: vi.fn().mockReturnValue(null),
|
|
90
|
-
setItem: vi.fn(),
|
|
91
|
-
removeItem: removeItemMock,
|
|
92
|
-
clear: vi.fn(),
|
|
93
|
-
});
|
|
94
|
-
renderHook(() => useLocalStorage<string>("empty-key"));
|
|
95
|
-
expect(removeItemMock).toHaveBeenCalledWith("empty-key");
|
|
96
|
-
vi.unstubAllGlobals();
|
|
97
99
|
});
|
|
98
100
|
|
|
99
|
-
it("
|
|
101
|
+
it("logs when localStorage.setItem throws during setValue", () => {
|
|
100
102
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
101
103
|
vi.stubGlobal("localStorage", {
|
|
102
104
|
getItem: vi.fn().mockReturnValue(null),
|
|
@@ -106,13 +108,20 @@ describe("useLocalStorage hook", () => {
|
|
|
106
108
|
removeItem: vi.fn(),
|
|
107
109
|
clear: vi.fn(),
|
|
108
110
|
});
|
|
109
|
-
|
|
111
|
+
|
|
112
|
+
const { result } = renderHook(() =>
|
|
113
|
+
useLocalStorage<string>("err-key", "value"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
act(() => {
|
|
117
|
+
const [, setValue] = result.current;
|
|
118
|
+
setValue("next");
|
|
119
|
+
});
|
|
120
|
+
|
|
110
121
|
expect(errorSpy).toHaveBeenCalled();
|
|
111
|
-
vi.unstubAllGlobals();
|
|
112
|
-
errorSpy.mockRestore();
|
|
113
122
|
});
|
|
114
123
|
|
|
115
|
-
it("
|
|
124
|
+
it("logs when localStorage.removeItem throws during remove()", () => {
|
|
116
125
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
117
126
|
vi.stubGlobal("localStorage", {
|
|
118
127
|
getItem: vi.fn().mockReturnValue(JSON.stringify("value")),
|
|
@@ -122,10 +131,66 @@ describe("useLocalStorage hook", () => {
|
|
|
122
131
|
}),
|
|
123
132
|
clear: vi.fn(),
|
|
124
133
|
});
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
|
|
135
|
+
const { result } = renderHook(() =>
|
|
136
|
+
useLocalStorage<string>("rm-key", "value"),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
act(() => {
|
|
140
|
+
result.current[2]();
|
|
141
|
+
});
|
|
142
|
+
|
|
127
143
|
expect(errorSpy).toHaveBeenCalled();
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("syncs two instances of the hook on the same key in the same tab", () => {
|
|
147
|
+
const a = renderHook(() => useLocalStorage<string>(key, "init"));
|
|
148
|
+
const b = renderHook(() => useLocalStorage<string>(key, "init"));
|
|
149
|
+
|
|
150
|
+
act(() => {
|
|
151
|
+
a.result.current[1]("from-a");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(a.result.current[0]).toBe("from-a");
|
|
155
|
+
expect(b.result.current[0]).toBe("from-a");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("reacts to a cross-tab `storage` event", () => {
|
|
159
|
+
const { result } = renderHook(() =>
|
|
160
|
+
useLocalStorage<string>(key, "initial"),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
localStorage.setItem(key, JSON.stringify("changed-externally"));
|
|
165
|
+
// jsdom rejects `storageArea` in StorageEventInit, so assign it after
|
|
166
|
+
// construction to satisfy the hook's `event.storageArea === localStorage`
|
|
167
|
+
// guard.
|
|
168
|
+
const event = new StorageEvent("storage", {
|
|
169
|
+
key,
|
|
170
|
+
newValue: JSON.stringify("changed-externally"),
|
|
171
|
+
});
|
|
172
|
+
Object.defineProperty(event, "storageArea", { value: localStorage });
|
|
173
|
+
window.dispatchEvent(event);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(result.current[0]).toBe("changed-externally");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("supports a custom serializer", () => {
|
|
180
|
+
const serializer = {
|
|
181
|
+
read: (raw: string) => raw.toUpperCase(),
|
|
182
|
+
write: (value: string) => value.toLowerCase(),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const { result } = renderHook(() =>
|
|
186
|
+
useLocalStorage<string>(key, "INITIAL", { serializer }),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
act(() => {
|
|
190
|
+
result.current[1]("MixedCase");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(localStorage.getItem(key)).toBe("mixedcase");
|
|
194
|
+
expect(result.current[0]).toBe("MIXEDCASE");
|
|
130
195
|
});
|
|
131
196
|
});
|
|
@@ -1,56 +1,125 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import { isBrowser } from "../internal";
|
|
3
|
+
|
|
4
|
+
export interface Serializer<T> {
|
|
5
|
+
read: (raw: string) => T;
|
|
6
|
+
write: (value: T) => string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseLocalStorageOptions<T> {
|
|
10
|
+
serializer?: Serializer<T>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const defaultSerializer: Serializer<unknown> = {
|
|
14
|
+
read: (raw) => JSON.parse(raw) as unknown,
|
|
15
|
+
write: (value) => JSON.stringify(value),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Listener = () => void;
|
|
19
|
+
const listeners = new Map<string, Set<Listener>>();
|
|
20
|
+
|
|
21
|
+
function notifyKey(key: string): void {
|
|
22
|
+
listeners.get(key)?.forEach((listener) => listener());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function subscribeKey(key: string, listener: Listener): () => void {
|
|
26
|
+
if (!isBrowser) return () => {};
|
|
27
|
+
|
|
28
|
+
let bucket = listeners.get(key);
|
|
29
|
+
if (!bucket) {
|
|
30
|
+
bucket = new Set();
|
|
31
|
+
listeners.set(key, bucket);
|
|
32
|
+
}
|
|
33
|
+
bucket.add(listener);
|
|
34
|
+
|
|
35
|
+
const onStorage = (event: StorageEvent) => {
|
|
36
|
+
if (event.storageArea !== localStorage) return;
|
|
37
|
+
if (event.key === key || event.key === null) listener();
|
|
38
|
+
};
|
|
39
|
+
window.addEventListener("storage", onStorage);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
bucket.delete(listener);
|
|
43
|
+
if (bucket.size === 0) listeners.delete(key);
|
|
44
|
+
window.removeEventListener("storage", onStorage);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
2
47
|
|
|
3
48
|
export function useLocalStorage<T>(
|
|
4
49
|
key: string,
|
|
5
|
-
): [
|
|
50
|
+
): [
|
|
51
|
+
T | undefined,
|
|
52
|
+
(value: T | ((prev: T | undefined) => T)) => void,
|
|
53
|
+
() => void,
|
|
54
|
+
];
|
|
6
55
|
export function useLocalStorage<T>(
|
|
7
56
|
key: string,
|
|
8
57
|
initialValue: T,
|
|
9
|
-
|
|
58
|
+
options?: UseLocalStorageOptions<T>,
|
|
59
|
+
): [T, (value: T | ((prev: T) => T)) => void, () => void];
|
|
10
60
|
export function useLocalStorage<T>(
|
|
11
61
|
key: string,
|
|
12
62
|
initialValue?: T,
|
|
13
|
-
|
|
14
|
-
|
|
63
|
+
options: UseLocalStorageOptions<T> = {},
|
|
64
|
+
): [
|
|
65
|
+
T | undefined,
|
|
66
|
+
(value: T | ((prev: T | undefined) => T)) => void,
|
|
67
|
+
() => void,
|
|
68
|
+
] {
|
|
69
|
+
const serializer = (options.serializer ?? defaultSerializer) as Serializer<T>;
|
|
70
|
+
|
|
71
|
+
const subscribe = useCallback(
|
|
72
|
+
(listener: Listener) => subscribeKey(key, listener),
|
|
73
|
+
[key],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const getSnapshot = useCallback((): string | null => {
|
|
77
|
+
if (!isBrowser) return null;
|
|
15
78
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return initialValue !== undefined ? initialValue : undefined;
|
|
21
|
-
} catch (error) {
|
|
22
|
-
console.error(`Error parsing localStorage key "${key}":`, error);
|
|
23
|
-
return initialValue !== undefined ? initialValue : undefined;
|
|
79
|
+
return localStorage.getItem(key);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
24
82
|
}
|
|
25
|
-
};
|
|
83
|
+
}, [key]);
|
|
26
84
|
|
|
27
|
-
const
|
|
85
|
+
const getServerSnapshot = useCallback((): string | null => null, []);
|
|
28
86
|
|
|
29
|
-
|
|
87
|
+
const raw = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
88
|
+
|
|
89
|
+
const value = useMemo<T | undefined>(() => {
|
|
90
|
+
if (raw === null) return initialValue;
|
|
30
91
|
try {
|
|
31
|
-
|
|
32
|
-
localStorage.setItem(key, JSON.stringify(storedValue));
|
|
33
|
-
} else {
|
|
34
|
-
localStorage.removeItem(key);
|
|
35
|
-
}
|
|
92
|
+
return serializer.read(raw);
|
|
36
93
|
} catch (error) {
|
|
37
|
-
console.error(`Error
|
|
94
|
+
console.error(`Error parsing localStorage key "${key}":`, error);
|
|
95
|
+
return initialValue;
|
|
38
96
|
}
|
|
39
|
-
}, [key,
|
|
97
|
+
}, [raw, initialValue, key, serializer]);
|
|
40
98
|
|
|
41
|
-
const setValue = (
|
|
42
|
-
|
|
43
|
-
|
|
99
|
+
const setValue = useCallback(
|
|
100
|
+
(next: T | ((prev: T | undefined) => T)) => {
|
|
101
|
+
try {
|
|
102
|
+
const resolved =
|
|
103
|
+
typeof next === "function"
|
|
104
|
+
? (next as (prev: T | undefined) => T)(value)
|
|
105
|
+
: next;
|
|
106
|
+
localStorage.setItem(key, serializer.write(resolved));
|
|
107
|
+
notifyKey(key);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(`Error setting localStorage key "${key}":`, error);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
[key, value, serializer],
|
|
113
|
+
);
|
|
44
114
|
|
|
45
|
-
const remove = () => {
|
|
115
|
+
const remove = useCallback(() => {
|
|
46
116
|
try {
|
|
47
117
|
localStorage.removeItem(key);
|
|
48
|
-
|
|
118
|
+
notifyKey(key);
|
|
49
119
|
} catch (error) {
|
|
50
120
|
console.error(`Error removing localStorage key "${key}":`, error);
|
|
51
121
|
}
|
|
52
|
-
};
|
|
122
|
+
}, [key]);
|
|
53
123
|
|
|
54
|
-
return [
|
|
124
|
+
return [value, setValue, remove];
|
|
55
125
|
}
|
|
56
|
-
|