mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
108 lines
4.3 KiB
TypeScript
108 lines
4.3 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import type {
|
|
ButtonProps as AriaButtonProps,
|
|
TooltipProps as AriaTooltipProps,
|
|
TooltipTriggerComponentProps as AriaTooltipTriggerComponentProps,
|
|
} from "react-aria-components";
|
|
import { Button as AriaButton, OverlayArrow as AriaOverlayArrow, Tooltip as AriaTooltip, TooltipTrigger as AriaTooltipTrigger } from "react-aria-components";
|
|
import { cx } from "@/utils/cx";
|
|
|
|
interface TooltipProps extends AriaTooltipTriggerComponentProps, Omit<AriaTooltipProps, "children"> {
|
|
/**
|
|
* The title of the tooltip.
|
|
*/
|
|
title: ReactNode;
|
|
/**
|
|
* The description of the tooltip.
|
|
*/
|
|
description?: ReactNode;
|
|
/**
|
|
* Whether to show the arrow on the tooltip.
|
|
*
|
|
* @default false
|
|
*/
|
|
arrow?: boolean;
|
|
/**
|
|
* Delay in milliseconds before the tooltip is shown.
|
|
*
|
|
* @default 300
|
|
*/
|
|
delay?: number;
|
|
}
|
|
|
|
export const Tooltip = ({
|
|
title,
|
|
description,
|
|
children,
|
|
arrow = false,
|
|
delay = 300,
|
|
closeDelay = 0,
|
|
trigger,
|
|
isDisabled,
|
|
isOpen,
|
|
defaultOpen,
|
|
offset = 6,
|
|
crossOffset,
|
|
placement = "top",
|
|
onOpenChange,
|
|
...tooltipProps
|
|
}: TooltipProps) => {
|
|
const isTopOrBottomLeft = ["top left", "top end", "bottom left", "bottom end"].includes(placement);
|
|
const isTopOrBottomRight = ["top right", "top start", "bottom right", "bottom start"].includes(placement);
|
|
// Set negative cross offset for left and right placement to visually balance the tooltip.
|
|
const calculatedCrossOffset = isTopOrBottomLeft ? -12 : isTopOrBottomRight ? 12 : 0;
|
|
|
|
return (
|
|
<AriaTooltipTrigger {...{ trigger, delay, closeDelay, isDisabled, isOpen, defaultOpen, onOpenChange }}>
|
|
{children}
|
|
|
|
<AriaTooltip
|
|
{...tooltipProps}
|
|
offset={offset}
|
|
placement={placement}
|
|
crossOffset={crossOffset ?? calculatedCrossOffset}
|
|
className={({ isEntering, isExiting }) => cx(isEntering && "ease-out animate-in", isExiting && "ease-in animate-out")}
|
|
>
|
|
{({ isEntering, isExiting }) => (
|
|
<div
|
|
className={cx(
|
|
"z-50 flex max-w-xs origin-(--trigger-anchor-point) flex-col items-start gap-1 rounded-lg bg-primary-solid px-3 shadow-lg will-change-transform",
|
|
description ? "py-3" : "py-2",
|
|
|
|
isEntering &&
|
|
"ease-out animate-in fade-in zoom-in-95 in-placement-left:slide-in-from-right-0.5 in-placement-right:slide-in-from-left-0.5 in-placement-top:slide-in-from-bottom-0.5 in-placement-bottom:slide-in-from-top-0.5",
|
|
isExiting &&
|
|
"ease-in animate-out fade-out zoom-out-95 in-placement-left:slide-out-to-right-0.5 in-placement-right:slide-out-to-left-0.5 in-placement-top:slide-out-to-bottom-0.5 in-placement-bottom:slide-out-to-top-0.5",
|
|
)}
|
|
>
|
|
<span className="text-xs font-semibold text-white">{title}</span>
|
|
|
|
{description && <span className="text-xs font-medium text-tooltip-supporting-text">{description}</span>}
|
|
|
|
{arrow && (
|
|
<AriaOverlayArrow>
|
|
<svg
|
|
viewBox="0 0 100 100"
|
|
className="size-2.5 fill-bg-primary-solid in-placement-left:-rotate-90 in-placement-right:rotate-90 in-placement-top:rotate-0 in-placement-bottom:rotate-180"
|
|
>
|
|
<path d="M0,0 L35.858,35.858 Q50,50 64.142,35.858 L100,0 Z" />
|
|
</svg>
|
|
</AriaOverlayArrow>
|
|
)}
|
|
</div>
|
|
)}
|
|
</AriaTooltip>
|
|
</AriaTooltipTrigger>
|
|
);
|
|
};
|
|
|
|
interface TooltipTriggerProps extends AriaButtonProps {}
|
|
|
|
export const TooltipTrigger = ({ children, className, ...buttonProps }: TooltipTriggerProps) => {
|
|
return (
|
|
<AriaButton {...buttonProps} className={(values) => cx("h-max w-max outline-hidden", typeof className === "function" ? className(values) : className)}>
|
|
{children}
|
|
</AriaButton>
|
|
);
|
|
};
|