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