@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,35 @@
1
- export const isInputField = (element: HTMLElement): boolean => {
2
- return (
3
- element.tagName === "INPUT" ||
4
- element.tagName === "TEXTAREA" ||
5
- element.isContentEditable
6
- );
7
- };
1
+ import type { KeyboardEventLike } from "./match-and-run";
8
2
 
3
+ /**
4
+ * Decides whether a keyboard event should be suppressed because it originated
5
+ * inside a form control or editable region.
6
+ *
7
+ * - Suppressed when the target's tag name is in `tagsToIgnore` (case-sensitive
8
+ * uppercase tag names, e.g. `["INPUT","TEXTAREA","SELECT"]`). An empty list
9
+ * suppresses nothing.
10
+ * - Suppressed when the target is `contentEditable`, unless
11
+ * `triggerOnContentEditable` is `true`.
12
+ */
9
13
  export const shouldIgnoreEvent = (
10
- event: KeyboardEvent,
11
- ignoreInputFields: boolean,
14
+ event: KeyboardEventLike,
15
+ tagsToIgnore: string[],
16
+ triggerOnContentEditable: boolean,
12
17
  ): boolean => {
13
- return ignoreInputFields && isInputField(event.target as HTMLElement);
18
+ const target = event.target as
19
+ | (Element & { isContentEditable?: boolean })
20
+ | null;
21
+ if (!target) return false;
22
+
23
+ if (
24
+ typeof target.tagName === "string" &&
25
+ tagsToIgnore.includes(target.tagName)
26
+ ) {
27
+ return true;
28
+ }
29
+
30
+ if (!triggerOnContentEditable && target.isContentEditable === true) {
31
+ return true;
32
+ }
33
+
34
+ return false;
14
35
  };
@@ -0,0 +1,203 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import type { HotkeyItem, KeyboardEventLike } from "./match-and-run";
3
+ import { matchAndRun } from "./match-and-run";
4
+
5
+ const DEFAULT_IGNORE = ["INPUT", "TEXTAREA", "SELECT"];
6
+
7
+ type EventOverrides = Partial<
8
+ Pick<KeyboardEventLike, "shiftKey" | "ctrlKey" | "altKey" | "metaKey">
9
+ > & {
10
+ key: string;
11
+ target?: EventTarget | null;
12
+ };
13
+
14
+ const makeEvent = (overrides: EventOverrides): KeyboardEventLike => ({
15
+ key: overrides.key,
16
+ shiftKey: overrides.shiftKey ?? false,
17
+ ctrlKey: overrides.ctrlKey ?? false,
18
+ altKey: overrides.altKey ?? false,
19
+ metaKey: overrides.metaKey ?? false,
20
+ target: overrides.target ?? null,
21
+ preventDefault: vi.fn(),
22
+ });
23
+
24
+ const makeTarget = (props: {
25
+ tagName?: string;
26
+ isContentEditable?: boolean;
27
+ }): EventTarget =>
28
+ ({
29
+ tagName: props.tagName,
30
+ isContentEditable: props.isContentEditable ?? false,
31
+ }) as unknown as EventTarget;
32
+
33
+ afterEach(() => {
34
+ vi.unstubAllGlobals();
35
+ });
36
+
37
+ describe("matchAndRun", () => {
38
+ it("calls the handler when a single binding matches the main key", () => {
39
+ const handler = vi.fn();
40
+ const items: HotkeyItem[] = [["k", handler]];
41
+ const event = makeEvent({ key: "k" });
42
+
43
+ matchAndRun(event, items, DEFAULT_IGNORE);
44
+
45
+ expect(handler).toHaveBeenCalledTimes(1);
46
+ expect(handler).toHaveBeenCalledWith(event);
47
+ });
48
+
49
+ it("dispatches each matching binding independently", () => {
50
+ const handlerA = vi.fn();
51
+ const handlerB = vi.fn();
52
+ const items: HotkeyItem[] = [
53
+ ["a", handlerA],
54
+ ["b", handlerB],
55
+ ];
56
+
57
+ matchAndRun(makeEvent({ key: "b" }), items, DEFAULT_IGNORE);
58
+
59
+ expect(handlerB).toHaveBeenCalledTimes(1);
60
+ expect(handlerA).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it("matches only the exact modifier set (extra modifier rejects)", () => {
64
+ const handler = vi.fn();
65
+ const items: HotkeyItem[] = [["ctrl+k", handler]];
66
+
67
+ matchAndRun(makeEvent({ key: "k", ctrlKey: true }), items, DEFAULT_IGNORE);
68
+ expect(handler).toHaveBeenCalledTimes(1);
69
+
70
+ handler.mockClear();
71
+ matchAndRun(
72
+ makeEvent({ key: "k", ctrlKey: true, shiftKey: true }),
73
+ items,
74
+ DEFAULT_IGNORE,
75
+ );
76
+ expect(handler).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it("resolves mod to meta on Apple and rejects the opposite (ctrl)", () => {
80
+ vi.stubGlobal("navigator", { platform: "MacIntel" });
81
+ const handler = vi.fn();
82
+ const items: HotkeyItem[] = [["mod+k", handler]];
83
+
84
+ matchAndRun(makeEvent({ key: "k", metaKey: true }), items, DEFAULT_IGNORE);
85
+ expect(handler).toHaveBeenCalledTimes(1);
86
+
87
+ handler.mockClear();
88
+ matchAndRun(makeEvent({ key: "k", ctrlKey: true }), items, DEFAULT_IGNORE);
89
+ expect(handler).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it("resolves mod to ctrl on non-Apple and rejects the opposite (meta)", () => {
93
+ vi.stubGlobal("navigator", { platform: "Win32" });
94
+ const handler = vi.fn();
95
+ const items: HotkeyItem[] = [["mod+k", handler]];
96
+
97
+ matchAndRun(makeEvent({ key: "k", ctrlKey: true }), items, DEFAULT_IGNORE);
98
+ expect(handler).toHaveBeenCalledTimes(1);
99
+
100
+ handler.mockClear();
101
+ matchAndRun(makeEvent({ key: "k", metaKey: true }), items, DEFAULT_IGNORE);
102
+ expect(handler).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it("ignores events whose target tag is in tagsToIgnore", () => {
106
+ const handler = vi.fn();
107
+ const items: HotkeyItem[] = [["k", handler]];
108
+ const event = makeEvent({
109
+ key: "k",
110
+ target: makeTarget({ tagName: "INPUT" }),
111
+ });
112
+
113
+ matchAndRun(event, items, DEFAULT_IGNORE);
114
+
115
+ expect(handler).not.toHaveBeenCalled();
116
+ });
117
+
118
+ it("ignores SELECT targets by default", () => {
119
+ const handler = vi.fn();
120
+ const items: HotkeyItem[] = [["k", handler]];
121
+ const event = makeEvent({
122
+ key: "k",
123
+ target: makeTarget({ tagName: "SELECT" }),
124
+ });
125
+
126
+ matchAndRun(event, items, DEFAULT_IGNORE);
127
+
128
+ expect(handler).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it("ignores contentEditable targets by default", () => {
132
+ const handler = vi.fn();
133
+ const items: HotkeyItem[] = [["k", handler]];
134
+ const event = makeEvent({
135
+ key: "k",
136
+ target: makeTarget({ tagName: "DIV", isContentEditable: true }),
137
+ });
138
+
139
+ matchAndRun(event, items, DEFAULT_IGNORE);
140
+
141
+ expect(handler).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("fires on contentEditable when triggerOnContentEditable is true", () => {
145
+ const handler = vi.fn();
146
+ const items: HotkeyItem[] = [
147
+ ["k", handler, { triggerOnContentEditable: true }],
148
+ ];
149
+ const event = makeEvent({
150
+ key: "k",
151
+ target: makeTarget({ tagName: "DIV", isContentEditable: true }),
152
+ });
153
+
154
+ matchAndRun(event, items, DEFAULT_IGNORE);
155
+
156
+ expect(handler).toHaveBeenCalledTimes(1);
157
+ });
158
+
159
+ it("ignores nothing when tagsToIgnore is empty (INPUT still fires)", () => {
160
+ const handler = vi.fn();
161
+ const items: HotkeyItem[] = [["k", handler]];
162
+ const event = makeEvent({
163
+ key: "k",
164
+ target: makeTarget({ tagName: "INPUT" }),
165
+ });
166
+
167
+ matchAndRun(event, items, []);
168
+
169
+ expect(handler).toHaveBeenCalledTimes(1);
170
+ });
171
+
172
+ it("calls preventDefault by default before the handler", () => {
173
+ const handler = vi.fn();
174
+ const items: HotkeyItem[] = [["k", handler]];
175
+ const event = makeEvent({ key: "k" });
176
+
177
+ matchAndRun(event, items, DEFAULT_IGNORE);
178
+
179
+ expect(event.preventDefault).toHaveBeenCalledTimes(1);
180
+ });
181
+
182
+ it("does not call preventDefault when the binding opts out", () => {
183
+ const handler = vi.fn();
184
+ const items: HotkeyItem[] = [["k", handler, { preventDefault: false }]];
185
+ const event = makeEvent({ key: "k" });
186
+
187
+ matchAndRun(event, items, DEFAULT_IGNORE);
188
+
189
+ expect(handler).toHaveBeenCalledTimes(1);
190
+ expect(event.preventDefault).not.toHaveBeenCalled();
191
+ });
192
+
193
+ it("does not fire when no binding matches", () => {
194
+ const handler = vi.fn();
195
+ const items: HotkeyItem[] = [["k", handler]];
196
+ const event = makeEvent({ key: "j" });
197
+
198
+ matchAndRun(event, items, DEFAULT_IGNORE);
199
+
200
+ expect(handler).not.toHaveBeenCalled();
201
+ expect(event.preventDefault).not.toHaveBeenCalled();
202
+ });
203
+ });
@@ -0,0 +1,62 @@
1
+ import { shouldIgnoreEvent } from "./is-input-field";
2
+ import { matchKeyModifiers } from "./match-key-modifiers";
3
+
4
+ /**
5
+ * Structural subset of a keyboard event used by the matching core. Both the
6
+ * DOM `KeyboardEvent` (used by `useHotkey`) and React's synthetic
7
+ * `KeyboardEvent` (used by `getHotkeyHandler`) satisfy this shape, so the core
8
+ * stays React-free and requires no casts at either call site.
9
+ */
10
+ export type KeyboardEventLike = Pick<
11
+ KeyboardEvent,
12
+ "key" | "shiftKey" | "ctrlKey" | "altKey" | "metaKey"
13
+ > & {
14
+ target: EventTarget | null;
15
+ preventDefault: () => void;
16
+ };
17
+
18
+ /**
19
+ * Per-binding options. Defaults: `preventDefault` true,
20
+ * `triggerOnContentEditable` false, `tagsToIgnore` is resolved by the caller
21
+ * (`useHotkey` passes its configured list; `getHotkeyHandler` passes `[]`).
22
+ */
23
+ export type HotkeyItemOptions = {
24
+ preventDefault?: boolean;
25
+ triggerOnContentEditable?: boolean;
26
+ };
27
+
28
+ /** A single hotkey binding: `[key, handler, options?]`. */
29
+ export type HotkeyItem = [
30
+ key: string,
31
+ handler: (event: KeyboardEventLike) => void,
32
+ options?: HotkeyItemOptions,
33
+ ];
34
+
35
+ /**
36
+ * The shared matching engine. For each binding it:
37
+ * 1. skips the event if the target should be ignored (tag / contentEditable);
38
+ * 2. parses the binding key (resolving `mod` to `meta`/`ctrl` per platform);
39
+ * 3. strictly matches the main key and all four modifiers;
40
+ * 4. calls `preventDefault` (unless opted out) then the handler.
41
+ *
42
+ * `tagsToIgnore` is a core parameter: `useHotkey` passes its configured list,
43
+ * while `getHotkeyHandler` passes `[]` (element-scoped, so it ignores nothing).
44
+ */
45
+ export const matchAndRun = (
46
+ event: KeyboardEventLike,
47
+ items: HotkeyItem[],
48
+ tagsToIgnore: string[],
49
+ ): void => {
50
+ for (const [key, handler, options] of items) {
51
+ const triggerOnContentEditable = options?.triggerOnContentEditable ?? false;
52
+ if (shouldIgnoreEvent(event, tagsToIgnore, triggerOnContentEditable)) {
53
+ continue;
54
+ }
55
+
56
+ if (!matchKeyModifiers(event, key)) continue;
57
+
58
+ const preventDefault = options?.preventDefault ?? true;
59
+ if (preventDefault) event.preventDefault();
60
+ handler(event);
61
+ }
62
+ };
@@ -0,0 +1,65 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import type { KeyboardEventLike } from "./match-and-run";
3
+ import { matchKeyModifiers } from "./match-key-modifiers";
4
+
5
+ const makeEvent = (
6
+ overrides: Partial<KeyboardEventLike> & { key: string },
7
+ ): KeyboardEventLike => ({
8
+ key: overrides.key,
9
+ shiftKey: overrides.shiftKey ?? false,
10
+ ctrlKey: overrides.ctrlKey ?? false,
11
+ altKey: overrides.altKey ?? false,
12
+ metaKey: overrides.metaKey ?? false,
13
+ target: null,
14
+ preventDefault: vi.fn(),
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.unstubAllGlobals();
19
+ });
20
+
21
+ describe("matchKeyModifiers", () => {
22
+ it("matches a bare key with no modifiers", () => {
23
+ expect(matchKeyModifiers(makeEvent({ key: "k" }), "k")).toBe(true);
24
+ });
25
+
26
+ it("is case-insensitive on the main key", () => {
27
+ expect(matchKeyModifiers(makeEvent({ key: "K" }), "k")).toBe(true);
28
+ });
29
+
30
+ it("requires the exact modifier set", () => {
31
+ expect(
32
+ matchKeyModifiers(makeEvent({ key: "k", ctrlKey: true }), "ctrl+k"),
33
+ ).toBe(true);
34
+ expect(
35
+ matchKeyModifiers(
36
+ makeEvent({ key: "k", ctrlKey: true, shiftKey: true }),
37
+ "ctrl+k",
38
+ ),
39
+ ).toBe(false);
40
+ });
41
+
42
+ it("rejects when a required modifier is missing", () => {
43
+ expect(matchKeyModifiers(makeEvent({ key: "k" }), "ctrl+k")).toBe(false);
44
+ });
45
+
46
+ it("resolves mod to meta on Apple and requires ctrl absent", () => {
47
+ vi.stubGlobal("navigator", { platform: "MacIntel" });
48
+ expect(
49
+ matchKeyModifiers(makeEvent({ key: "k", metaKey: true }), "mod+k"),
50
+ ).toBe(true);
51
+ expect(
52
+ matchKeyModifiers(makeEvent({ key: "k", ctrlKey: true }), "mod+k"),
53
+ ).toBe(false);
54
+ });
55
+
56
+ it("resolves mod to ctrl on non-Apple and requires meta absent", () => {
57
+ vi.stubGlobal("navigator", { platform: "Win32" });
58
+ expect(
59
+ matchKeyModifiers(makeEvent({ key: "k", ctrlKey: true }), "mod+k"),
60
+ ).toBe(true);
61
+ expect(
62
+ matchKeyModifiers(makeEvent({ key: "k", metaKey: true }), "mod+k"),
63
+ ).toBe(false);
64
+ });
65
+ });
@@ -1,25 +1,52 @@
1
- export const getKeyModifiers = (key: string) => {
1
+ import { isAppleDevice } from "../../internal";
2
+ import type { KeyboardEventLike } from "./match-and-run";
3
+
4
+ type ParsedKey = {
5
+ mainKey: string;
6
+ shift: boolean;
7
+ ctrl: boolean;
8
+ alt: boolean;
9
+ meta: boolean;
10
+ };
11
+
12
+ /**
13
+ * Parses a hotkey string (e.g. `"mod+shift+k"`) into its main key and the
14
+ * exact set of required modifiers. `mod` resolves to `meta` on Apple platforms
15
+ * and `ctrl` elsewhere; the opposite modifier is then required to be absent
16
+ * (it stays `false` here, enforcing the strict exact-match rule).
17
+ */
18
+ export const getKeyModifiers = (key: string): ParsedKey => {
2
19
  const parts = key.toLowerCase().split("+");
3
- const mainKey = parts.pop()!;
20
+ const mainKey = parts.pop() ?? "";
21
+
22
+ const hasMod = parts.includes("mod");
23
+ const modIsMeta = hasMod && isAppleDevice();
24
+ const modIsCtrl = hasMod && !isAppleDevice();
25
+
4
26
  return {
5
27
  mainKey,
6
- isShift: parts.includes("shift"),
7
- isCtrl: parts.includes("ctrl"),
8
- isAlt: parts.includes("alt"),
9
- isMeta: parts.includes("meta"),
28
+ shift: parts.includes("shift"),
29
+ ctrl: parts.includes("ctrl") || modIsCtrl,
30
+ alt: parts.includes("alt"),
31
+ meta: parts.includes("meta") || modIsMeta,
10
32
  };
11
33
  };
12
34
 
35
+ /**
36
+ * Returns `true` only when the event's main key matches AND every modifier
37
+ * state (shift/ctrl/alt/meta) exactly equals the parsed requirement. This is a
38
+ * strict match: any extra or missing modifier rejects the binding.
39
+ */
13
40
  export const matchKeyModifiers = (
14
- event: KeyboardEvent,
41
+ event: KeyboardEventLike,
15
42
  key: string,
16
43
  ): boolean => {
17
- const { mainKey, isShift, isCtrl, isAlt, isMeta } = getKeyModifiers(key);
44
+ const { mainKey, shift, ctrl, alt, meta } = getKeyModifiers(key);
18
45
  return (
19
46
  event.key.toLowerCase() === mainKey &&
20
- event.shiftKey === isShift &&
21
- event.ctrlKey === isCtrl &&
22
- event.altKey === isAlt &&
23
- event.metaKey === isMeta
47
+ event.shiftKey === shift &&
48
+ event.ctrlKey === ctrl &&
49
+ event.altKey === alt &&
50
+ event.metaKey === meta
24
51
  );
25
52
  };