From 39e862a48704d31c9c1ca932a789438e3c54ec31 Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Wed, 8 Oct 2025 16:26:06 -0500 Subject: [PATCH] feat: add transition status to React tooltip (#42) --- .../react/react/src/components/Tooltip.tsx | 47 ++++++++++--------- .../react/react/src/skins/default/styles.ts | 19 ++++---- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/packages/react/react/src/components/Tooltip.tsx b/packages/react/react/src/components/Tooltip.tsx index 6698a52f..effa40cc 100644 --- a/packages/react/react/src/components/Tooltip.tsx +++ b/packages/react/react/src/components/Tooltip.tsx @@ -16,6 +16,7 @@ import { useHover, useInteractions, useRole, + useTransitionStatus, } from '@floating-ui/react'; interface UpdatePositioningProps { @@ -25,15 +26,12 @@ interface UpdatePositioningProps { } interface TooltipContextType { - open: boolean; - setOpen: (open: boolean) => void; - refs: ReturnType['refs']; - floatingStyles: ReturnType['floatingStyles']; + arrowRef: React.MutableRefObject; + context: ReturnType['context']; + transitionStatus: ReturnType['status']; getReferenceProps: ReturnType['getReferenceProps']; getFloatingProps: ReturnType['getFloatingProps']; - context: ReturnType['context']; updatePositioning: (props: UpdatePositioningProps) => void; - arrowRef: React.MutableRefObject; } interface TooltipRootProps { @@ -100,7 +98,7 @@ function TooltipRoot({ delay = 600, closeDelay = 0, children }: TooltipRootProps const [collisionPadding, setCollisionPadding] = useState(0); const arrowRef = useRef(null); - const { refs, floatingStyles, context } = useFloating({ + const { context } = useFloating({ open, onOpenChange: setOpen, placement, @@ -115,6 +113,8 @@ function TooltipRoot({ delay = 600, closeDelay = 0, children }: TooltipRootProps whileElementsMounted: autoUpdate, }); + const { status: transitionStatus } = useTransitionStatus(context); + const hover = useHover(context, { delay: { open: delay, @@ -134,41 +134,37 @@ function TooltipRoot({ delay = 600, closeDelay = 0, children }: TooltipRootProps }; const value: TooltipContextType = { - open, - setOpen, - refs, - floatingStyles, getReferenceProps, getFloatingProps, context, updatePositioning, arrowRef, + transitionStatus, }; return {children}; } function TooltipTrigger({ children }: TooltipTriggerProps): JSX.Element { - const { refs, getReferenceProps } = useTooltipContext(); + const { context, getReferenceProps } = useTooltipContext(); + const { refs, open } = context; return React.cloneElement(React.Children.only(children) as JSX.Element, { ref: refs.setReference, ...getReferenceProps(), + 'data-popup-open': open ? '' : undefined, }); } function TooltipPositioner({ side = 'top', sideOffset = 0, collisionPadding = 0, children }: TooltipPositionerProps): JSX.Element | null { - const { open, refs, floatingStyles, updatePositioning } = useTooltipContext(); + const { context, updatePositioning } = useTooltipContext(); + const { refs, floatingStyles } = context; // Update positioning when props change useEffect(() => { updatePositioning({ side, sideOffset, collisionPadding }); }, [side, sideOffset, collisionPadding, updatePositioning]); - if (!open) { - return null; - } - const positionerContextValue: TooltipPositionerContextType = { side, }; @@ -182,9 +178,9 @@ function TooltipPositioner({ side = 'top', sideOffset = 0, collisionPadding = 0, ); } -function TooltipPopup({ className = '', children }: TooltipPopupProps): JSX.Element { - const { getFloatingProps } = useTooltipContext(); - const { refs } = useTooltipContext(); +function TooltipPopup({ className = '', children }: TooltipPopupProps): JSX.Element | null { + const { context, getFloatingProps, transitionStatus } = useTooltipContext(); + const { refs, placement } = context; const triggerElement = refs.reference.current as HTMLElement | null; // Copy data attributes from trigger element @@ -197,7 +193,16 @@ function TooltipPopup({ className = '', children }: TooltipPopupProps): JSX.Elem : {}; return ( -
+
{children}
); diff --git a/packages/react/react/src/skins/default/styles.ts b/packages/react/react/src/skins/default/styles.ts index a2fcce7f..07d6f125 100644 --- a/packages/react/react/src/skins/default/styles.ts +++ b/packages/react/react/src/skins/default/styles.ts @@ -70,18 +70,17 @@ const styles: MediaDefaultSkinStyles = { PlayIcon: cn('play-icon'), PauseIcon: cn('pause-icon'), TooltipPopup: cn( - 'whitespace-nowrap relative px-2 py-1 text-xs font-medium rounded-md text-white text-shadow', + 'whitespace-nowrap flex origin-[var(--transform-origin)] flex-col rounded-md text-white text-xs @7xl/root:text-sm px-2 py-1', // Background 'bg-white/10 backdrop-blur-3xl backdrop-saturate-150 backdrop-brightness-90', - // Shadow - // 'shadow-lg shadow-black/25', - // Border - 'border border-white/10', - // Reduced transparency for users with preference - // XXX: This requires a Tailwind custom variant (see 1 below) - 'reduced-transparency:bg-black/70 reduced-transparency:ring-black reduced-transparency:after:ring-white/20', - // High contrast mode - 'contrast-more:bg-black/90 contrast-more:ring-black contrast-more:after:ring-white/20' + // Animation + 'transition-[transform,scale,opacity] data-[ending-style]:scale-90 data-[ending-style]:opacity-0 data-[instant]:duration-0 data-[starting-style]:scale-90 data-[starting-style]:opacity-0', + // Ring + 'ring-1 ring-white/10 ring-inset', + // Text shadow + 'text-shadow shadow-black/10', + // Border to enhance contrast on lighter videos + 'after:absolute after:inset-0 after:ring after:rounded-[inherit] after:ring-black/15 after:pointer-events-none', ), PlayTooltipPopup: cn( '[&_.pause-tooltip]:inline [&[data-paused]_.pause-tooltip]:hidden',