diff --git a/apps/mantine.dev/src/pages/hooks/use-hotkeys.mdx b/apps/mantine.dev/src/pages/hooks/use-hotkeys.mdx index 95ddd1c152..24a51b168b 100644 --- a/apps/mantine.dev/src/pages/hooks/use-hotkeys.mdx +++ b/apps/mantine.dev/src/pages/hooks/use-hotkeys.mdx @@ -75,6 +75,7 @@ document.body.addEventListener( - `alt + shift + L` – you can use whitespace inside hotkey - `ArrowLeft` – you can use special keys using [this format](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) - `shift + [plus]` – you can use `[plus]` to detect `+` key +- `Digit1` and `Hotkey1` - You can use physical key assignments [defined on MDN](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values). ## Types @@ -83,6 +84,7 @@ document.body.addEventListener( ```tsx interface HotkeyItemOptions { preventDefault?: boolean; + usePhysicalKeys?: boolean; } type HotkeyItem = [ @@ -92,6 +94,8 @@ type HotkeyItem = [ ]; ``` +`HotkeyItemOptions` provides the `usePhysicalKeys` option to force the physical key assignment. Useful for non-QWERTY keyboard layouts. + `HotkeyItem` type can be used to create hotkey items outside of `use-hotkeys` hook: ```tsx @@ -105,6 +109,11 @@ const hotkeys: HotkeyItem[] = [ ], ['ctrl+K', () => console.log('Trigger search')], ['alt+mod+shift+X', () => console.log('Rick roll')], + [ + 'D', + () => console.log('Triggers when pressing "E" on Dvorak keyboards!'), + { usePhysicalKeys: true } + ], ]; useHotkeys(hotkeys); diff --git a/packages/@mantine/hooks/src/use-hotkeys/parse-hotkey.ts b/packages/@mantine/hooks/src/use-hotkeys/parse-hotkey.ts index 8069c1485f..910c2d3713 100644 --- a/packages/@mantine/hooks/src/use-hotkeys/parse-hotkey.ts +++ b/packages/@mantine/hooks/src/use-hotkeys/parse-hotkey.ts @@ -38,9 +38,9 @@ export function parseHotkey(hotkey: string): Hotkey { }; } -function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean { +function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent, usePhysicalKeys?: boolean): boolean { const { alt, ctrl, meta, mod, shift, key } = hotkey; - const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event; + const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey, code: pressedCode } = event; if (alt !== altKey) { return false; @@ -64,8 +64,8 @@ function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean { if ( key && - (pressedKey.toLowerCase() === key.toLowerCase() || - event.code.replace('Key', '').toLowerCase() === key.toLowerCase()) + ((!usePhysicalKeys && pressedKey.toLowerCase() === key.toLowerCase()) || + pressedCode.replace('Key', '').toLowerCase() === key.toLowerCase()) ) { return true; } @@ -73,12 +73,13 @@ function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean { return false; } -export function getHotkeyMatcher(hotkey: string): CheckHotkeyMatch { - return (event) => isExactHotkey(parseHotkey(hotkey), event); +export function getHotkeyMatcher(hotkey: string, usePhysicalKeys?: boolean): CheckHotkeyMatch { + return (event) => isExactHotkey(parseHotkey(hotkey), event, usePhysicalKeys); } export interface HotkeyItemOptions { preventDefault?: boolean; + usePhysicalKeys?: boolean; } type HotkeyItem = [string, (event: any) => void, HotkeyItemOptions?]; @@ -86,14 +87,16 @@ type HotkeyItem = [string, (event: any) => void, HotkeyItemOptions?]; export function getHotkeyHandler(hotkeys: HotkeyItem[]) { return (event: React.KeyboardEvent | KeyboardEvent) => { const _event = 'nativeEvent' in event ? event.nativeEvent : event; - hotkeys.forEach(([hotkey, handler, options = { preventDefault: true }]) => { - if (getHotkeyMatcher(hotkey)(_event)) { - if (options.preventDefault) { - event.preventDefault(); + hotkeys.forEach( + ([hotkey, handler, options = { preventDefault: true, usePhysicalKeys: false }]) => { + if (getHotkeyMatcher(hotkey, options.usePhysicalKeys)(_event)) { + if (options.preventDefault) { + event.preventDefault(); + } + + handler(_event); } - - handler(_event); } - }); + ); }; } diff --git a/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.test.tsx b/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.test.tsx index 6c9aa394f0..549dc285d3 100644 --- a/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.test.tsx +++ b/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.test.tsx @@ -34,4 +34,25 @@ describe('@mantine/hooks/use-hotkey', () => { dispatchEvent({ shiftKey: true, key: '+' }); expect(handler).toHaveBeenCalled(); }); + + it('correctly handles physical key assignments like Digit1', () => { + const handler = jest.fn(); + renderHook(() => useHotkeys([['Digit1', handler]])); + dispatchEvent({ code: 'Digit1' }); + expect(handler).toHaveBeenCalled(); + }); + + it('correctly ignores unclear numerical assignments when usePhyiscalKeys is true', () => { + const handler = jest.fn(); + renderHook(() => useHotkeys([['1', handler, { usePhysicalKeys: true }]], [], true)); + dispatchEvent({ code: 'Numpad1' }); + expect(handler).not.toHaveBeenCalled(); + }); + + it('correctly assumes physical keys when usePhysicalKeys is true', () => { + const handler = jest.fn(); + renderHook(() => useHotkeys([['A', handler, { usePhysicalKeys: true }]], [], true)); + dispatchEvent({ code: 'KeyA' }); + expect(handler).toHaveBeenCalled(); + }); }); diff --git a/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.ts b/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.ts index 92eb2730fd..dd7624d56d 100644 --- a/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.ts +++ b/packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.ts @@ -29,18 +29,20 @@ export function useHotkeys( ) { useEffect(() => { const keydownListener = (event: KeyboardEvent) => { - hotkeys.forEach(([hotkey, handler, options = { preventDefault: true }]) => { - if ( - getHotkeyMatcher(hotkey)(event) && - shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable) - ) { - if (options.preventDefault) { - event.preventDefault(); + hotkeys.forEach( + ([hotkey, handler, options = { preventDefault: true, usePhysicalKeys: false }]) => { + if ( + getHotkeyMatcher(hotkey, options.usePhysicalKeys)(event) && + shouldFireEvent(event, tagsToIgnore, triggerOnContentEditable) + ) { + if (options.preventDefault) { + event.preventDefault(); + } + + handler(event); } - - handler(event); } - }); + ); }; document.documentElement.addEventListener('keydown', keydownListener);