@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
|
@@ -3,70 +3,218 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { useDisclosure } from "../use-disclosure";
|
|
4
4
|
|
|
5
5
|
describe("useDisclosure", () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// ── Return shape ──────────────────────────────────────────────────────────
|
|
7
|
+
describe("return shape", () => {
|
|
8
|
+
it("returns a two-element tuple [boolean, actions]", () => {
|
|
9
|
+
const { result } = renderHook(() => useDisclosure());
|
|
10
|
+
expect(Array.isArray(result.current)).toBe(true);
|
|
11
|
+
expect(result.current).toHaveLength(2);
|
|
12
|
+
expect(typeof result.current[0]).toBe("boolean");
|
|
13
|
+
const actions = result.current[1];
|
|
14
|
+
expect(typeof actions.open).toBe("function");
|
|
15
|
+
expect(typeof actions.close).toBe("function");
|
|
16
|
+
expect(typeof actions.toggle).toBe("function");
|
|
17
|
+
expect(typeof actions.setOpen).toBe("function");
|
|
18
|
+
});
|
|
9
19
|
});
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
// ── Uncontrolled mode ─────────────────────────────────────────────────────
|
|
22
|
+
describe("uncontrolled mode", () => {
|
|
23
|
+
it("defaults to closed (opened === false) when no args passed", () => {
|
|
24
|
+
const { result } = renderHook(() => useDisclosure());
|
|
25
|
+
expect(result.current[0]).toBe(false);
|
|
26
|
+
});
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
28
|
+
it("starts open when defaultOpen: true", () => {
|
|
29
|
+
const { result } = renderHook(() => useDisclosure({ defaultOpen: true }));
|
|
30
|
+
expect(result.current[0]).toBe(true);
|
|
31
|
+
});
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
it("open() sets opened to true", async () => {
|
|
34
|
+
const { result } = renderHook(() => useDisclosure());
|
|
35
|
+
await act(async () => {
|
|
36
|
+
result.current[1].open();
|
|
37
|
+
});
|
|
38
|
+
expect(result.current[0]).toBe(true);
|
|
39
|
+
});
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
it("close() sets opened to false", async () => {
|
|
42
|
+
const { result } = renderHook(() => useDisclosure({ defaultOpen: true }));
|
|
43
|
+
await act(async () => {
|
|
44
|
+
result.current[1].close();
|
|
45
|
+
});
|
|
46
|
+
expect(result.current[0]).toBe(false);
|
|
47
|
+
});
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
it("toggle() flips false → true", async () => {
|
|
50
|
+
const { result } = renderHook(() => useDisclosure());
|
|
51
|
+
await act(async () => {
|
|
52
|
+
result.current[1].toggle();
|
|
53
|
+
});
|
|
54
|
+
expect(result.current[0]).toBe(true);
|
|
55
|
+
});
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
it("toggle() flips true → false", async () => {
|
|
58
|
+
const { result } = renderHook(() => useDisclosure({ defaultOpen: true }));
|
|
59
|
+
await act(async () => {
|
|
60
|
+
result.current[1].toggle();
|
|
61
|
+
});
|
|
62
|
+
expect(result.current[0]).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("setOpen(true) sets opened to true", async () => {
|
|
66
|
+
const { result } = renderHook(() => useDisclosure());
|
|
67
|
+
await act(async () => {
|
|
68
|
+
result.current[1].setOpen(true);
|
|
69
|
+
});
|
|
70
|
+
expect(result.current[0]).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("setOpen(false) sets opened to false", async () => {
|
|
74
|
+
const { result } = renderHook(() => useDisclosure({ defaultOpen: true }));
|
|
75
|
+
await act(async () => {
|
|
76
|
+
result.current[1].setOpen(false);
|
|
77
|
+
});
|
|
78
|
+
expect(result.current[0]).toBe(false);
|
|
79
|
+
});
|
|
44
80
|
});
|
|
45
81
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
// ── Idempotency ───────────────────────────────────────────────────────────
|
|
83
|
+
describe("idempotency", () => {
|
|
84
|
+
it("open() when already open does NOT call onOpenChange", async () => {
|
|
85
|
+
const onOpenChange = vi.fn();
|
|
86
|
+
const { result } = renderHook(() =>
|
|
87
|
+
useDisclosure({ defaultOpen: true, onOpenChange }),
|
|
88
|
+
);
|
|
89
|
+
await act(async () => {});
|
|
90
|
+
onOpenChange.mockClear();
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
result.current[1].open();
|
|
94
|
+
});
|
|
95
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("close() when already closed does NOT call onOpenChange", async () => {
|
|
99
|
+
const onOpenChange = vi.fn();
|
|
100
|
+
const { result } = renderHook(() => useDisclosure({ onOpenChange }));
|
|
101
|
+
await act(async () => {});
|
|
102
|
+
onOpenChange.mockClear();
|
|
103
|
+
|
|
104
|
+
await act(async () => {
|
|
105
|
+
result.current[1].close();
|
|
106
|
+
});
|
|
107
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("setOpen(same value) does NOT call onOpenChange", async () => {
|
|
111
|
+
const onOpenChange = vi.fn();
|
|
112
|
+
const { result } = renderHook(() =>
|
|
113
|
+
useDisclosure({ defaultOpen: true, onOpenChange }),
|
|
114
|
+
);
|
|
115
|
+
await act(async () => {});
|
|
116
|
+
onOpenChange.mockClear();
|
|
117
|
+
|
|
118
|
+
await act(async () => {
|
|
119
|
+
result.current[1].setOpen(true);
|
|
120
|
+
});
|
|
121
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
50
123
|
});
|
|
51
124
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
125
|
+
// ── onOpenChange ──────────────────────────────────────────────────────────
|
|
126
|
+
describe("onOpenChange", () => {
|
|
127
|
+
it("fires once with true on real transition via open()", async () => {
|
|
128
|
+
const onOpenChange = vi.fn();
|
|
129
|
+
const { result } = renderHook(() => useDisclosure({ onOpenChange }));
|
|
130
|
+
await act(async () => {
|
|
131
|
+
result.current[1].open();
|
|
132
|
+
});
|
|
133
|
+
expect(onOpenChange).toHaveBeenCalledOnce();
|
|
134
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("fires once with false on real transition via close()", async () => {
|
|
138
|
+
const onOpenChange = vi.fn();
|
|
139
|
+
const { result } = renderHook(() =>
|
|
140
|
+
useDisclosure({ defaultOpen: true, onOpenChange }),
|
|
141
|
+
);
|
|
142
|
+
await act(async () => {});
|
|
143
|
+
onOpenChange.mockClear();
|
|
144
|
+
|
|
145
|
+
await act(async () => {
|
|
146
|
+
result.current[1].close();
|
|
147
|
+
});
|
|
148
|
+
expect(onOpenChange).toHaveBeenCalledOnce();
|
|
149
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("fires once with new value on real transition via setOpen()", async () => {
|
|
153
|
+
const onOpenChange = vi.fn();
|
|
154
|
+
const { result } = renderHook(() => useDisclosure({ onOpenChange }));
|
|
155
|
+
await act(async () => {
|
|
156
|
+
result.current[1].setOpen(true);
|
|
157
|
+
});
|
|
158
|
+
expect(onOpenChange).toHaveBeenCalledOnce();
|
|
159
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
160
|
+
});
|
|
57
161
|
});
|
|
58
162
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
163
|
+
// ── Action reference stability ────────────────────────────────────────────
|
|
164
|
+
describe("action reference stability", () => {
|
|
165
|
+
it("open ref is stable after state change (uncontrolled)", async () => {
|
|
166
|
+
const { result } = renderHook(() => useDisclosure());
|
|
167
|
+
const before = result.current[1].open;
|
|
168
|
+
await act(async () => {
|
|
169
|
+
result.current[1].open();
|
|
170
|
+
});
|
|
171
|
+
expect(result.current[1].open).toBe(before);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("close ref is stable after state change (uncontrolled)", async () => {
|
|
175
|
+
const { result } = renderHook(() => useDisclosure({ defaultOpen: true }));
|
|
176
|
+
const before = result.current[1].close;
|
|
177
|
+
await act(async () => {
|
|
178
|
+
result.current[1].close();
|
|
179
|
+
});
|
|
180
|
+
expect(result.current[1].close).toBe(before);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("toggle ref is stable after state change (uncontrolled)", async () => {
|
|
184
|
+
const { result } = renderHook(() => useDisclosure());
|
|
185
|
+
const before = result.current[1].toggle;
|
|
186
|
+
await act(async () => {
|
|
187
|
+
result.current[1].toggle();
|
|
188
|
+
});
|
|
189
|
+
expect(result.current[1].toggle).toBe(before);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("setOpen ref is stable after state change (uncontrolled)", async () => {
|
|
193
|
+
const { result } = renderHook(() => useDisclosure());
|
|
194
|
+
const before = result.current[1].setOpen;
|
|
195
|
+
await act(async () => {
|
|
196
|
+
result.current[1].setOpen(true);
|
|
197
|
+
});
|
|
198
|
+
expect(result.current[1].setOpen).toBe(before);
|
|
199
|
+
});
|
|
64
200
|
});
|
|
65
201
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
202
|
+
// ── Cleanup on unmount ────────────────────────────────────────────────────
|
|
203
|
+
describe("cleanup on unmount", () => {
|
|
204
|
+
it("does not call onOpenChange after unmount", async () => {
|
|
205
|
+
const onOpenChange = vi.fn();
|
|
206
|
+
const { result, unmount } = renderHook(() =>
|
|
207
|
+
useDisclosure({ onOpenChange }),
|
|
208
|
+
);
|
|
209
|
+
unmount();
|
|
210
|
+
await act(async () => {
|
|
211
|
+
try {
|
|
212
|
+
result.current[1].open();
|
|
213
|
+
} catch {
|
|
214
|
+
// suppress any unmount-related errors
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
expect(onOpenChange).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
71
219
|
});
|
|
72
220
|
});
|
|
@@ -1,36 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useLatestRef } from "../internal";
|
|
3
|
+
|
|
4
|
+
export type UseDisclosureProps = {
|
|
5
|
+
/** Initial open value (default: false). */
|
|
6
|
+
defaultOpen?: boolean;
|
|
7
|
+
/** Called once per real state transition. Not called on no-op calls. */
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type UseDisclosureActions = {
|
|
12
|
+
/** Transitions state to true. No-op (and onOpenChange not called) if already open. */
|
|
13
|
+
open: () => void;
|
|
14
|
+
/** Transitions state to false. No-op (and onOpenChange not called) if already closed. */
|
|
15
|
+
close: () => void;
|
|
16
|
+
/** Flips the current boolean state. */
|
|
17
|
+
toggle: () => void;
|
|
18
|
+
/** Sets state to the given value. No-op (and onOpenChange not called) if value is unchanged. */
|
|
19
|
+
setOpen: (open: boolean) => void;
|
|
20
|
+
};
|
|
2
21
|
|
|
3
22
|
export function useDisclosure(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
const
|
|
8
|
-
const
|
|
23
|
+
props?: UseDisclosureProps,
|
|
24
|
+
): readonly [boolean, UseDisclosureActions] {
|
|
25
|
+
const [opened, setOpenState] = useState<boolean>(props?.defaultOpen ?? false);
|
|
26
|
+
const onOpenChangeRef = useLatestRef(props?.onOpenChange);
|
|
27
|
+
const mountedRef = useRef(true);
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return isOpened;
|
|
17
|
-
});
|
|
18
|
-
}, [onOpen]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
mountedRef.current = true;
|
|
31
|
+
return () => {
|
|
32
|
+
mountedRef.current = false;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
19
35
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
const actions = useMemo<UseDisclosureActions>(() => {
|
|
37
|
+
const transition = (compute: (prev: boolean) => boolean) =>
|
|
38
|
+
setOpenState((prev) => {
|
|
39
|
+
const next = compute(prev);
|
|
40
|
+
if (next === prev) return prev; // no-op: silent, no re-render
|
|
41
|
+
if (mountedRef.current) {
|
|
42
|
+
onOpenChangeRef.current?.(next);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
47
|
+
return {
|
|
48
|
+
open: () => transition(() => true),
|
|
49
|
+
close: () => transition(() => false),
|
|
50
|
+
toggle: () => transition((prev) => !prev),
|
|
51
|
+
setOpen: (next) => transition(() => next),
|
|
52
|
+
};
|
|
53
|
+
}, []); // setOpenState, onOpenChangeRef, and mountedRef are stable
|
|
33
54
|
|
|
34
|
-
return [
|
|
55
|
+
return [opened, actions] as const;
|
|
35
56
|
}
|
|
36
|
-
|
|
@@ -70,3 +70,57 @@ export const Default: Story = {
|
|
|
70
70
|
);
|
|
71
71
|
},
|
|
72
72
|
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Pass a `template` with a `%s` placeholder to wrap every title in a
|
|
76
|
+
* suffix or prefix — e.g. append the app name to the page name.
|
|
77
|
+
*
|
|
78
|
+
* ```tsx
|
|
79
|
+
* useDocumentTitle(pageName, { template: "%s | Acme" });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export const Template: Story = {
|
|
83
|
+
render: () => {
|
|
84
|
+
const [page, setPage] = useState("Inbox");
|
|
85
|
+
useDocumentTitle(page, { template: "%s | Acme" });
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Stack direction="vertical" gap={12} style={{ minWidth: 360 }}>
|
|
89
|
+
<Input value={page} onValueChange={setPage} placeholder="Page name…" />
|
|
90
|
+
<code className="rounded-md bg-muted px-3 py-2 text-xs">
|
|
91
|
+
{`document.title = "${page.trim() || "…"} | Acme"`}
|
|
92
|
+
</code>
|
|
93
|
+
</Stack>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set `enabled: false` to make the hook a no-op. The title is only
|
|
100
|
+
* written while `enabled` is `true`, so a component can own the tab
|
|
101
|
+
* title conditionally without unmounting.
|
|
102
|
+
*/
|
|
103
|
+
export const Enabled: Story = {
|
|
104
|
+
render: () => {
|
|
105
|
+
const [enabled, setEnabled] = useState(true);
|
|
106
|
+
useDocumentTitle("Live title", { enabled });
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Stack direction="vertical" gap={12} style={{ minWidth: 360 }}>
|
|
110
|
+
<label className="flex items-center gap-2 text-sm">
|
|
111
|
+
<input
|
|
112
|
+
type="checkbox"
|
|
113
|
+
checked={enabled}
|
|
114
|
+
onChange={(e) => setEnabled(e.target.checked)}
|
|
115
|
+
/>
|
|
116
|
+
enabled
|
|
117
|
+
</label>
|
|
118
|
+
<code className="rounded-md bg-muted px-3 py-2 text-xs">
|
|
119
|
+
{enabled
|
|
120
|
+
? 'document.title = "Live title"'
|
|
121
|
+
: "(disabled — current title preserved)"}
|
|
122
|
+
</code>
|
|
123
|
+
</Stack>
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
@@ -59,6 +59,32 @@ describe("useDocumentTitle", () => {
|
|
|
59
59
|
expect(document.title).toBe("Original");
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
it("applies a template with the %s placeholder", () => {
|
|
63
|
+
renderHook(() => useDocumentTitle("Inbox", { template: "%s | Acme" }));
|
|
64
|
+
expect(document.title).toBe("Inbox | Acme");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("trims before applying the template", () => {
|
|
68
|
+
renderHook(() => useDocumentTitle(" Inbox ", { template: "%s | Acme" }));
|
|
69
|
+
expect(document.title).toBe("Inbox | Acme");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not touch the title when enabled is false", () => {
|
|
73
|
+
renderHook(() => useDocumentTitle("New", { enabled: false }));
|
|
74
|
+
expect(document.title).toBe("Original");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("starts writing once enabled flips to true", () => {
|
|
78
|
+
const { rerender } = renderHook(
|
|
79
|
+
({ enabled }) => useDocumentTitle("New", { enabled }),
|
|
80
|
+
{ initialProps: { enabled: false } },
|
|
81
|
+
);
|
|
82
|
+
expect(document.title).toBe("Original");
|
|
83
|
+
|
|
84
|
+
rerender({ enabled: true });
|
|
85
|
+
expect(document.title).toBe("New");
|
|
86
|
+
});
|
|
87
|
+
|
|
62
88
|
it("captures the original title at mount, not the latest one", () => {
|
|
63
89
|
document.title = "First";
|
|
64
90
|
const { unmount, rerender } = renderHook(
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
+
import { isBrowser } from "../internal";
|
|
2
3
|
|
|
3
4
|
export interface UseDocumentTitleInput {
|
|
4
5
|
/** Restore the previous `document.title` when the component unmounts. Defaults to `false`. */
|
|
5
6
|
restoreOnUnmount?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Template applied to the title. The `%s` placeholder is replaced with
|
|
9
|
+
* the (trimmed) title value. Example: `"%s | Acme"` turns `"Inbox"`
|
|
10
|
+
* into `"Inbox | Acme"`. When omitted, the title is written as-is.
|
|
11
|
+
*/
|
|
12
|
+
template?: string;
|
|
13
|
+
/**
|
|
14
|
+
* When `false`, the hook does not touch `document.title`. Defaults to
|
|
15
|
+
* `true`. Useful to conditionally own the title.
|
|
16
|
+
*/
|
|
17
|
+
enabled?: boolean;
|
|
6
18
|
}
|
|
7
19
|
|
|
8
20
|
export function useDocumentTitle(
|
|
9
21
|
title: string,
|
|
10
22
|
options: UseDocumentTitleInput = {},
|
|
11
23
|
): void {
|
|
12
|
-
const { restoreOnUnmount = false } = options;
|
|
24
|
+
const { restoreOnUnmount = false, template, enabled = true } = options;
|
|
13
25
|
const restoreRef = useRef(restoreOnUnmount);
|
|
14
26
|
restoreRef.current = restoreOnUnmount;
|
|
15
27
|
|
|
16
28
|
// biome-ignore lint/correctness/useExhaustiveDependencies: mount/unmount only
|
|
17
29
|
useEffect(() => {
|
|
30
|
+
if (!isBrowser) return;
|
|
18
31
|
const previous = document.title;
|
|
19
32
|
return () => {
|
|
20
33
|
if (restoreRef.current) {
|
|
@@ -24,9 +37,10 @@ export function useDocumentTitle(
|
|
|
24
37
|
}, []);
|
|
25
38
|
|
|
26
39
|
useEffect(() => {
|
|
40
|
+
if (!isBrowser || !enabled) return;
|
|
27
41
|
if (typeof title !== "string") return;
|
|
28
42
|
const trimmed = title.trim();
|
|
29
43
|
if (trimmed.length === 0) return;
|
|
30
|
-
document.title = trimmed;
|
|
31
|
-
}, [title]);
|
|
44
|
+
document.title = template ? template.replace("%s", trimmed) : trimmed;
|
|
45
|
+
}, [title, template, enabled]);
|
|
32
46
|
}
|
|
@@ -1,28 +1,124 @@
|
|
|
1
|
-
import { Meta } from "@storybook/react-vite";
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
2
3
|
import { action } from "storybook/actions";
|
|
3
4
|
import { Button } from "../../components";
|
|
4
5
|
import { useEventListener } from "../use-event-listener";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Subscribe to a DOM event with automatic add/remove, without writing a
|
|
9
|
+
* manual `useEffect` + cleanup.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* The `listener` is held in an internal ref (handler-stable pattern): passing
|
|
12
|
+
* an inline function does **not** re-attach the underlying listener on every
|
|
13
|
+
* render, and the listener always sees fresh props/state when it fires.
|
|
14
|
+
*
|
|
15
|
+
* The target is **explicit** and chosen by overload — there is no returned
|
|
16
|
+
* ref to wire up:
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // window — omit the target
|
|
20
|
+
* useEventListener("resize", onResize);
|
|
21
|
+
*
|
|
22
|
+
* // an element — create your own ref and pass it
|
|
23
|
+
* const ref = useRef<HTMLButtonElement>(null);
|
|
24
|
+
* useEventListener("click", onClick, ref);
|
|
25
|
+
*
|
|
26
|
+
* // document — pass it directly (global key handlers, etc.)
|
|
27
|
+
* useEventListener("keydown", onKeyDown, document);
|
|
28
|
+
*
|
|
29
|
+
* // a MediaQueryList — react to media query changes
|
|
30
|
+
* const mql = window.matchMedia("(min-width: 768px)");
|
|
31
|
+
* useEventListener("change", onChange, mql);
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The fourth argument forwards the native `addEventListener` options
|
|
35
|
+
* (`{ capture, once, passive, signal }` or a boolean). The listener is
|
|
36
|
+
* re-attached when the `capture` / `once` / `passive` flags change.
|
|
37
|
+
*
|
|
38
|
+
* SSR-safe: the effect no-ops when there is no DOM.
|
|
39
|
+
*
|
|
40
|
+
* Migrating from v1: the hook no longer returns a ref. Replace
|
|
41
|
+
* `const ref = useEventListener("click", cb)` with an explicit ref —
|
|
42
|
+
* `const ref = useRef(null); useEventListener("click", cb, ref)`.
|
|
10
43
|
*/
|
|
11
44
|
const meta: Meta = {
|
|
12
45
|
title: "hooks/useEventListener",
|
|
13
46
|
};
|
|
14
47
|
|
|
15
48
|
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof meta>;
|
|
16
50
|
|
|
17
51
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* return <HTMLELEMENT ref={ref} />;
|
|
21
|
-
* ```
|
|
52
|
+
* Listen on an element by creating a ref yourself and passing it as the
|
|
53
|
+
* third argument. The hook attaches the listener once the element is mounted.
|
|
22
54
|
*/
|
|
23
|
-
export const
|
|
55
|
+
export const ElementByRef: Story = {
|
|
24
56
|
render: () => {
|
|
25
|
-
const ref =
|
|
57
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
58
|
+
useEventListener("click", action("click"), ref);
|
|
26
59
|
return <Button ref={ref}>click me</Button>;
|
|
27
60
|
},
|
|
28
61
|
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Omitting the target listens on `window`. Resize the preview pane to watch
|
|
65
|
+
* the value update — no manual subscription or cleanup required.
|
|
66
|
+
*/
|
|
67
|
+
export const WindowResize: Story = {
|
|
68
|
+
render: () => {
|
|
69
|
+
const [width, setWidth] = useState(window.innerWidth);
|
|
70
|
+
useEventListener("resize", () => setWidth(window.innerWidth));
|
|
71
|
+
return (
|
|
72
|
+
<p>
|
|
73
|
+
window width: <strong>{width}px</strong>
|
|
74
|
+
</p>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pass `document` directly for document-level events such as global keyboard
|
|
81
|
+
* shortcuts that should fire regardless of focus. The listener receives the
|
|
82
|
+
* native `KeyboardEvent`.
|
|
83
|
+
*/
|
|
84
|
+
export const DocumentKeydown: Story = {
|
|
85
|
+
render: () => {
|
|
86
|
+
const [key, setKey] = useState("—");
|
|
87
|
+
useEventListener("keydown", (event) => setKey(event.key), document);
|
|
88
|
+
return (
|
|
89
|
+
<p>
|
|
90
|
+
last key pressed: <kbd>{key}</kbd>
|
|
91
|
+
</p>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Pass a `MediaQueryList` (from `window.matchMedia`) to react to viewport
|
|
98
|
+
* changes. Create the `MediaQueryList` once so its identity stays stable and
|
|
99
|
+
* the listener is not re-attached on every render.
|
|
100
|
+
*/
|
|
101
|
+
export const MediaQueryChange: Story = {
|
|
102
|
+
render: () => {
|
|
103
|
+
const [mql] = useState(() => window.matchMedia("(min-width: 768px)"));
|
|
104
|
+
const [matches, setMatches] = useState(mql.matches);
|
|
105
|
+
useEventListener("change", (event) => setMatches(event.matches), mql);
|
|
106
|
+
return <p>{matches ? "≥ 768px (desktop)" : "< 768px (mobile)"}</p>;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The fourth argument is forwarded to `addEventListener`. Here `{ once: true }`
|
|
112
|
+
* makes the listener fire a single time and then detach itself — the counter
|
|
113
|
+
* never goes past one regardless of how many times you click.
|
|
114
|
+
*/
|
|
115
|
+
export const OnceOption: Story = {
|
|
116
|
+
render: () => {
|
|
117
|
+
const ref = useRef<HTMLButtonElement>(null);
|
|
118
|
+
const [count, setCount] = useState(0);
|
|
119
|
+
useEventListener("click", () => setCount((c) => c + 1), ref, {
|
|
120
|
+
once: true,
|
|
121
|
+
});
|
|
122
|
+
return <Button ref={ref}>clicked {count} time(s)</Button>;
|
|
123
|
+
},
|
|
124
|
+
};
|