{"version":3,"file":"useHotkeys.mjs","names":[],"sources":["../../../../package/common/hooks/useHotkeys.ts"],"sourcesContent":["import { useEffect, useRef, type RefObject } from 'react';\n\nexport type HotkeyHandler = (event: KeyboardEvent) => void;\n\n/** Combo → handler, e.g. `{ 'mod+k': open, Escape: close }`. */\nexport type HotkeyMap = Record<string, HotkeyHandler>;\n\nexport interface UseHotkeysOptions {\n  /** Attach the listener at all. Default `true`; pass `false` to suspend every combo at once. */\n  enabled?: boolean;\n  /** Scope the shortcuts to one element instead of the whole window. */\n  target?: RefObject<HTMLElement | null>;\n  /**\n   * Let plain-key combos fire while a text field or `contenteditable` has focus. Default `false`,\n   * so `'n'` does not fire for every `n` typed into an input. Combos carrying Ctrl, Cmd or Alt\n   * are never suppressed — they cannot be confused with typing.\n   */\n  enableInFields?: boolean;\n  /**\n   * Call `preventDefault()` on a match, so `mod+s` does not also open the browser's save dialog.\n   * Default `true`.\n   */\n  preventDefault?: boolean;\n  /**\n   * Fire again for each repeat while the combo is held. Default `false`, so holding `mod+s` saves\n   * once. Turn it on for keys that step through something, such as arrows or zoom.\n   */\n  repeat?: boolean;\n}\n\ninterface Combo {\n  key: string;\n  ctrl: boolean;\n  meta: boolean;\n  alt: boolean;\n  shift: boolean;\n  /** Set when the combo named Shift itself — see `matches`. */\n  shiftNamed: boolean;\n}\n\nconst FIELD_SELECTOR = 'input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]';\n\n/** Spellings accepted for keys whose `event.key` is awkward to write inside a `+`-separated combo. */\nconst KEY_ALIASES: Record<string, string> = {\n  esc: 'escape',\n  space: ' ',\n  spacebar: ' ',\n  plus: '+',\n  up: 'arrowup',\n  down: 'arrowdown',\n  left: 'arrowleft',\n  right: 'arrowright',\n};\n\n/** `mod` is Cmd where Cmd is the command modifier, and Ctrl everywhere else. */\nfunction isApplePlatform(): boolean {\n  return typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);\n}\n\nfunction parseCombo(combo: string): Combo | null {\n  const parts = combo\n    .split('+')\n    .map((part) => part.trim().toLowerCase())\n    .filter(Boolean);\n  // A trailing separator (`'mod+'`) loses its key to the filter above, so the whole combo is\n  // dropped rather than matching on the modifier alone. `'mod+plus'` is how to mean `+` itself.\n  if (parts.length === 0) return null;\n\n  const parsed: Combo = { key: '', ctrl: false, meta: false, alt: false, shift: false, shiftNamed: false };\n  const apple = isApplePlatform();\n\n  for (const part of parts) {\n    switch (part) {\n      case 'mod':\n        if (apple) parsed.meta = true;\n        else parsed.ctrl = true;\n        break;\n      case 'ctrl':\n      case 'control':\n        parsed.ctrl = true;\n        break;\n      case 'meta':\n      case 'cmd':\n      case 'command':\n        parsed.meta = true;\n        break;\n      case 'alt':\n      case 'option':\n        parsed.alt = true;\n        break;\n      case 'shift':\n        parsed.shift = true;\n        parsed.shiftNamed = true;\n        break;\n      default:\n        parsed.key = KEY_ALIASES[part] ?? part;\n    }\n  }\n\n  return parsed.key ? parsed : null;\n}\n\n/**\n * The key the event carries, plus the one its physical position would produce unshifted.\n *\n * Both are needed: Alt+K on a Mac arrives as `key: '˚'` and Shift+1 as `key: '!'`, so a combo\n * written against the letter or digit on the keycap only matches through `code`.\n */\nfunction pressedKeys(event: KeyboardEvent): string[] {\n  const keys = [event.key.toLowerCase()];\n  const physical = /^(?:Key|Digit)(.)$/.exec(event.code);\n  if (physical) keys.push(physical[1].toLowerCase());\n  return keys;\n}\n\nfunction matches(combo: Combo, event: KeyboardEvent): boolean {\n  if (combo.ctrl !== event.ctrlKey || combo.meta !== event.metaKey || combo.alt !== event.altKey) return false;\n  // Shift is only compared when the combo named it. A symbol is typed *with* Shift on most\n  // layouts, so requiring `shiftKey === false` would make `'?'` and `'mod+/'` unreachable.\n  if (combo.shiftNamed && combo.shift !== event.shiftKey) return false;\n  return pressedKeys(event).includes(combo.key);\n}\n\nfunction isInField(target: EventTarget | null): boolean {\n  return target instanceof Element && !!target.closest(FIELD_SELECTOR);\n}\n\n/** Keys a text field uses for itself, beyond the characters it inserts. */\nconst FIELD_KEYS = new Set([\n  'arrowup',\n  'arrowdown',\n  'arrowleft',\n  'arrowright',\n  'home',\n  'end',\n  'pageup',\n  'pagedown',\n  'backspace',\n  'delete',\n  'enter',\n  'tab',\n]);\n\n/**\n * Whether a combo would be taken out of a focused field's mouth.\n *\n * Ctrl, Cmd and Alt take a key out of the field's hands whatever it is. Without one, a single\n * character is typing and the listed keys are editing, but `F2` and `Escape` are neither, and\n * suppressing those made a menu's `F2` do nothing the moment the page had a focused input.\n */\nfunction isFieldKey(combo: Combo): boolean {\n  if (combo.ctrl || combo.meta || combo.alt) return false;\n  return combo.key.length === 1 || FIELD_KEYS.has(combo.key);\n}\n\n/** Keys whose name is not what a keycap says. Anything else is capitalised, so `f2` reads `F2`. */\nconst KEY_LABELS: Record<string, string> = {\n  ' ': 'Space',\n  arrowup: '↑',\n  arrowdown: '↓',\n  arrowleft: '←',\n  arrowright: '→',\n  escape: 'Esc',\n  enter: '↵',\n  backspace: '⌫',\n  delete: 'Del',\n  pageup: 'PgUp',\n  pagedown: 'PgDn',\n};\n\n/**\n * A combo written for the reader: `'mod+shift+k'` reads `⇧⌘K` on an Apple platform and\n * `Ctrl+Shift+K` everywhere else.\n *\n * So a shortcut is written once, in one spelling, and the hint beside it cannot contradict what\n * the binding actually listens for. Hand-written `'⌘K'` hints are wrong on every other platform,\n * silently, because nothing compares the label to the binding.\n *\n * Resolved from the platform at call time, so a server-rendered page renders the non-Apple form.\n */\nexport function formatHotkey(combo: string): string {\n  const parsed = parseCombo(combo);\n  if (!parsed) return combo;\n\n  const key = KEY_LABELS[parsed.key] ?? (parsed.key.length === 1 ? parsed.key.toUpperCase() : capitalise(parsed.key));\n\n  // Apple orders modifier symbols ⌃⌥⇧⌘ and runs them into the key; everywhere else they are\n  // words joined by `+`.\n  if (isApplePlatform()) {\n    const symbols = [parsed.ctrl && '⌃', parsed.alt && '⌥', parsed.shift && '⇧', parsed.meta && '⌘'];\n    return symbols.filter(Boolean).join('') + key;\n  }\n  const words = [parsed.ctrl && 'Ctrl', parsed.meta && 'Win', parsed.alt && 'Alt', parsed.shift && 'Shift'];\n  return [...words.filter(Boolean), key].join('+');\n}\n\nfunction capitalise(value: string): string {\n  return value[0].toUpperCase() + value.slice(1);\n}\n\n/** UI Events key names that are not the lowercase spelling capitalised. */\nconst ARIA_KEY_NAMES: Record<string, string> = {\n  // The attribute's syntax uses both characters as separators, so ARIA names them instead.\n  ' ': 'Space',\n  '+': 'Plus',\n  arrowup: 'ArrowUp',\n  arrowdown: 'ArrowDown',\n  arrowleft: 'ArrowLeft',\n  arrowright: 'ArrowRight',\n  pageup: 'PageUp',\n  pagedown: 'PageDown',\n};\n\n/**\n * A combo as `aria-keyshortcuts` spells it: `'mod+shift+k'` is `Control+Shift+K`, or\n * `Meta+Shift+K` on an Apple platform. Resolved from the platform like `formatHotkey`, so the two\n * always name the same keys.\n */\nexport function ariaKeyshortcuts(combo: string | undefined): string | undefined {\n  const parsed = combo ? parseCombo(combo) : null;\n  if (!parsed) return undefined;\n\n  const key = ARIA_KEY_NAMES[parsed.key] ?? capitalise(parsed.key);\n  const modifiers = [parsed.ctrl && 'Control', parsed.alt && 'Alt', parsed.shift && 'Shift', parsed.meta && 'Meta'];\n  return [...modifiers.filter(Boolean), key].join('+');\n}\n\n/**\n * Keyboard shortcuts, for a whole app or for one element.\n *\n * ```tsx\n * useHotkeys({ 'mod+k': () => setPaletteOpen(true), '?': () => setHelpOpen(true) });\n * ```\n *\n * The map is read through a ref at event time, so an inline object literal is fine and no handler\n * needs memoising. Shortcuts keep firing while a Modal or Drawer is open — pass `enabled` for the\n * ones that should not.\n */\nexport function useHotkeys(hotkeys: HotkeyMap, options: UseHotkeysOptions = {}): void {\n  const { enabled = true, target, enableInFields = false, preventDefault = true, repeat = false } = options;\n\n  const hotkeysRef = useRef(hotkeys);\n  useEffect(() => {\n    hotkeysRef.current = hotkeys;\n  });\n\n  useEffect(() => {\n    if (!enabled) return;\n    const node: HTMLElement | Window | null = target ? target.current : window;\n    if (!node) return;\n\n    function onKeyDown(event: Event) {\n      const keyboardEvent = event as KeyboardEvent;\n      // A key pressed to compose a character in an IME is not a shortcut.\n      if (keyboardEvent.isComposing) return;\n\n      for (const [spelling, handler] of Object.entries(hotkeysRef.current)) {\n        const combo = parseCombo(spelling);\n        if (!combo || !matches(combo, keyboardEvent)) continue;\n        if (isFieldKey(combo) && !enableInFields && isInField(keyboardEvent.target)) continue;\n        // Prevented on a skipped repeat too, or the second keydown of a held mod+s opens the\n        // browser's save dialog.\n        if (preventDefault) keyboardEvent.preventDefault();\n        if (keyboardEvent.repeat && !repeat) return;\n        handler(keyboardEvent);\n        return;\n      }\n    }\n\n    node.addEventListener('keydown', onKeyDown);\n    return () => node.removeEventListener('keydown', onKeyDown);\n  }, [enabled, target, enableInFields, preventDefault, repeat]);\n}\n\nexport default useHotkeys;\n"],"mappings":"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"}