All files / libs/ext/react/hooks/src/lib/use-keyboard-event use-keyboard-event.ts

0% Statements 0/88
0% Branches 0/1
0% Functions 0/1
0% Lines 0/88

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89                                                                                                                                                                                 
import { match } from 'ts-pattern';

import { useWindowEvent } from '../use-window-event/use-window-event';

export type KbdEventType = 'keydown' | 'keypress' | 'keyup';

const KBD_MOFIFIERS = ['alt', 'ctrl', 'meta', 'shift'] as const;
export type KbdModifier = (typeof KBD_MOFIFIERS)[number];

export type KbdKey =
  | 'a'
  | 'b'
  | 'c'
  | 'd'
  | 'e'
  | 'f'
  | 'g'
  | 'h'
  | 'i'
  | 'j'
  | 'k'
  | 'l'
  | 'm'
  | 'n'
  | 'o'
  | 'p'
  | 'q'
  | 'r'
  | 's'
  | 't'
  | 'u'
  | 'v'
  | 'w'
  | 'x'
  | 'y'
  | 'z';

export const KBD_SEPARATOR = '.';
type KbdSeparator = typeof KBD_SEPARATOR;

export type KbdCode =
  | `${KbdKey}`
  | `${KbdModifier}${KbdSeparator}${KbdKey}`
  | `${KbdModifier}${KbdSeparator}${KbdModifier}${KbdSeparator}${KbdKey}`
  | `${KbdModifier}${KbdSeparator}${KbdModifier}${KbdSeparator}${KbdModifier}${KbdSeparator}${KbdKey}`
  | `${KbdModifier}${KbdSeparator}${KbdModifier}${KbdSeparator}${KbdModifier}${KbdSeparator}${KbdModifier}${KbdSeparator}${KbdKey}`;

const eventMatchesKbdCode = (code: KbdCode, event: KeyboardEvent) => {
  const parts = code.split(KBD_SEPARATOR) as (KbdKey | KbdModifier)[];
  const modifiers = new Set(parts.slice(0, -1));
  const key = parts.at(-1)!;

  return (
    event.key.toLowerCase() === key &&
    // Make sure only specified modifiers are pressed (e.g. ctrl.a doesn't match ctrl + shift + a).
    KBD_MOFIFIERS.every((modifier) => {
      const required = modifiers.has(modifier);
      return match(modifier)
        .with('alt', () => event.altKey === required)
        .with('ctrl', () => event.ctrlKey === required)
        .with('meta', () => event.metaKey === required)
        .with('shift', () => event.shiftKey === required)
        .exhaustive();
    })
  );
};

export const useKeyboardEvent = (
  eventType: KbdEventType,
  filter: KbdCode | KbdCode[] | ((event: KeyboardEvent) => boolean),
  listener: (ev: KeyboardEvent) => void,
  options?: AddEventListenerOptions | boolean,
) =>
  useWindowEvent(
    eventType,
    (event) => {
      if (
        typeof filter === 'function'
          ? filter(event)
          : Array.isArray(filter)
            ? filter.some((code) => eventMatchesKbdCode(code, event))
            : eventMatchesKbdCode(filter, event)
      ) {
        listener(event);
      }
    },
    options,
  );