@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,39 +1,209 @@
|
|
|
1
|
-
import { Meta } from "@storybook/react-vite";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { Meta } from "@storybook/react-vite";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { Button } from "../../components/button";
|
|
5
|
+
import { useClickOutside } from "./use-click-outside";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Fires a callback when a `pointerdown` (or any configured event) happens
|
|
9
|
+
* outside the returned ref. Listener lives on `document`, so the wrapped
|
|
10
|
+
* element does not need to receive focus or capture events itself.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Good to know:
|
|
17
|
+
* - **Default event is `pointerdown`** (handles mouse + touch + pen in one
|
|
18
|
+
* listener). Override via `events` if you need legacy `mousedown` or want
|
|
19
|
+
* to combine `mousedown` + `touchstart`.
|
|
20
|
+
* - **Callback identity does not matter.** The hook reads the callback
|
|
21
|
+
* through an internal ref, so an inline arrow function will not re-attach
|
|
22
|
+
* the listener every render.
|
|
23
|
+
* - **`enabled` controls subscription.** When `false` no listener is
|
|
24
|
+
* attached at all — cheaper than checking inside the callback, and the
|
|
25
|
+
* typical way to "pause" the hook while a popover is closed.
|
|
26
|
+
* - **Portals:** elements rendered into another part of the tree (modals,
|
|
27
|
+
* popovers, tooltips) are NOT inside the ref's subtree. Use
|
|
28
|
+
* `additionalRefs` to whitelist them, or `ignore(event)` for finer
|
|
29
|
+
* control (e.g. ignore clicks on a specific class).
|
|
30
|
+
* - **SSR-safe:** the effect early-returns on the server.
|
|
31
|
+
*
|
|
32
|
+
* Breaking change from v1: the second argument is now an options object
|
|
33
|
+
* (`enabled`, `events`, `ignore`, `additionalRefs`) instead of the boolean
|
|
34
|
+
* `watch`. Default event changed from `mousedown` to `pointerdown`.
|
|
35
|
+
*/
|
|
6
36
|
const meta: Meta = {
|
|
7
37
|
title: "hooks/useClickOutside",
|
|
38
|
+
parameters: { layout: "centered" },
|
|
39
|
+
tags: ["beta"],
|
|
8
40
|
};
|
|
9
41
|
|
|
10
42
|
export default meta;
|
|
11
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Open the popover with the button, then click anywhere outside the
|
|
46
|
+
* yellow card to close it. Clicking inside the card (including the
|
|
47
|
+
* "Close from inside" button) does not trigger the callback.
|
|
48
|
+
*/
|
|
12
49
|
export const Default = {
|
|
13
50
|
render: () => {
|
|
14
|
-
const [
|
|
51
|
+
const [open, setOpen] = useState(true);
|
|
52
|
+
const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
|
|
15
53
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex flex-col items-start gap-3">
|
|
56
|
+
<Button onClick={() => setOpen(true)} disabled={open}>
|
|
57
|
+
Open
|
|
58
|
+
</Button>
|
|
59
|
+
{open ? (
|
|
60
|
+
<div
|
|
61
|
+
ref={ref}
|
|
62
|
+
className="rounded-md border border-warning bg-warning/10 p-4 text-sm shadow-md"
|
|
63
|
+
>
|
|
64
|
+
<p className="mb-2">Click outside this card to dismiss it.</p>
|
|
65
|
+
<Button size="sm" onClick={() => setOpen(false)}>
|
|
66
|
+
Close from inside
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
) : (
|
|
70
|
+
<p className="text-sm text-muted-foreground">
|
|
71
|
+
Popover closed. Re-open with the button.
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
19
78
|
|
|
20
|
-
|
|
79
|
+
/**
|
|
80
|
+
* `enabled: false` removes the listener entirely. Useful when the consumer
|
|
81
|
+
* already has a separate "is open" flag — the hook does not need to be
|
|
82
|
+
* conditionally rendered, just toggled.
|
|
83
|
+
*
|
|
84
|
+
* ```tsx
|
|
85
|
+
* useClickOutside(close, { enabled: isOpen });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const Enabled = {
|
|
89
|
+
render: () => {
|
|
90
|
+
const [enabled, setEnabled] = useState(true);
|
|
91
|
+
const [hits, setHits] = useState(0);
|
|
92
|
+
const ref = useClickOutside<HTMLDivElement>(() => setHits((n) => n + 1), {
|
|
93
|
+
enabled,
|
|
94
|
+
});
|
|
21
95
|
|
|
22
96
|
return (
|
|
23
|
-
<div className="flex gap-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
97
|
+
<div className="flex flex-col items-start gap-3">
|
|
98
|
+
<label className="flex items-center gap-2 text-sm">
|
|
99
|
+
<input
|
|
100
|
+
type="checkbox"
|
|
101
|
+
checked={enabled}
|
|
102
|
+
onChange={(e) => setEnabled(e.target.checked)}
|
|
103
|
+
/>
|
|
104
|
+
listener enabled
|
|
105
|
+
</label>
|
|
106
|
+
<div
|
|
107
|
+
ref={ref}
|
|
108
|
+
className="rounded-md border border-input bg-card p-4 text-sm"
|
|
109
|
+
>
|
|
110
|
+
Click outside this box.
|
|
111
|
+
</div>
|
|
112
|
+
<code className="text-xs">outside clicks counted: {hits}</code>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Use `additionalRefs` when the popover has a separate trigger element
|
|
120
|
+
* that should NOT count as "outside". Without this, clicking the trigger
|
|
121
|
+
* to toggle the popover would immediately close it again because the
|
|
122
|
+
* button lives outside the popover's DOM subtree.
|
|
123
|
+
*
|
|
124
|
+
* ```tsx
|
|
125
|
+
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
126
|
+
* const popoverRef = useClickOutside<HTMLDivElement>(close, {
|
|
127
|
+
* additionalRefs: [triggerRef],
|
|
128
|
+
* });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export const AdditionalRefs = {
|
|
132
|
+
render: () => {
|
|
133
|
+
const [open, setOpen] = useState(false);
|
|
134
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
135
|
+
const popoverRef = useClickOutside<HTMLDivElement>(() => setOpen(false), {
|
|
136
|
+
additionalRefs: [triggerRef],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="flex flex-col items-start gap-3">
|
|
141
|
+
<Button ref={triggerRef} onClick={() => setOpen((v) => !v)}>
|
|
142
|
+
Toggle
|
|
29
143
|
</Button>
|
|
144
|
+
{open ? (
|
|
145
|
+
<div
|
|
146
|
+
ref={popoverRef}
|
|
147
|
+
className="rounded-md border border-input bg-card p-4 text-sm shadow-md"
|
|
148
|
+
>
|
|
149
|
+
Toggling the trigger does NOT close me, but clicking elsewhere does.
|
|
150
|
+
</div>
|
|
151
|
+
) : null}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* `ignore(event)` is the escape hatch for content rendered into a React
|
|
159
|
+
* portal — by definition that DOM is not under the ref's subtree, so a
|
|
160
|
+
* raw click on a tooltip / overlay / dropdown would close the parent.
|
|
161
|
+
* Returning `true` from `ignore` skips the callback for that event.
|
|
162
|
+
*
|
|
163
|
+
* Here, the green portal is anchored to `document.body` but tagged with
|
|
164
|
+
* `data-inside-portal`. The `ignore` predicate checks that attribute and
|
|
165
|
+
* treats clicks on the portal as "inside".
|
|
166
|
+
*
|
|
167
|
+
* ```tsx
|
|
168
|
+
* useClickOutside(close, {
|
|
169
|
+
* ignore: (event) =>
|
|
170
|
+
* (event.target as HTMLElement | null)?.closest("[data-tooltip]") != null,
|
|
171
|
+
* });
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export const IgnorePortals = {
|
|
175
|
+
render: () => {
|
|
176
|
+
const [closed, setClosed] = useState(false);
|
|
177
|
+
const ref = useClickOutside<HTMLDivElement>(() => setClosed(true), {
|
|
178
|
+
ignore: (event) =>
|
|
179
|
+
(event.target as HTMLElement | null)?.closest("[data-inside-portal]") !=
|
|
180
|
+
null,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className="flex flex-col items-start gap-3">
|
|
30
185
|
<div
|
|
31
186
|
ref={ref}
|
|
32
|
-
className=
|
|
187
|
+
className="rounded-md border border-input bg-card p-4 text-sm"
|
|
33
188
|
>
|
|
34
|
-
|
|
35
|
-
|
|
189
|
+
{closed
|
|
190
|
+
? "Closed — click Reset."
|
|
191
|
+
: "Click the green portal: NOT counted as outside. Click anywhere else: counted."}
|
|
36
192
|
</div>
|
|
193
|
+
{closed ? (
|
|
194
|
+
<Button size="sm" onClick={() => setClosed(false)}>
|
|
195
|
+
Reset
|
|
196
|
+
</Button>
|
|
197
|
+
) : null}
|
|
198
|
+
{createPortal(
|
|
199
|
+
<div
|
|
200
|
+
data-inside-portal
|
|
201
|
+
className="fixed right-4 bottom-4 rounded-md border border-success bg-success/15 p-3 text-xs shadow-md"
|
|
202
|
+
>
|
|
203
|
+
Portal content (anchored to body, but ignored).
|
|
204
|
+
</div>,
|
|
205
|
+
document.body,
|
|
206
|
+
)}
|
|
37
207
|
</div>
|
|
38
208
|
);
|
|
39
209
|
},
|
|
@@ -1,39 +1,118 @@
|
|
|
1
1
|
import { act, render, screen } from "@testing-library/react";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
import { describe, expect, it, vi } from "vitest";
|
|
3
4
|
import { useClickOutside } from "../use-click-outside";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
type ComponentProps = {
|
|
7
|
+
onOutside: (event: Event) => void;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
events?: Array<"mousedown" | "pointerdown" | "touchstart">;
|
|
10
|
+
ignore?: (event: Event) => boolean;
|
|
11
|
+
withExtra?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function Component({
|
|
15
|
+
onOutside,
|
|
16
|
+
enabled,
|
|
17
|
+
events,
|
|
18
|
+
ignore,
|
|
19
|
+
withExtra,
|
|
20
|
+
}: ComponentProps) {
|
|
21
|
+
const extraRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const ref = useClickOutside<HTMLDivElement>(onOutside, {
|
|
23
|
+
enabled,
|
|
24
|
+
events,
|
|
25
|
+
ignore,
|
|
26
|
+
additionalRefs: withExtra ? [extraRef] : undefined,
|
|
27
|
+
});
|
|
7
28
|
return (
|
|
8
29
|
<div data-testid="wrapper">
|
|
9
30
|
<div ref={ref} data-testid="target" />
|
|
31
|
+
{withExtra ? <div ref={extraRef} data-testid="extra" /> : null}
|
|
10
32
|
</div>
|
|
11
33
|
);
|
|
12
34
|
}
|
|
13
35
|
|
|
14
|
-
function
|
|
15
|
-
el
|
|
36
|
+
function fireEvent(
|
|
37
|
+
el: Element,
|
|
38
|
+
type: "mousedown" | "pointerdown" | "touchstart",
|
|
39
|
+
) {
|
|
40
|
+
el.dispatchEvent(new Event(type, { bubbles: true }));
|
|
16
41
|
}
|
|
17
42
|
|
|
18
43
|
describe("useClickOutside", () => {
|
|
19
|
-
it("calls callback
|
|
44
|
+
it("calls callback on pointerdown outside the ref element (default event)", () => {
|
|
20
45
|
const fn = vi.fn();
|
|
21
46
|
render(<Component onOutside={fn} />);
|
|
22
|
-
act(() =>
|
|
47
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "pointerdown"));
|
|
23
48
|
expect(fn).toHaveBeenCalledOnce();
|
|
24
49
|
});
|
|
25
50
|
|
|
51
|
+
it("passes the event to the callback", () => {
|
|
52
|
+
const fn = vi.fn();
|
|
53
|
+
render(<Component onOutside={fn} />);
|
|
54
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "pointerdown"));
|
|
55
|
+
expect(fn.mock.calls[0][0]).toBeInstanceOf(Event);
|
|
56
|
+
});
|
|
57
|
+
|
|
26
58
|
it("does not call callback when clicking inside the ref element", () => {
|
|
27
59
|
const fn = vi.fn();
|
|
28
60
|
render(<Component onOutside={fn} />);
|
|
29
|
-
act(() =>
|
|
61
|
+
act(() => fireEvent(screen.getByTestId("target"), "pointerdown"));
|
|
62
|
+
expect(fn).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not attach listener when enabled=false", () => {
|
|
66
|
+
const fn = vi.fn();
|
|
67
|
+
render(<Component onOutside={fn} enabled={false} />);
|
|
68
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "pointerdown"));
|
|
30
69
|
expect(fn).not.toHaveBeenCalled();
|
|
31
70
|
});
|
|
32
71
|
|
|
33
|
-
it("
|
|
72
|
+
it("attaches listeners for each event in the events option", () => {
|
|
73
|
+
const fn = vi.fn();
|
|
74
|
+
render(<Component onOutside={fn} events={["mousedown", "touchstart"]} />);
|
|
75
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "mousedown"));
|
|
76
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "touchstart"));
|
|
77
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("does not call callback for events not in the events option", () => {
|
|
81
|
+
const fn = vi.fn();
|
|
82
|
+
render(<Component onOutside={fn} events={["mousedown"]} />);
|
|
83
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "pointerdown"));
|
|
84
|
+
expect(fn).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("skips the callback when ignore returns true", () => {
|
|
88
|
+
const fn = vi.fn();
|
|
89
|
+
render(<Component onOutside={fn} ignore={() => true} />);
|
|
90
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "pointerdown"));
|
|
91
|
+
expect(fn).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("treats additional refs as inside", () => {
|
|
95
|
+
const fn = vi.fn();
|
|
96
|
+
render(<Component onOutside={fn} withExtra />);
|
|
97
|
+
act(() => fireEvent(screen.getByTestId("extra"), "pointerdown"));
|
|
98
|
+
expect(fn).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses the latest callback without re-attaching listeners", () => {
|
|
102
|
+
const first = vi.fn();
|
|
103
|
+
const second = vi.fn();
|
|
104
|
+
const { rerender } = render(<Component onOutside={first} />);
|
|
105
|
+
rerender(<Component onOutside={second} />);
|
|
106
|
+
act(() => fireEvent(screen.getByTestId("wrapper"), "pointerdown"));
|
|
107
|
+
expect(first).not.toHaveBeenCalled();
|
|
108
|
+
expect(second).toHaveBeenCalledOnce();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("removes listeners on unmount", () => {
|
|
34
112
|
const fn = vi.fn();
|
|
35
|
-
render(<Component onOutside={fn}
|
|
36
|
-
|
|
113
|
+
const { unmount } = render(<Component onOutside={fn} />);
|
|
114
|
+
unmount();
|
|
115
|
+
act(() => fireEvent(document.body, "pointerdown"));
|
|
37
116
|
expect(fn).not.toHaveBeenCalled();
|
|
38
117
|
});
|
|
39
118
|
});
|
|
@@ -1,25 +1,71 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { type RefObject, useEffect, useRef } from "react";
|
|
2
|
+
import { isBrowser, useLatestRef } from "../internal";
|
|
3
|
+
|
|
4
|
+
type UseClickOutsideOptions = {
|
|
5
|
+
/** Disable the listener without unmounting the hook. Defaults to `true`. */
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Which DOM events trigger the outside check. Any DOM event names work;
|
|
9
|
+
* pass `null` to fall back to the default. Defaults to `["pointerdown"]`.
|
|
10
|
+
*/
|
|
11
|
+
events?: string[] | null;
|
|
12
|
+
/**
|
|
13
|
+
* Return `true` to skip the callback for a given event. Useful for portals
|
|
14
|
+
* or popovers whose DOM lives outside the wrapped ref but should still count
|
|
15
|
+
* as "inside".
|
|
16
|
+
*/
|
|
17
|
+
ignore?: (event: Event) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Extra refs that also count as "inside". An event whose target lives in
|
|
20
|
+
* any of these elements will not fire the callback.
|
|
21
|
+
*/
|
|
22
|
+
additionalRefs?: RefObject<HTMLElement | null>[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_EVENTS = ["pointerdown"];
|
|
2
26
|
|
|
3
27
|
export const useClickOutside = <T extends HTMLElement = HTMLElement>(
|
|
4
|
-
callback: () => void,
|
|
5
|
-
|
|
6
|
-
) => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
callback: (event: Event) => void,
|
|
29
|
+
options: UseClickOutsideOptions = {},
|
|
30
|
+
): RefObject<T | null> => {
|
|
31
|
+
const { enabled = true, events, ignore, additionalRefs } = options;
|
|
32
|
+
const resolvedEvents = events ?? DEFAULT_EVENTS;
|
|
33
|
+
|
|
34
|
+
const ref = useRef<T | null>(null);
|
|
35
|
+
const callbackRef = useLatestRef(callback);
|
|
36
|
+
const ignoreRef = useLatestRef(ignore);
|
|
37
|
+
const additionalRefsRef = useLatestRef(additionalRefs);
|
|
14
38
|
|
|
15
39
|
useEffect(() => {
|
|
16
|
-
if (!
|
|
17
|
-
|
|
40
|
+
if (!enabled || !isBrowser) return;
|
|
41
|
+
|
|
42
|
+
const handler = (event: Event) => {
|
|
43
|
+
if (ignoreRef.current?.(event)) return;
|
|
44
|
+
|
|
45
|
+
const target = event.target as Node | null;
|
|
46
|
+
if (!target) return;
|
|
47
|
+
|
|
48
|
+
if (ref.current?.contains(target)) return;
|
|
49
|
+
|
|
50
|
+
const extras = additionalRefsRef.current;
|
|
51
|
+
if (extras) {
|
|
52
|
+
for (const extra of extras) {
|
|
53
|
+
if (extra.current?.contains(target)) return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
callbackRef.current(event);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const event of resolvedEvents) {
|
|
61
|
+
document.addEventListener(event, handler);
|
|
62
|
+
}
|
|
18
63
|
return () => {
|
|
19
|
-
|
|
64
|
+
for (const event of resolvedEvents) {
|
|
65
|
+
document.removeEventListener(event, handler);
|
|
66
|
+
}
|
|
20
67
|
};
|
|
21
|
-
}, [
|
|
68
|
+
}, [enabled, resolvedEvents.join("|")]);
|
|
22
69
|
|
|
23
70
|
return ref;
|
|
24
71
|
};
|
|
25
|
-
|
|
@@ -1,15 +1,46 @@
|
|
|
1
1
|
import { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { Input } from "../../components";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createToastManager,
|
|
5
|
+
ToastProvider,
|
|
6
|
+
toast,
|
|
7
|
+
} from "../../components/toast";
|
|
4
8
|
import { useDebouncedCallback } from "./use-debounced-callback";
|
|
5
9
|
|
|
6
10
|
const toastManager = createToastManager();
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
type Args = { delay: number };
|
|
13
|
+
|
|
14
|
+
const meta: Meta<Args> = {
|
|
9
15
|
title: "hooks/useDebouncedCallback",
|
|
16
|
+
argTypes: {
|
|
17
|
+
delay: {
|
|
18
|
+
control: { type: "range", min: 100, max: 3000, step: 100 },
|
|
19
|
+
description: "Debounce delay in milliseconds.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
args: { delay: 600 },
|
|
23
|
+
parameters: {
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component: [
|
|
27
|
+
"Returns a stable debounced wrapper around `callback` with controls.",
|
|
28
|
+
"",
|
|
29
|
+
"**Signature**: `[debounced, { cancel, flush, isPending }] = useDebouncedCallback(callback, delay)`",
|
|
30
|
+
"",
|
|
31
|
+
"- `cancel()` — discard the pending call without invoking.",
|
|
32
|
+
"- `flush()` — invoke the pending call immediately.",
|
|
33
|
+
"- `isPending` — reactive boolean, `true` while a call is scheduled.",
|
|
34
|
+
"",
|
|
35
|
+
"The wrapper has a **stable identity** — it is only recreated when `delay` changes,",
|
|
36
|
+
"never when `callback` changes. Inline arrow functions are safe to pass.",
|
|
37
|
+
].join("\n"),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
10
41
|
decorators: [
|
|
11
42
|
(Story) => (
|
|
12
|
-
<ToastProvider
|
|
43
|
+
<ToastProvider>
|
|
13
44
|
<Story />
|
|
14
45
|
</ToastProvider>
|
|
15
46
|
),
|
|
@@ -18,66 +49,135 @@ const meta: Meta = {
|
|
|
18
49
|
|
|
19
50
|
export default meta;
|
|
20
51
|
|
|
21
|
-
export const Basic = {
|
|
22
|
-
render: () => {
|
|
23
|
-
const handleSearch = useDebouncedCallback(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
52
|
+
export const Basic: StoryObj<Args> = {
|
|
53
|
+
render: ({ delay }) => {
|
|
54
|
+
const [handleSearch, { isPending, cancel, flush }] = useDebouncedCallback(
|
|
55
|
+
(value: string) => {
|
|
56
|
+
toast({
|
|
57
|
+
description: `Fired with: "${value}"`,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
delay,
|
|
61
|
+
);
|
|
30
62
|
|
|
31
63
|
return (
|
|
32
|
-
<div className="
|
|
33
|
-
<pre className="rounded-md bg-slate-950 p-4 text-white">
|
|
34
|
-
<code className="block">
|
|
35
|
-
Escribe en el input para ver el callback debounced en acción.
|
|
36
|
-
</code>
|
|
37
|
-
<code className="block">Revisa la consola para ver los logs.</code>
|
|
38
|
-
</pre>
|
|
64
|
+
<div className="flex max-w-sm flex-col gap-4">
|
|
39
65
|
<Input
|
|
40
66
|
onValueChange={(value) => handleSearch(value)}
|
|
41
|
-
placeholder="
|
|
67
|
+
placeholder="Type to trigger the debounced callback..."
|
|
42
68
|
/>
|
|
69
|
+
<div className="overflow-hidden rounded-lg border border-slate-800 bg-slate-900 font-mono text-sm">
|
|
70
|
+
<div className="border-b border-slate-800 bg-slate-800/60 px-4 py-2 font-sans text-xs font-semibold uppercase tracking-widest text-slate-400">
|
|
71
|
+
State
|
|
72
|
+
</div>
|
|
73
|
+
<div className="px-4 py-2.5">
|
|
74
|
+
<span className="text-slate-500">status</span>
|
|
75
|
+
<span className="ml-4">
|
|
76
|
+
<span
|
|
77
|
+
className={isPending ? "text-yellow-300" : "text-slate-500"}
|
|
78
|
+
>
|
|
79
|
+
{isPending ? "pending" : "idle"}
|
|
80
|
+
</span>
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex gap-2">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
onClick={cancel}
|
|
88
|
+
disabled={!isPending}
|
|
89
|
+
className="rounded border border-slate-300 px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
|
|
90
|
+
>
|
|
91
|
+
Cancel
|
|
92
|
+
</button>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={flush}
|
|
96
|
+
disabled={!isPending}
|
|
97
|
+
className="rounded border border-slate-300 px-3 py-1.5 text-sm text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
|
|
98
|
+
>
|
|
99
|
+
Flush
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
43
102
|
</div>
|
|
44
103
|
);
|
|
45
104
|
},
|
|
46
105
|
};
|
|
47
106
|
|
|
48
|
-
export const DifferentDelays: StoryObj = {
|
|
107
|
+
export const DifferentDelays: StoryObj<Args> = {
|
|
108
|
+
parameters: {
|
|
109
|
+
controls: { exclude: ["delay"] },
|
|
110
|
+
},
|
|
49
111
|
render: () => {
|
|
50
|
-
const
|
|
112
|
+
const [
|
|
113
|
+
handleFast,
|
|
114
|
+
{ isPending: fastPending, cancel: cancelFast, flush: flushFast },
|
|
115
|
+
] = useDebouncedCallback((value: string) => {
|
|
51
116
|
toastManager.add({
|
|
52
117
|
variant: "success",
|
|
53
|
-
description: `
|
|
118
|
+
description: `Fast (200ms): "${value}"`,
|
|
54
119
|
});
|
|
55
120
|
}, 200);
|
|
56
121
|
|
|
57
|
-
const
|
|
122
|
+
const [
|
|
123
|
+
handleSlow,
|
|
124
|
+
{ isPending: slowPending, cancel: cancelSlow, flush: flushSlow },
|
|
125
|
+
] = useDebouncedCallback((value: string) => {
|
|
58
126
|
toastManager.add({
|
|
59
127
|
variant: "success",
|
|
60
|
-
description: `
|
|
128
|
+
description: `Slow (1000ms): "${value}"`,
|
|
61
129
|
});
|
|
62
130
|
}, 1000);
|
|
63
131
|
|
|
64
132
|
return (
|
|
65
|
-
<div className="
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
<div className="flex max-w-sm flex-col gap-6">
|
|
134
|
+
{[
|
|
135
|
+
{
|
|
136
|
+
label: "Fast — 200ms",
|
|
137
|
+
handler: handleFast,
|
|
138
|
+
pending: fastPending,
|
|
139
|
+
cancel: cancelFast,
|
|
140
|
+
flush: flushFast,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: "Slow — 1000ms",
|
|
144
|
+
handler: handleSlow,
|
|
145
|
+
pending: slowPending,
|
|
146
|
+
cancel: cancelSlow,
|
|
147
|
+
flush: flushSlow,
|
|
148
|
+
},
|
|
149
|
+
].map(({ label, handler, pending, cancel, flush }) => (
|
|
150
|
+
<div key={label} className="flex flex-col gap-2">
|
|
151
|
+
<p className="text-sm font-medium text-slate-700">{label}</p>
|
|
152
|
+
<Input
|
|
153
|
+
onValueChange={(value) => handler(value)}
|
|
154
|
+
placeholder="Type..."
|
|
155
|
+
/>
|
|
156
|
+
<div className="flex items-center gap-3">
|
|
157
|
+
<span
|
|
158
|
+
className={`text-xs font-mono ${pending ? "text-yellow-500" : "text-slate-400"}`}
|
|
159
|
+
>
|
|
160
|
+
{pending ? "pending" : "idle"}
|
|
161
|
+
</span>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={cancel}
|
|
165
|
+
disabled={!pending}
|
|
166
|
+
className="rounded border border-slate-300 px-2.5 py-1 text-xs text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
|
167
|
+
>
|
|
168
|
+
Cancel
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={flush}
|
|
173
|
+
disabled={!pending}
|
|
174
|
+
className="rounded border border-slate-300 px-2.5 py-1 text-xs text-slate-600 hover:bg-slate-50 disabled:opacity-40"
|
|
175
|
+
>
|
|
176
|
+
Flush
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
81
181
|
</div>
|
|
82
182
|
);
|
|
83
183
|
},
|