diff --git a/src/isHotkeyPressed.ts b/src/isHotkeyPressed.ts index b10b2ec1..54490d57 100644 --- a/src/isHotkeyPressed.ts +++ b/src/isHotkeyPressed.ts @@ -29,8 +29,13 @@ import { isHotkeyModifier, mapKey } from './parseHotkeys' const currentlyPressedKeys: Set = new Set() -export function isHotkeyPressed(key: string | string[], splitKey = ','): boolean { - const hotkeyArray = Array.isArray(key) ? key : key.split(splitKey) +// https://github.com/microsoft/TypeScript/issues/17002 +export function isReadonlyArray(value: unknown): value is readonly unknown[] { + return Array.isArray(value) +} + +export function isHotkeyPressed(key: string | readonly string[], splitKey = ','): boolean { + const hotkeyArray = isReadonlyArray(key) ? key : key.split(splitKey) return hotkeyArray.every((hotkey) => currentlyPressedKeys.has(hotkey.trim().toLowerCase())) } diff --git a/src/types.ts b/src/types.ts index a99c6fbc..a5181c29 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,8 @@ import type { DependencyList } from 'react' export type FormTags = 'input' | 'textarea' | 'select' | 'INPUT' | 'TEXTAREA' | 'SELECT' -export type Keys = string | string[] -export type Scopes = string | string[] +export type Keys = string | readonly string[] +export type Scopes = string | readonly string[] export type RefType = T | null @@ -15,7 +15,7 @@ export type KeyboardModifiers = { } export type Hotkey = KeyboardModifiers & { - keys?: string[] + keys?: readonly string[] scopes?: Scopes description?: string } @@ -28,7 +28,7 @@ export type Trigger = boolean | ((keyboardEvent: KeyboardEvent, hotkeysEvent: Ho export type Options = { enabled?: Trigger // Main setting that determines if the hotkey is enabled or not. (Default: true) - enableOnFormTags?: FormTags[] | boolean // Enable hotkeys on a list of tags. (Default: false) + enableOnFormTags?: readonly FormTags[] | boolean // Enable hotkeys on a list of tags. (Default: false) enableOnContentEditable?: boolean // Enable hotkeys on tags with contentEditable props. (Default: false) ignoreEventWhen?: (e: KeyboardEvent) => boolean // Ignore evenets based on a condition (Default: undefined) combinationKey?: string // Character to split keys in hotkeys combinations. (Default: +) diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index fd22fd21..74c573b6 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -12,7 +12,7 @@ import { import { useHotkeysContext } from './HotkeysProvider' import { useBoundHotkeysProxy } from './BoundHotkeysProxyProvider' import useDeepEqualMemo from './useDeepEqualMemo' -import { pushToCurrentlyPressedKeys, removeFromCurrentlyPressedKeys } from './isHotkeyPressed' +import { isReadonlyArray, pushToCurrentlyPressedKeys, removeFromCurrentlyPressedKeys } from './isHotkeyPressed' const stopPropagation = (e: KeyboardEvent): void => { e.stopPropagation() @@ -36,7 +36,7 @@ export default function useHotkeys( : !(dependencies instanceof Array) ? (dependencies as Options) : undefined - const _keys: string = keys instanceof Array ? keys.join(_options?.splitKey) : keys + const _keys: string = isReadonlyArray(keys ) ? keys.join(_options?.splitKey) : keys const _deps: DependencyList | undefined = options instanceof Array ? options : dependencies instanceof Array ? dependencies : undefined diff --git a/src/validators.ts b/src/validators.ts index 59081bcf..629cc783 100644 --- a/src/validators.ts +++ b/src/validators.ts @@ -1,5 +1,5 @@ import { FormTags, Hotkey, Scopes, Trigger } from './types' -import { isHotkeyPressed } from './isHotkeyPressed' +import { isHotkeyPressed, isReadonlyArray } from './isHotkeyPressed' import { mapKey } from './parseHotkeys' export function maybePreventDefault(e: KeyboardEvent, hotkey: Hotkey, preventDefault?: Trigger): void { @@ -20,10 +20,10 @@ export function isKeyboardEventTriggeredByInput(ev: KeyboardEvent): boolean { return isHotkeyEnabledOnTag(ev, ['input', 'textarea', 'select']) } -export function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags: FormTags[] | boolean = false): boolean { +export function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags: readonly FormTags[] | boolean = false): boolean { const targetTagName = target && (target as HTMLElement).tagName - if (enabledOnTags instanceof Array) { + if (isReadonlyArray(enabledOnTags)) { return Boolean( targetTagName && enabledOnTags && enabledOnTags.some((tag) => tag.toLowerCase() === targetTagName.toLowerCase()) ) diff --git a/tests/useHotkeys.test.tsx b/tests/useHotkeys.test.tsx index 4de1c515..5d0db463 100644 --- a/tests/useHotkeys.test.tsx +++ b/tests/useHotkeys.test.tsx @@ -238,7 +238,7 @@ test('should listen to multiple hotkeys', async () => { expect(callback).toHaveBeenCalledTimes(2) }) -test('should be able to parse first argument as string or array', async () => { +test('should be able to parse first argument as string, array or readonly array', async () => { const user = userEvent.setup() const callback = jest.fn() @@ -257,6 +257,12 @@ test('should be able to parse first argument as string or array', async () => { await user.keyboard('B') expect(callback).toHaveBeenCalledTimes(2) + + rerender({ keys: ['a', 'c'] as const }) + + await user.keyboard('C') + + expect(callback).toHaveBeenCalledTimes(3) }) test('should listen to combinations with modifiers', async () => { @@ -457,7 +463,7 @@ test('should be disabled on form tags by default', async () => { test('should be enabled on given form tags', async () => { const user = userEvent.setup() const callback = jest.fn() - const Component = ({ cb, enableOnFormTags }: { cb: HotkeyCallback; enableOnFormTags?: FormTags[] }) => { + const Component = ({ cb, enableOnFormTags }: { cb: HotkeyCallback; enableOnFormTags?: readonly FormTags[] }) => { useHotkeys('a', cb, { enableOnFormTags }) return