feat: add transition status to React tooltip (#42)

This commit is contained in:
Wesley Luyten
2025-10-08 16:26:06 -05:00
committed by GitHub
parent 1bb8166704
commit 39e862a487
2 changed files with 35 additions and 31 deletions
+26 -21
View File
@@ -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<typeof useFloating>['refs'];
floatingStyles: ReturnType<typeof useFloating>['floatingStyles'];
arrowRef: React.MutableRefObject<HTMLElement | null>;
context: ReturnType<typeof useFloating>['context'];
transitionStatus: ReturnType<typeof useTransitionStatus>['status'];
getReferenceProps: ReturnType<typeof useInteractions>['getReferenceProps'];
getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
context: ReturnType<typeof useFloating>['context'];
updatePositioning: (props: UpdatePositioningProps) => void;
arrowRef: React.MutableRefObject<HTMLElement | null>;
}
interface TooltipRootProps {
@@ -100,7 +98,7 @@ function TooltipRoot({ delay = 600, closeDelay = 0, children }: TooltipRootProps
const [collisionPadding, setCollisionPadding] = useState(0);
const arrowRef = useRef<HTMLElement | null>(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 <TooltipContext.Provider value={value}>{children}</TooltipContext.Provider>;
}
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 (
<div className={className} {...getFloatingProps()} {...dataAttributes}>
<div
className={className}
{...getFloatingProps()}
{...dataAttributes}
data-side={placement}
data-starting-style={transitionStatus === 'initial' ? '' : undefined}
data-open={transitionStatus === 'initial' || transitionStatus === 'open' ? '' : undefined}
data-ending-style={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
data-closed={transitionStatus === 'close' || transitionStatus === 'unmounted' ? '' : undefined}
>
{children}
</div>
);
@@ -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',