@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/button/button.cjs.js +1 -1
- package/dist/components/button/button.es.js +19 -18
- package/dist/components/button/components/base-button.cjs.js +1 -1
- package/dist/components/button/components/base-button.es.js +20 -20
- package/dist/components/calendar/calendar.cjs.js +1 -1
- package/dist/components/calendar/calendar.es.js +1 -0
- package/dist/components/date-picker/date-input.cjs.js +1 -1
- package/dist/components/date-picker/date-input.es.js +92 -75
- package/dist/components/date-picker/date-picker.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.es.js +104 -95
- package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
- package/dist/components/date-picker/date-picker.utils.es.js +51 -43
- package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
- package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
- package/dist/components/menu/menu.es.js +1 -9
- package/dist/components/otp/otp.cjs.js +2 -0
- package/dist/components/otp/otp.es.js +93 -0
- package/dist/components/password/password.cjs.js +1 -1
- package/dist/components/password/password.es.js +2 -2
- package/dist/components/select/select.cjs.js +1 -1
- package/dist/components/select/select.es.js +68 -60
- package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
- package/dist/hooks/internal/is-apple-device.es.js +9 -0
- package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
- package/dist/hooks/internal/use-latest-ref.es.js +11 -0
- package/dist/hooks/use-array/use-array.cjs.js +1 -1
- package/dist/hooks/use-array/use-array.es.js +54 -42
- package/dist/hooks/use-async/use-async.cjs.js +1 -1
- package/dist/hooks/use-async/use-async.es.js +53 -20
- package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
- package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
- package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
- package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
- package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
- package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
- package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
- package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
- package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
- package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
- package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
- package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
- package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
- package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
- package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
- package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
- package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
- package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
- package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
- package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
- package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
- package/dist/hooks/use-hover/use-hover.es.js +32 -17
- package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
- package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
- package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
- package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
- package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
- package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
- package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
- package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
- package/dist/hooks/use-object/use-object.cjs.js +1 -1
- package/dist/hooks/use-object/use-object.es.js +26 -22
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
- package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
- package/dist/hooks/use-step/use-step.cjs.js +1 -1
- package/dist/hooks/use-step/use-step.es.js +25 -24
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +308 -300
- package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
- package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/hooks/index.d.ts +2 -2
- package/dist/src/hooks/internal/index.d.ts +2 -0
- package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
- package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
- package/dist/src/hooks/use-array/use-array.d.ts +24 -11
- package/dist/src/hooks/use-async/use-async.d.ts +16 -13
- package/dist/src/hooks/use-boolean/index.d.ts +1 -0
- package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
- package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
- package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
- package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
- package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
- package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
- package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
- package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
- package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
- package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
- package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
- package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
- package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
- package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
- package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
- package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
- package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
- package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
- package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
- package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
- package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
- package/dist/src/hooks/use-object/use-object.d.ts +15 -6
- package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
- package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
- package/dist/src/hooks/use-step/use-step.d.ts +18 -11
- package/dist/src/utils/form.d.ts +10 -0
- package/package.json +1 -1
- package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
- package/src/components/auto-complete/auto-complete.test.tsx +4 -14
- package/src/components/avatar/avatar.test.tsx +7 -12
- package/src/components/button/button.test.tsx +10 -15
- package/src/components/button/button.tsx +14 -9
- package/src/components/button/components/base-button.tsx +2 -4
- package/src/components/calendar/calendar.test.tsx +12 -19
- package/src/components/calendar/calendar.tsx +4 -0
- package/src/components/card/card.test.tsx +4 -6
- package/src/components/checkbox/checkbox.test.tsx +12 -8
- package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
- package/src/components/combobox/combobox.test.tsx +24 -21
- package/src/components/date-picker/date-input-form.test.tsx +77 -0
- package/src/components/date-picker/date-input.stories.tsx +30 -18
- package/src/components/date-picker/date-input.tsx +77 -44
- package/src/components/date-picker/date-picker.stories.tsx +31 -1
- package/src/components/date-picker/date-picker.test.tsx +3 -13
- package/src/components/date-picker/date-picker.tsx +35 -16
- package/src/components/date-picker/date-picker.utils.test.ts +32 -14
- package/src/components/date-picker/date-picker.utils.ts +33 -0
- package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
- package/src/components/date-picker/use-hidden-field-value.ts +23 -0
- package/src/components/dialog/dialog.test.tsx +10 -8
- package/src/components/dropzone/dropzone.test.tsx +11 -13
- package/src/components/empty/empty.test.tsx +4 -3
- package/src/components/field/field.test.tsx +12 -13
- package/src/components/form/form.stories.tsx +16 -1
- package/src/components/index.ts +1 -0
- package/src/components/label/label.test.tsx +3 -3
- package/src/components/menu/menu.tsx +1 -5
- package/src/components/number-input/number-input.test.tsx +6 -2
- package/src/components/password/password.test.tsx +20 -6
- package/src/components/password/password.tsx +2 -2
- package/src/components/popover/popover.test.tsx +4 -4
- package/src/components/progress/progress.test.tsx +7 -8
- package/src/components/radio-group/radio-group.test.tsx +17 -11
- package/src/components/select/select.test.tsx +10 -10
- package/src/components/select/select.tsx +9 -1
- package/src/components/stepper/stepper.stories.tsx +11 -15
- package/src/components/stepper/stepper.test.tsx +6 -4
- package/src/components/switch/switch.test.tsx +3 -3
- package/src/components/table/table.test.tsx +9 -3
- package/src/components/tabs/tabs.test.tsx +6 -2
- package/src/components/tag/tag.test.tsx +1 -3
- package/src/components/textarea/textarea.test.tsx +4 -1
- package/src/components/timeline/timeline.test.tsx +10 -5
- package/src/components/toast/toast.test.tsx +11 -14
- package/src/components/tooltip/tooltip.test.tsx +1 -5
- package/src/components/tree/tree.test.tsx +3 -1
- package/src/hooks/index.ts +2 -2
- package/src/hooks/internal/index.ts +2 -0
- package/src/hooks/internal/is-apple-device.test.ts +41 -0
- package/src/hooks/internal/is-apple-device.ts +33 -0
- package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
- package/src/hooks/internal/use-latest-ref.ts +21 -0
- package/src/hooks/use-array/use-array.stories.tsx +435 -64
- package/src/hooks/use-array/use-array.test.tsx +398 -15
- package/src/hooks/use-array/use-array.ts +105 -66
- package/src/hooks/use-async/use-async.stories.tsx +255 -131
- package/src/hooks/use-async/use-async.test.ts +397 -0
- package/src/hooks/use-async/use-async.ts +117 -39
- package/src/hooks/use-boolean/index.ts +1 -0
- package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
- package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
- package/src/hooks/use-boolean/use-boolean.ts +50 -0
- package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
- package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
- package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
- package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
- package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
- package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
- package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
- package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
- package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
- package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
- package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
- package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
- package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
- package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
- package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
- package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
- package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
- package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
- package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
- package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
- package/src/hooks/use-hotkey/index.ts +9 -1
- package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
- package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
- package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
- package/src/hooks/use-hotkey/utils/index.ts +8 -3
- package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
- package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
- package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
- package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
- package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
- package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
- package/src/hooks/use-hover/use-hover.test.tsx +266 -26
- package/src/hooks/use-hover/use-hover.tsx +93 -28
- package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
- package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
- package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
- package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
- package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
- package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
- package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
- package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
- package/src/hooks/use-media-query/use-media-query.ts +39 -14
- package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
- package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
- package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
- package/src/hooks/use-mutation/use-mutation.ts +97 -0
- package/src/hooks/use-object/use-object.stories.tsx +310 -79
- package/src/hooks/use-object/use-object.test.tsx +235 -56
- package/src/hooks/use-object/use-object.ts +59 -0
- package/src/hooks/use-pagination/use-pagination.tsx +0 -1
- package/src/hooks/use-prevent-page-close/index.ts +1 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
- package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
- package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
- package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
- package/src/hooks/use-selection/use-selection.ts +0 -1
- package/src/hooks/use-step/use-step.stories.tsx +178 -65
- package/src/hooks/use-step/use-step.test.ts +178 -53
- package/src/hooks/use-step/use-step.ts +57 -49
- package/src/utils/form.test.tsx +13 -8
- package/src/utils/form.tsx +10 -0
- package/src/utils/functions/getFormData.test.ts +1 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
- package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
- package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
- package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
- package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
- package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
- package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
- package/dist/src/hooks/use-toggle/index.d.ts +0 -1
- package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
- package/src/hooks/use-async/use-async.test.tsx +0 -68
- package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
- package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
- package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
- package/src/hooks/use-mutation/use-mutation.tsx +0 -59
- package/src/hooks/use-object/use-object.tsx +0 -46
- package/src/hooks/use-prevent-close-window/index.ts +0 -1
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
- package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
- package/src/hooks/use-toggle/index.ts +0 -1
- package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
- package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
- package/src/hooks/use-toggle/use-toggle.ts +0 -14
- /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
- /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
|
@@ -1,14 +1,35 @@
|
|
|
1
|
-
|
|
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:
|
|
11
|
-
|
|
14
|
+
event: KeyboardEventLike,
|
|
15
|
+
tagsToIgnore: string[],
|
|
16
|
+
triggerOnContentEditable: boolean,
|
|
12
17
|
): boolean => {
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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:
|
|
41
|
+
event: KeyboardEventLike,
|
|
15
42
|
key: string,
|
|
16
43
|
): boolean => {
|
|
17
|
-
const { mainKey,
|
|
44
|
+
const { mainKey, shift, ctrl, alt, meta } = getKeyModifiers(key);
|
|
18
45
|
return (
|
|
19
46
|
event.key.toLowerCase() === mainKey &&
|
|
20
|
-
event.shiftKey ===
|
|
21
|
-
event.ctrlKey ===
|
|
22
|
-
event.altKey ===
|
|
23
|
-
event.metaKey ===
|
|
47
|
+
event.shiftKey === shift &&
|
|
48
|
+
event.ctrlKey === ctrl &&
|
|
49
|
+
event.altKey === alt &&
|
|
50
|
+
event.metaKey === meta
|
|
24
51
|
);
|
|
25
52
|
};
|