Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle readonly arrays in options as we do not modify them. #1029

Merged
merged 1 commit into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/isHotkeyPressed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ import { isHotkeyModifier, mapKey } from './parseHotkeys'

const currentlyPressedKeys: Set<string> = new Set<string>()

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()))
}
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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> = T | null

Expand All @@ -15,7 +15,7 @@ export type KeyboardModifiers = {
}

export type Hotkey = KeyboardModifiers & {
keys?: string[]
keys?: readonly string[]
scopes?: Scopes
description?: string
}
Expand All @@ -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: +)
Expand Down
4 changes: 2 additions & 2 deletions src/useHotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -36,7 +36,7 @@ export default function useHotkeys<T extends HTMLElement>(
: !(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

Expand Down
6 changes: 3 additions & 3 deletions src/validators.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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())
)
Expand Down
10 changes: 8 additions & 2 deletions tests/useHotkeys.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 () => {
Expand Down Expand Up @@ -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<HTMLDivElement>('a', cb, { enableOnFormTags })

return <input type={'text'} data-testid={'form-tag'} />
Expand Down