ShortcutTooltip.tsx 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import { TooltipContentProps } from '@ui/components/shadcn/ui/tooltip'
  2. import { Fragment, type ReactNode } from 'react'
  3. import { KeyboardShortcut, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  4. import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
  5. import { SHORTCUT_DEFINITIONS, type ShortcutId } from '@/state/shortcuts/registry'
  6. interface ShortcutTooltipProps {
  7. shortcutId: ShortcutId
  8. children: ReactNode
  9. side?: TooltipContentProps['side']
  10. align?: TooltipContentProps['align']
  11. sideOffset?: number
  12. delayDuration?: number
  13. /**
  14. * Override the label from the registry. Use when the wrapped element's
  15. * action is a narrower/contextual variant of the registered shortcut.
  16. */
  17. label?: string
  18. /**
  19. * Controlled open state for the tooltip. Pass `false` to force the tooltip
  20. * closed (e.g. while a popover or dialog opened by the wrapped element is
  21. * visible). Leave `undefined` for default uncontrolled behavior.
  22. */
  23. open?: boolean
  24. }
  25. /**
  26. * Wraps any element to show its bound keyboard shortcut on hover/focus, in the
  27. * style of Linear's shortcut tooltips: `"<label> <key> [then <key>]"`.
  28. *
  29. * Uses Radix's `asChild` trigger, so the wrapped element remains fully
  30. * interactive — clicks, focus, and event handlers pass through untouched.
  31. *
  32. * @example
  33. * <ShortcutTooltip shortcutId={SHORTCUT_IDS.RESULTS_COPY_MARKDOWN}>
  34. * <Button onClick={handleCopy}>Copy</Button>
  35. * </ShortcutTooltip>
  36. */
  37. export const ShortcutTooltip = ({
  38. shortcutId,
  39. children,
  40. side,
  41. align,
  42. sideOffset,
  43. delayDuration,
  44. label: labelOverride,
  45. open,
  46. }: ShortcutTooltipProps) => {
  47. const def = SHORTCUT_DEFINITIONS[shortcutId]
  48. const label = labelOverride ?? def.label
  49. return (
  50. <Tooltip delayDuration={delayDuration} open={open}>
  51. <TooltipTrigger asChild>{children}</TooltipTrigger>
  52. <TooltipContent
  53. side={side}
  54. align={align}
  55. sideOffset={sideOffset}
  56. className="flex items-center gap-2"
  57. >
  58. <span>{label}</span>
  59. <span className="flex items-center gap-1">
  60. {def.sequence.map((step, i) => (
  61. <Fragment key={i}>
  62. {i > 0 && <span className="text-foreground-lighter text-[11px]">then</span>}
  63. <KeyboardShortcut keys={hotkeyToKeys(step)} />
  64. </Fragment>
  65. ))}
  66. </span>
  67. </TooltipContent>
  68. </Tooltip>
  69. )
  70. }