Skip to content

Commit

Permalink
[@mantine/hooks] use-hotkeys: Add better support for non-QUERTY keybo…
Browse files Browse the repository at this point in the history
…ards (#7390)

* Updating use-hotkeys to support physical keys. Needed for Dvorak et. al.

* Updating docs.

* Updating tests.
  • Loading branch information
danpeavey-classdojo authored Jan 26, 2025
1 parent f7347e9 commit bb6f692
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 23 deletions.
9 changes: 9 additions & 0 deletions apps/mantine.dev/src/pages/hooks/use-hotkeys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -83,6 +84,7 @@ document.body.addEventListener(
```tsx
interface HotkeyItemOptions {
preventDefault?: boolean;
usePhysicalKeys?: boolean;
}

type HotkeyItem = [
Expand All @@ -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
Expand All @@ -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);
Expand Down
29 changes: 16 additions & 13 deletions packages/@mantine/hooks/src/use-hotkeys/parse-hotkey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -64,36 +64,39 @@ 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;
}

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?];

export function getHotkeyHandler(hotkeys: HotkeyItem[]) {
return (event: React.KeyboardEvent<HTMLElement> | 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);
}
});
);
};
}
21 changes: 21 additions & 0 deletions packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
22 changes: 12 additions & 10 deletions packages/@mantine/hooks/src/use-hotkeys/use-hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit bb6f692

Please sign in to comment.