@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
@@ -1,14 +1,18 @@
1
- import { render, screen, waitFor } from "@testing-library/react";
2
- import { describe, expect, it, vi } from "vitest";
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import { keyboard } from "../../utils/tests/keyboard";
4
- import { useHotkey, HotkeyOptions } from "../use-hotkey";
4
+ import { getHotkeyHandler, type HotkeyOptions, useHotkey } from "../use-hotkey";
5
+
6
+ afterEach(() => {
7
+ vi.unstubAllGlobals();
8
+ });
5
9
 
6
10
  describe("useHotkey hook", () => {
7
11
  it("should be defined", () => {
8
12
  expect(useHotkey).toBeDefined();
9
13
  });
10
14
 
11
- it("should call handler on keydown event", async () => {
15
+ it("calls handler on a matching keydown event", async () => {
12
16
  const handler = vi.fn();
13
17
  const TestComponent = () => {
14
18
  useHotkey("a", handler);
@@ -16,17 +20,14 @@ describe("useHotkey hook", () => {
16
20
  };
17
21
 
18
22
  render(<TestComponent />);
19
-
20
- keyboard(document, {
21
- key: "a",
22
- });
23
+ keyboard(document, { key: "a" });
23
24
 
24
25
  await waitFor(() => {
25
- expect(handler).toHaveBeenCalled();
26
+ expect(handler).toHaveBeenCalledTimes(1);
26
27
  });
27
28
  });
28
29
 
29
- it("should call handler with correct key combination", async () => {
30
+ it("matches an exact key combination", async () => {
30
31
  const handler = vi.fn();
31
32
  const TestComponent = () => {
32
33
  useHotkey("ctrl+a", handler);
@@ -34,18 +35,29 @@ describe("useHotkey hook", () => {
34
35
  };
35
36
 
36
37
  render(<TestComponent />);
38
+ keyboard(document, { key: "a", ctrlKey: true });
37
39
 
38
- keyboard(document, {
39
- key: "a",
40
- ctrlKey: true,
40
+ await waitFor(() => {
41
+ expect(handler).toHaveBeenCalledTimes(1);
41
42
  });
43
+ });
44
+
45
+ it("rejects when an extra modifier is pressed", async () => {
46
+ const handler = vi.fn();
47
+ const TestComponent = () => {
48
+ useHotkey("ctrl+a", handler);
49
+ return null;
50
+ };
51
+
52
+ render(<TestComponent />);
53
+ keyboard(document, { key: "a", ctrlKey: true, shiftKey: true });
42
54
 
43
55
  await waitFor(() => {
44
- expect(handler).toHaveBeenCalled();
56
+ expect(handler).not.toHaveBeenCalled();
45
57
  });
46
58
  });
47
59
 
48
- it("should prevent default action if preventDefault is true", async () => {
60
+ it("calls preventDefault by default", async () => {
49
61
  const handler = vi.fn();
50
62
  const TestComponent = () => {
51
63
  useHotkey("b", handler);
@@ -53,7 +65,6 @@ describe("useHotkey hook", () => {
53
65
  };
54
66
 
55
67
  render(<TestComponent />);
56
-
57
68
  const event = new KeyboardEvent("keydown", { key: "b" });
58
69
  Object.defineProperty(event, "preventDefault", { value: vi.fn() });
59
70
  document.dispatchEvent(event);
@@ -63,7 +74,7 @@ describe("useHotkey hook", () => {
63
74
  });
64
75
  });
65
76
 
66
- it("should not prevent default action if preventDefault is false", async () => {
77
+ it("does not call preventDefault when preventDefault is false", async () => {
67
78
  const handler = vi.fn();
68
79
  const options: HotkeyOptions = { preventDefault: false };
69
80
  const TestComponent = () => {
@@ -81,7 +92,7 @@ describe("useHotkey hook", () => {
81
92
  });
82
93
  });
83
94
 
84
- it("should handle multiple keys", async () => {
95
+ it("shares one handler across an array of keys", async () => {
85
96
  const handler = vi.fn();
86
97
  const TestComponent = () => {
87
98
  useHotkey(["d", "e"], handler);
@@ -89,48 +100,289 @@ describe("useHotkey hook", () => {
89
100
  };
90
101
 
91
102
  render(<TestComponent />);
92
-
93
- keyboard(document, {
94
- key: "d",
95
- });
96
-
97
- keyboard(document, {
98
- key: "e",
99
- });
103
+ keyboard(document, { key: "d" });
104
+ keyboard(document, { key: "e" });
100
105
 
101
106
  await waitFor(() => {
102
107
  expect(handler).toHaveBeenCalledTimes(2);
103
108
  });
104
109
  });
105
110
 
106
- it("does not call handler when watch=false", async () => {
111
+ it("does not call handler when watch is false", async () => {
107
112
  const handler = vi.fn();
108
113
  const TestComponent = () => {
109
114
  useHotkey("f", handler, { watch: false });
110
115
  return null;
111
116
  };
117
+
112
118
  render(<TestComponent />);
113
119
  keyboard(document, { key: "f" });
120
+
114
121
  await waitFor(() => {
115
122
  expect(handler).not.toHaveBeenCalled();
116
123
  });
117
124
  });
118
125
 
119
- it("does not call handler when keydown originates from an input field", async () => {
126
+ it("listens on keyup when eventName is 'keyup'", async () => {
120
127
  const handler = vi.fn();
121
128
  const TestComponent = () => {
122
- useHotkey("z", handler);
123
- return <input data-testid="input" />;
129
+ useHotkey("g", handler, { eventName: "keyup" });
130
+ return null;
124
131
  };
132
+
125
133
  render(<TestComponent />);
126
134
 
127
- const input = screen.getByTestId("input");
128
- input.dispatchEvent(
129
- new KeyboardEvent("keydown", { key: "z", bubbles: true }),
135
+ keyboard(document, { key: "g" }, "keydown");
136
+ await waitFor(() => {
137
+ expect(handler).not.toHaveBeenCalled();
138
+ });
139
+
140
+ keyboard(document, { key: "g" }, "keyup");
141
+ await waitFor(() => {
142
+ expect(handler).toHaveBeenCalledTimes(1);
143
+ });
144
+ });
145
+
146
+ describe("array-of-tuples form", () => {
147
+ it("dispatches each binding independently", async () => {
148
+ const handlerA = vi.fn();
149
+ const handlerB = vi.fn();
150
+ const TestComponent = () => {
151
+ useHotkey([
152
+ ["a", handlerA],
153
+ ["b", handlerB],
154
+ ]);
155
+ return null;
156
+ };
157
+
158
+ render(<TestComponent />);
159
+ keyboard(document, { key: "b" });
160
+
161
+ await waitFor(() => {
162
+ expect(handlerB).toHaveBeenCalledTimes(1);
163
+ expect(handlerA).not.toHaveBeenCalled();
164
+ });
165
+ });
166
+
167
+ it("honors per-binding preventDefault override", async () => {
168
+ const handler = vi.fn();
169
+ const TestComponent = () => {
170
+ useHotkey([["a", handler, { preventDefault: false }]]);
171
+ return null;
172
+ };
173
+
174
+ render(<TestComponent />);
175
+ const event = new KeyboardEvent("keydown", { key: "a" });
176
+ Object.defineProperty(event, "preventDefault", { value: vi.fn() });
177
+ document.dispatchEvent(event);
178
+
179
+ await waitFor(() => {
180
+ expect(handler).toHaveBeenCalledTimes(1);
181
+ expect(event.preventDefault).not.toHaveBeenCalled();
182
+ });
183
+ });
184
+ });
185
+
186
+ describe("platform-aware mod", () => {
187
+ it("matches meta (⌘) and rejects ctrl on Apple", async () => {
188
+ vi.stubGlobal("navigator", { platform: "MacIntel" });
189
+ const handler = vi.fn();
190
+ const TestComponent = () => {
191
+ useHotkey("mod+k", handler);
192
+ return null;
193
+ };
194
+
195
+ render(<TestComponent />);
196
+
197
+ keyboard(document, { key: "k", ctrlKey: true });
198
+ await waitFor(() => {
199
+ expect(handler).not.toHaveBeenCalled();
200
+ });
201
+
202
+ keyboard(document, { key: "k", metaKey: true });
203
+ await waitFor(() => {
204
+ expect(handler).toHaveBeenCalledTimes(1);
205
+ });
206
+ });
207
+
208
+ it("matches ctrl and rejects meta on non-Apple", async () => {
209
+ vi.stubGlobal("navigator", { platform: "Win32" });
210
+ const handler = vi.fn();
211
+ const TestComponent = () => {
212
+ useHotkey("mod+k", handler);
213
+ return null;
214
+ };
215
+
216
+ render(<TestComponent />);
217
+
218
+ keyboard(document, { key: "k", metaKey: true });
219
+ await waitFor(() => {
220
+ expect(handler).not.toHaveBeenCalled();
221
+ });
222
+
223
+ keyboard(document, { key: "k", ctrlKey: true });
224
+ await waitFor(() => {
225
+ expect(handler).toHaveBeenCalledTimes(1);
226
+ });
227
+ });
228
+ });
229
+
230
+ describe("tag and contentEditable suppression", () => {
231
+ it("does not fire when the event originates from an input field", async () => {
232
+ const handler = vi.fn();
233
+ const TestComponent = () => {
234
+ useHotkey("z", handler);
235
+ return <input data-testid="input" />;
236
+ };
237
+
238
+ render(<TestComponent />);
239
+ const input = screen.getByTestId("input");
240
+ input.dispatchEvent(
241
+ new KeyboardEvent("keydown", { key: "z", bubbles: true }),
242
+ );
243
+
244
+ await waitFor(() => {
245
+ expect(handler).not.toHaveBeenCalled();
246
+ });
247
+ });
248
+
249
+ it("fires from an input when tagsToIgnore is empty", async () => {
250
+ const handler = vi.fn();
251
+ const TestComponent = () => {
252
+ useHotkey("z", handler, { tagsToIgnore: [] });
253
+ return <input data-testid="input" />;
254
+ };
255
+
256
+ render(<TestComponent />);
257
+ const input = screen.getByTestId("input");
258
+ input.dispatchEvent(
259
+ new KeyboardEvent("keydown", { key: "z", bubbles: true }),
260
+ );
261
+
262
+ await waitFor(() => {
263
+ expect(handler).toHaveBeenCalledTimes(1);
264
+ });
265
+ });
266
+
267
+ it("fires from a contentEditable target when triggerOnContentEditable is true", async () => {
268
+ const handler = vi.fn();
269
+ const TestComponent = () => {
270
+ useHotkey("z", handler, { triggerOnContentEditable: true });
271
+ return (
272
+ <div
273
+ contentEditable
274
+ data-testid="editable"
275
+ suppressContentEditableWarning
276
+ />
277
+ );
278
+ };
279
+
280
+ render(<TestComponent />);
281
+ const editable = screen.getByTestId("editable");
282
+ editable.dispatchEvent(
283
+ new KeyboardEvent("keydown", { key: "z", bubbles: true }),
284
+ );
285
+
286
+ await waitFor(() => {
287
+ expect(handler).toHaveBeenCalledTimes(1);
288
+ });
289
+ });
290
+ });
291
+
292
+ describe("lifecycle", () => {
293
+ it("removes the document listener on unmount", async () => {
294
+ const handler = vi.fn();
295
+ const TestComponent = () => {
296
+ useHotkey("u", handler);
297
+ return null;
298
+ };
299
+
300
+ const { unmount } = render(<TestComponent />);
301
+ unmount();
302
+ keyboard(document, { key: "u" });
303
+
304
+ await waitFor(() => {
305
+ expect(handler).not.toHaveBeenCalled();
306
+ });
307
+ });
308
+
309
+ it("invokes the latest handler after a re-render without re-binding", async () => {
310
+ const first = vi.fn();
311
+ const second = vi.fn();
312
+ const TestComponent = ({ handler }: { handler: () => void }) => {
313
+ useHotkey("r", handler);
314
+ return null;
315
+ };
316
+
317
+ const { rerender } = render(<TestComponent handler={first} />);
318
+ rerender(<TestComponent handler={second} />);
319
+
320
+ keyboard(document, { key: "r" });
321
+
322
+ await waitFor(() => {
323
+ expect(second).toHaveBeenCalledTimes(1);
324
+ expect(first).not.toHaveBeenCalled();
325
+ });
326
+ });
327
+ });
328
+ });
329
+
330
+ describe("getHotkeyHandler", () => {
331
+ it("returns a handler that fires on a matching key", async () => {
332
+ const handler = vi.fn();
333
+ const TestComponent = () => (
334
+ <input
335
+ data-testid="scoped"
336
+ onKeyDown={getHotkeyHandler([["a", handler]])}
337
+ />
130
338
  );
131
339
 
340
+ render(<TestComponent />);
341
+ fireEvent.keyDown(screen.getByTestId("scoped"), { key: "a" });
342
+
132
343
  await waitFor(() => {
133
- expect(handler).not.toHaveBeenCalled();
344
+ expect(handler).toHaveBeenCalledTimes(1);
345
+ });
346
+ });
347
+
348
+ it("shares mod behavior (Ctrl+K on non-Apple)", async () => {
349
+ vi.stubGlobal("navigator", { platform: "Win32" });
350
+ const handler = vi.fn();
351
+ const TestComponent = () => (
352
+ <input
353
+ data-testid="scoped"
354
+ onKeyDown={getHotkeyHandler([["mod+k", handler]])}
355
+ />
356
+ );
357
+
358
+ render(<TestComponent />);
359
+ fireEvent.keyDown(screen.getByTestId("scoped"), {
360
+ key: "k",
361
+ ctrlKey: true,
362
+ });
363
+
364
+ await waitFor(() => {
365
+ expect(handler).toHaveBeenCalledTimes(1);
366
+ });
367
+ });
368
+
369
+ it("does NOT suppress the input tag it is attached to", async () => {
370
+ const handler = vi.fn();
371
+ const TestComponent = () => (
372
+ <input
373
+ data-testid="scoped"
374
+ onKeyDown={getHotkeyHandler([["mod+enter", handler]])}
375
+ />
376
+ );
377
+
378
+ render(<TestComponent />);
379
+ fireEvent.keyDown(screen.getByTestId("scoped"), {
380
+ key: "Enter",
381
+ ctrlKey: true,
382
+ });
383
+
384
+ await waitFor(() => {
385
+ expect(handler).toHaveBeenCalledTimes(1);
134
386
  });
135
387
  });
136
388
  });
@@ -1,33 +1,157 @@
1
- import { useCallback } from "react";
2
1
  import { useEventListener } from "../use-event-listener";
3
- import { createListener } from "./utils";
2
+ import {
3
+ type HotkeyItem,
4
+ type HotkeyItemOptions,
5
+ type KeyboardEventLike,
6
+ matchAndRun,
7
+ } from "./utils";
4
8
 
9
+ export type { HotkeyItem, HotkeyItemOptions, KeyboardEventLike };
10
+
11
+ /** A single key string or a list of key strings sharing one handler. */
5
12
  export type Keys = string | string[];
6
- export type HotkeyEventName = "keydown" | "keypress" | "keyup";
7
- export type HotkeyOptions = {
13
+
14
+ /** Supported listener event names. `"keypress"` is removed in v2. */
15
+ export type HotkeyEventName = "keydown" | "keyup";
16
+
17
+ /**
18
+ * Options for the single-binding `useHotkey(keys, handler, options)` form.
19
+ * Combines hook-level concerns (`watch`, `eventName`, `tagsToIgnore`) with the
20
+ * per-binding behavior (`preventDefault`, `triggerOnContentEditable`).
21
+ */
22
+ export type HotkeyOptions = HotkeyItemOptions & {
23
+ /** Listen while `true` (default). When `false`, no handler fires. */
8
24
  watch?: boolean;
25
+ /** DOM event to listen for. Default `"keydown"`. */
9
26
  eventName?: HotkeyEventName;
10
- preventDefault?: boolean;
11
- ignoreInputFields?: boolean;
27
+ /**
28
+ * Tag names whose events are ignored. Default
29
+ * `["INPUT", "TEXTAREA", "SELECT"]`. Pass `[]` to ignore nothing.
30
+ */
31
+ tagsToIgnore?: string[];
12
32
  };
13
33
 
34
+ const DEFAULT_TAGS_TO_IGNORE = ["INPUT", "TEXTAREA", "SELECT"];
35
+
36
+ /**
37
+ * Bind one or more keyboard shortcuts at the `document` level.
38
+ *
39
+ * Two forms are supported:
40
+ *
41
+ * 1. Single binding — `useHotkey(keys, handler, options?)` where `keys` is a
42
+ * string or string array sharing one handler.
43
+ * 2. Multiple bindings — `useHotkey(bindings)` where `bindings` is a list of
44
+ * `[key, handler, options?]` tuples, each dispatched independently.
45
+ *
46
+ * Matching is strict: every required modifier (`shift`/`ctrl`/`alt`/`meta`)
47
+ * must be pressed and no other modifier may be. `mod` resolves to `meta` (⌘)
48
+ * on Apple platforms and `ctrl` elsewhere. Events originating in `tagsToIgnore`
49
+ * tags (or contentEditable regions) are suppressed unless opted out.
50
+ *
51
+ * The handler is read through a ref, so passing an inline function never
52
+ * re-attaches the underlying listener. SSR-safe: no DOM access on the server.
53
+ *
54
+ * @example
55
+ * useHotkey("mod+k", () => openPalette());
56
+ * useHotkey([["a", onA], ["b", onB, { preventDefault: false }]]);
57
+ */
14
58
  export function useHotkey(
15
- keys: string | string[],
16
- handler: (event: KeyboardEvent) => void,
17
- options: HotkeyOptions = {},
18
- ) {
59
+ keys: Keys,
60
+ handler: (event: KeyboardEventLike) => void,
61
+ options?: HotkeyOptions,
62
+ ): void;
63
+ export function useHotkey(
64
+ bindings: HotkeyItem[],
65
+ options?: HotkeyOptions,
66
+ ): void;
67
+ export function useHotkey(
68
+ keysOrBindings: Keys | HotkeyItem[],
69
+ handlerOrOptions?: ((event: KeyboardEventLike) => void) | HotkeyOptions,
70
+ maybeOptions?: HotkeyOptions,
71
+ ): void {
72
+ const isArrayForm =
73
+ Array.isArray(keysOrBindings) && typeof handlerOrOptions !== "function";
74
+
75
+ const options: HotkeyOptions = isArrayForm
76
+ ? ((handlerOrOptions as HotkeyOptions | undefined) ?? {})
77
+ : (maybeOptions ?? {});
78
+
19
79
  const {
20
80
  eventName = "keydown",
21
- preventDefault = true,
22
- ignoreInputFields = true,
23
81
  watch = true,
82
+ preventDefault = true,
83
+ triggerOnContentEditable = false,
84
+ tagsToIgnore = DEFAULT_TAGS_TO_IGNORE,
24
85
  } = options;
25
86
 
26
- const listener = useCallback(
27
- createListener(keys, handler, preventDefault, ignoreInputFields, watch),
28
- [keys, handler, preventDefault, ignoreInputFields, watch],
29
- );
87
+ const items: HotkeyItem[] = isArrayForm
88
+ ? (keysOrBindings as HotkeyItem[])
89
+ : normalizeSingle(
90
+ keysOrBindings as Keys,
91
+ handlerOrOptions as (event: KeyboardEventLike) => void,
92
+ { preventDefault, triggerOnContentEditable },
93
+ );
30
94
 
31
- return useEventListener(eventName, listener);
95
+ const listener = (event: KeyboardEvent) => {
96
+ if (!watch) return;
97
+ matchAndRun(event, items, tagsToIgnore);
98
+ };
99
+
100
+ useEventListener(eventName, listener, document);
32
101
  }
33
102
 
103
+ const normalizeSingle = (
104
+ keys: Keys,
105
+ handler: (event: KeyboardEventLike) => void,
106
+ perItemOptions: HotkeyItemOptions,
107
+ ): HotkeyItem[] => {
108
+ const keyList = Array.isArray(keys) ? keys : [keys];
109
+ return keyList.map((key) => [key, handler, perItemOptions]);
110
+ };
111
+
112
+ /**
113
+ * Build an element-scoped `onKeyDown` handler. Shares the same
114
+ * strict-modifier and `mod` behavior as {@link useHotkey}.
115
+ *
116
+ * Two forms are supported:
117
+ *
118
+ * 1. Single binding — `getHotkeyHandler(keys, handler, options?)`.
119
+ * 2. Multiple bindings — `getHotkeyHandler(bindings)` where `bindings` is a
120
+ * list of `[key, handler, options?]` tuples.
121
+ *
122
+ * Because it is attached to an element's `onKeyDown`, it applies NO tag
123
+ * suppression — it has no `tagsToIgnore` and matches regardless of the target
124
+ * tag (including the `<input>` it is attached to).
125
+ *
126
+ * @example
127
+ * <input onKeyDown={getHotkeyHandler("mod+enter", submit)} />
128
+ * <input onKeyDown={getHotkeyHandler([["mod+enter", submit], ["escape", cancel]])} />
129
+ */
130
+ export function getHotkeyHandler(
131
+ keys: Keys,
132
+ handler: (event: KeyboardEventLike) => void,
133
+ options?: HotkeyItemOptions,
134
+ ): (event: KeyboardEventLike) => void;
135
+ export function getHotkeyHandler(
136
+ bindings: HotkeyItem[],
137
+ ): (event: KeyboardEventLike) => void;
138
+ export function getHotkeyHandler(
139
+ keysOrBindings: Keys | HotkeyItem[],
140
+ handler?: (event: KeyboardEventLike) => void,
141
+ options?: HotkeyItemOptions,
142
+ ): (event: KeyboardEventLike) => void {
143
+ const isArrayForm =
144
+ Array.isArray(keysOrBindings) && typeof handler !== "function";
145
+
146
+ const bindings: HotkeyItem[] = isArrayForm
147
+ ? (keysOrBindings as HotkeyItem[])
148
+ : normalizeSingle(
149
+ keysOrBindings as Keys,
150
+ handler as (event: KeyboardEventLike) => void,
151
+ options ?? {},
152
+ );
153
+
154
+ return (event: KeyboardEventLike) => {
155
+ matchAndRun(event, bindings, []);
156
+ };
157
+ }
@@ -1,3 +1,8 @@
1
- export * from "./create-hotkey-listener";
2
- export * from "./is-input-field";
3
- export * from "./match-key-modifiers";
1
+ export { shouldIgnoreEvent } from "./is-input-field";
2
+ export type {
3
+ HotkeyItem,
4
+ HotkeyItemOptions,
5
+ KeyboardEventLike,
6
+ } from "./match-and-run";
7
+ export { matchAndRun } from "./match-and-run";
8
+ export { getKeyModifiers, matchKeyModifiers } from "./match-key-modifiers";
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { shouldIgnoreEvent } from "./is-input-field";
3
+ import type { KeyboardEventLike } from "./match-and-run";
4
+
5
+ const DEFAULT_IGNORE = ["INPUT", "TEXTAREA", "SELECT"];
6
+
7
+ const makeEvent = (target: EventTarget | null): KeyboardEventLike => ({
8
+ key: "k",
9
+ shiftKey: false,
10
+ ctrlKey: false,
11
+ altKey: false,
12
+ metaKey: false,
13
+ target,
14
+ preventDefault: vi.fn(),
15
+ });
16
+
17
+ const makeTarget = (props: {
18
+ tagName?: string;
19
+ isContentEditable?: boolean;
20
+ }): EventTarget =>
21
+ ({
22
+ tagName: props.tagName,
23
+ isContentEditable: props.isContentEditable ?? false,
24
+ }) as unknown as EventTarget;
25
+
26
+ describe("shouldIgnoreEvent", () => {
27
+ it("ignores targets whose tag is in the list", () => {
28
+ const event = makeEvent(makeTarget({ tagName: "INPUT" }));
29
+ expect(shouldIgnoreEvent(event, DEFAULT_IGNORE, false)).toBe(true);
30
+ });
31
+
32
+ it("ignores SELECT and TEXTAREA by default", () => {
33
+ expect(
34
+ shouldIgnoreEvent(
35
+ makeEvent(makeTarget({ tagName: "SELECT" })),
36
+ DEFAULT_IGNORE,
37
+ false,
38
+ ),
39
+ ).toBe(true);
40
+ expect(
41
+ shouldIgnoreEvent(
42
+ makeEvent(makeTarget({ tagName: "TEXTAREA" })),
43
+ DEFAULT_IGNORE,
44
+ false,
45
+ ),
46
+ ).toBe(true);
47
+ });
48
+
49
+ it("does not ignore tags outside the list", () => {
50
+ const event = makeEvent(makeTarget({ tagName: "BUTTON" }));
51
+ expect(shouldIgnoreEvent(event, DEFAULT_IGNORE, false)).toBe(false);
52
+ });
53
+
54
+ it("ignores contentEditable when triggerOnContentEditable is false", () => {
55
+ const event = makeEvent(
56
+ makeTarget({ tagName: "DIV", isContentEditable: true }),
57
+ );
58
+ expect(shouldIgnoreEvent(event, DEFAULT_IGNORE, false)).toBe(true);
59
+ });
60
+
61
+ it("does not ignore contentEditable when triggerOnContentEditable is true", () => {
62
+ const event = makeEvent(
63
+ makeTarget({ tagName: "DIV", isContentEditable: true }),
64
+ );
65
+ expect(shouldIgnoreEvent(event, DEFAULT_IGNORE, true)).toBe(false);
66
+ });
67
+
68
+ it("ignores nothing when the list is empty and not contentEditable", () => {
69
+ const event = makeEvent(makeTarget({ tagName: "INPUT" }));
70
+ expect(shouldIgnoreEvent(event, [], false)).toBe(false);
71
+ });
72
+
73
+ it("returns false when there is no target", () => {
74
+ expect(shouldIgnoreEvent(makeEvent(null), DEFAULT_IGNORE, false)).toBe(
75
+ false,
76
+ );
77
+ });
78
+ });