From bce45407443c0e89317cc81781fb1e4b98709efc Mon Sep 17 00:00:00 2001 From: Artem Lazarchuk Date: Sun, 26 Apr 2026 15:46:48 +0300 Subject: [PATCH] feat: Enhance Tooltip component with controlled state management and touch support --- src/frontend/src/components/ui/tooltip.tsx | 60 +++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/ui/tooltip.tsx b/src/frontend/src/components/ui/tooltip.tsx index dad190e2a9..59974d47b0 100644 --- a/src/frontend/src/components/ui/tooltip.tsx +++ b/src/frontend/src/components/ui/tooltip.tsx @@ -10,9 +10,65 @@ const TooltipProvider = ({ ); -const Tooltip = TooltipPrimitive.Root; +type TooltipControl = { + open: boolean; + setOpen: (next: boolean) => void; +}; + +const TooltipContext = React.createContext(null); + +/** + * Radix Tooltip is hover/focus driven, so coarse pointers (finger / stylus tap) + * never see it on iPad/iPhone. Wrap Root with a small controlled-state shim and + * let the Trigger toggle it on touch / pen pointerdown without affecting mouse. + */ +const Tooltip = ({ + open: openProp, + defaultOpen, + onOpenChange, + ...props +}: React.ComponentPropsWithoutRef) => { + const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false); + const isControlled = openProp !== undefined; + const open = isControlled ? openProp : internalOpen; + + const setOpen = React.useCallback( + (next: boolean) => { + if (!isControlled) { + setInternalOpen(next); + } + onOpenChange?.(next); + }, + [isControlled, onOpenChange], + ); -const TooltipTrigger = TooltipPrimitive.Trigger; + return ( + + + + ); +}; + +const TooltipTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => { + const ctx = React.useContext(TooltipContext); + return ( + { + props.onPointerDown?.(e); + if (!ctx) return; + if (e.pointerType === "touch" || e.pointerType === "pen") { + ctx.setOpen(!ctx.open); + } + }} + /> + ); +}); +TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName; const hasNoSpaces = (str: string): boolean => { const trimmed = str.trim();