Skip to content

Commit 25cd546

Browse files
committed
Add ignoreRepeat option to skip callback on held keys
Uses KeyboardEvent.repeat to detect when a key is being held down and skips the callback invocation when ignoreRepeat is enabled.
1 parent f2006c7 commit 25cd546

File tree

3 files changed

+67
-0
lines changed

3 files changed

+67
-0
lines changed

packages/react-hotkeys-hook/src/lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export type Options = {
8787
sequenceTimeoutMs?: number
8888
// The character to split the sequence of keys. (Default: >)
8989
sequenceSplitKey?: string
90+
// Ignore repeated events when the key is held down. (Default: false)
91+
ignoreRepeat?: boolean
9092
// MetaData | Custom data to store and retrieve with the hotkey (Default: undefined)
9193
metadata?: Record<string, unknown>
9294
}

packages/react-hotkeys-hook/src/lib/useHotkeys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export default function useHotkeys<T extends HTMLElement>(
6767
let sequenceTimer: NodeJS.Timeout | undefined
6868

6969
const listener = (e: KeyboardEvent, isKeyUp = false) => {
70+
if (memoisedOptions?.ignoreRepeat && e.repeat) {
71+
return
72+
}
73+
7074
if (isKeyboardEventTriggeredByInput(e) && !isHotkeyEnabledOnTag(e, memoisedOptions?.enableOnFormTags)) {
7175
return
7276
}

packages/react-hotkeys-hook/src/test/useHotkeys.test.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,67 @@ test('should be disabled on form tags inside custom elements by default', async
17071707
expect(within(getByTestId('form-tag').shadowRoot).getByTestId('input')).toHaveValue('A')
17081708
})
17091709

1710+
test('should not trigger callback on repeat events when ignoreRepeat is set to true', () => {
1711+
const callback = vi.fn()
1712+
1713+
renderHook(() => useHotkeys('a', callback, { ignoreRepeat: true }))
1714+
1715+
// First keydown (not a repeat) should trigger
1716+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: false, bubbles: true }))
1717+
1718+
expect(callback).toHaveBeenCalledTimes(1)
1719+
1720+
// Repeated keydown events (held key) should not trigger
1721+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1722+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1723+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1724+
1725+
expect(callback).toHaveBeenCalledTimes(1)
1726+
})
1727+
1728+
test('should trigger callback on repeat events by default', () => {
1729+
const callback = vi.fn()
1730+
1731+
renderHook(() => useHotkeys('a', callback))
1732+
1733+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: false, bubbles: true }))
1734+
1735+
expect(callback).toHaveBeenCalledTimes(1)
1736+
1737+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1738+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1739+
1740+
expect(callback).toHaveBeenCalledTimes(3)
1741+
})
1742+
1743+
test('should trigger callback on repeat events when ignoreRepeat is set to false', () => {
1744+
const callback = vi.fn()
1745+
1746+
renderHook(() => useHotkeys('a', callback, { ignoreRepeat: false }))
1747+
1748+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: false, bubbles: true }))
1749+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1750+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', repeat: true, bubbles: true }))
1751+
1752+
expect(callback).toHaveBeenCalledTimes(3)
1753+
})
1754+
1755+
test('should not trigger callback on repeat events for modifier combinations when ignoreRepeat is set', () => {
1756+
const callback = vi.fn()
1757+
1758+
renderHook(() => useHotkeys('meta+a', callback, { ignoreRepeat: true }))
1759+
1760+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Meta', code: 'MetaLeft', metaKey: true, repeat: false, bubbles: true }))
1761+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', metaKey: true, repeat: false, bubbles: true }))
1762+
1763+
expect(callback).toHaveBeenCalledTimes(1)
1764+
1765+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', metaKey: true, repeat: true, bubbles: true }))
1766+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', code: 'KeyA', metaKey: true, repeat: true, bubbles: true }))
1767+
1768+
expect(callback).toHaveBeenCalledTimes(1)
1769+
})
1770+
17101771
test('Should trigger only produced key hotkeys', async () => {
17111772
const user = userEvent.setup()
17121773

0 commit comments

Comments
 (0)