diff --git a/apps/desktop/src/components/ui/tooltip.tsx b/apps/desktop/src/components/ui/tooltip.tsx index fe16e3b97cfc..226bd9472b3f 100644 --- a/apps/desktop/src/components/ui/tooltip.tsx +++ b/apps/desktop/src/components/ui/tooltip.tsx @@ -5,6 +5,10 @@ import { useI18n } from '@/i18n' import { useKeybindHint } from '@/lib/keybinds/use-keybind-hint' import { cn } from '@/lib/utils' +/** True inside `RootTooltipProvider`. `Tip` uses this to decide whether it + * needs to supply its own provider — see the note on `Tip`. */ +const HasTooltipProvider = React.createContext(false) + function TooltipProvider({ delayDuration = 0, // Tips are labels, not interactive surfaces. Hoverable content + Radix's @@ -105,21 +109,55 @@ interface TipProps extends Omit{children} } + const tip = ( + + {children} + {label} + + ) + + return provided ? tip : {tip} +} + +/** The app's single tooltip provider. Mounted once at the root so no `Tip` + * needs its own. Defaults match what `Tip` used to pass per instance. */ +function RootTooltipProvider({ children }: { children: React.ReactNode }) { return ( - - - {children} - {label} - - + + + {children} + + ) } @@ -163,4 +201,13 @@ function TipKeybindLabel({ actionId, text }: TipKeybindLabelProps) { return } -export { Tip, TipHintLabel, TipKeybindLabel, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } +export { + RootTooltipProvider, + Tip, + TipHintLabel, + TipKeybindLabel, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 969b5c510d93..3d1c164b6dea 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -17,6 +17,7 @@ import { HashRouter } from 'react-router-dom' import App from './app' import { ErrorBoundary } from './components/error-boundary' import { HapticsProvider } from './components/haptics-provider' +import { RootTooltipProvider } from './components/ui/tooltip' import { I18nProvider } from './i18n' import { installClipboardShim } from './lib/clipboard' import { queryClient } from './lib/query-client' @@ -46,6 +47,12 @@ if (winParam === 'overlay') { + {/* ONE tooltip provider for the whole app. Every `Tip` used to + carry its own, and with ~107 call sites those subtrees + dominated unrelated interactions (52,784 TooltipProvider + renders in a single sash drag). Radix's provider holds only + refs and stable callbacks, so hoisting is what it's for. */} + {/* useTransitions={false}: react-router v7's HashRouter wraps every route state update in React.startTransition() by default. In React 19's concurrent renderer, transitions are non-urgent — React @@ -58,6 +65,7 @@ if (winParam === 'overlay') { +