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