Shortcut.tsx 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. import { TooltipContentProps } from '@ui/components/shadcn/ui/tooltip'
  2. import type { ReactNode } from 'react'
  3. import { ShortcutTooltip } from './ShortcutTooltip'
  4. import type { ShortcutId } from '@/state/shortcuts/registry'
  5. import type { ShortcutOptions } from '@/state/shortcuts/types'
  6. import { useShortcut } from '@/state/shortcuts/useShortcut'
  7. interface ShortcutProps {
  8. /** Registered shortcut id — drives both the hotkey binding and the tooltip. */
  9. id: ShortcutId
  10. /** Fires on the hotkey. Usually the same handler wired to the child's `onClick`. */
  11. onTrigger: () => void
  12. /** Element to bind the shortcut to and wrap in the tooltip. */
  13. children: ReactNode
  14. /** Per-mount overrides for the shortcut — see `ShortcutOptions`. */
  15. options?: ShortcutOptions
  16. side?: TooltipContentProps['side']
  17. align?: TooltipContentProps['align']
  18. sideOffset?: number
  19. delayDuration?: number
  20. /**
  21. * Override the label from the registry. Use when the wrapped element's
  22. * action is a narrower/contextual variant of the registered shortcut.
  23. */
  24. label?: string
  25. /**
  26. * Controlled open state for the tooltip. Pass `false` to force the tooltip
  27. * closed (e.g. while a popover or dialog opened by the wrapped element is
  28. * visible). Leave `undefined` for default uncontrolled behavior.
  29. */
  30. tooltipOpen?: boolean
  31. }
  32. /**
  33. * Bind a registered shortcut to an element AND show its keybind on hover,
  34. * Linear-style. Single source of truth: one `id` drives both the hotkey
  35. * listener and the tooltip, so they can't drift.
  36. *
  37. * The wrapped child stays fully interactive — Radix `asChild` passes clicks,
  38. * focus, and refs through untouched.
  39. *
  40. * @example
  41. * <Shortcut id={SHORTCUT_IDS.RESULTS_COPY_MARKDOWN} onTrigger={handleCopy}>
  42. * <Button onClick={handleCopy}>Copy</Button>
  43. * </Shortcut>
  44. *
  45. * @example
  46. * // Gate the hotkey on local state; tooltip still renders:
  47. * <Shortcut
  48. * id={SHORTCUT_IDS.ACTION_BAR_SAVE}
  49. * onTrigger={handleSave}
  50. * options={{ enabled: hasUnsavedChanges }}
  51. * >
  52. * <Button onClick={handleSave} disabled={!hasUnsavedChanges}>Save</Button>
  53. * </Shortcut>
  54. */
  55. export const Shortcut = ({
  56. id,
  57. onTrigger,
  58. children,
  59. options,
  60. side,
  61. align,
  62. sideOffset,
  63. delayDuration,
  64. label,
  65. tooltipOpen,
  66. }: ShortcutProps) => {
  67. useShortcut(id, onTrigger, label !== undefined ? { ...options, label } : options)
  68. return (
  69. <ShortcutTooltip
  70. shortcutId={id}
  71. side={side}
  72. align={align}
  73. sideOffset={sideOffset}
  74. delayDuration={delayDuration}
  75. label={label}
  76. open={tooltipOpen}
  77. >
  78. {children}
  79. </ShortcutTooltip>
  80. )
  81. }