client/src/components/tooltip/component.tsx
import React, { useRef, useState } from 'react';
import {
offset,
useFloating,
arrow as arrowMiddleware,
flip,
useInteractions,
useHover,
useClick,
safePolygon,
useDismiss,
autoUpdate,
FloatingPortal,
} from '@floating-ui/react';
import { shift } from '@floating-ui/core';
import classNames from 'classnames';
import type { Placement } from '@floating-ui/core';
interface TooltipProps {
className?: string;
content: React.ReactNode;
arrow?: boolean;
theme?: 'light' | 'dark';
placement?: Placement;
hoverTrigger?: boolean;
enabled?: boolean;
}
const THEME: Record<Required<TooltipProps>['theme'], string> = {
light: 'bg-white',
dark: 'bg-gray-900',
};
const ARROW_POSITION_CLASSES = {
top: 'translate-y-1/2 bottom-0 rounded-r-sm',
right: '-translate-x-1/2 left-0 rounded-b-sm',
bottom: '-translate-y-1/2 top-0 rounded-l-sm',
left: 'translate-x-1/2 right-0 rounded-t-sm',
};
export const ToolTip: React.FC<React.PropsWithChildren<TooltipProps>> = ({
className,
children,
content,
arrow = true,
theme = 'light',
placement = 'top',
hoverTrigger = false,
enabled = true,
}) => {
const arrowRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const {
x,
y,
refs,
strategy,
middlewareData: { arrow: { x: arrowX, y: arrowY } = {} },
placement: floatingPlacement,
context,
} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement,
whileElementsMounted: autoUpdate,
strategy: 'fixed',
middleware: [
offset({ mainAxis: arrow ? 10 : 5 }),
flip(),
shift({ padding: 4 }),
arrowMiddleware({ element: arrowRef, padding: 5 }),
],
});
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, {
enabled: hoverTrigger && enabled,
handleClose: safePolygon({ buffer: 50 }),
}),
useClick(context, { enabled: !hoverTrigger && enabled, toggle: true }),
useDismiss(context),
]);
return (
<>
<button
type="button"
{...getReferenceProps({
ref: refs.setReference,
className: 'relative',
onClick: (e) => e.stopPropagation(),
})}
>
{children}
</button>
<FloatingPortal>
{isOpen && enabled && (
<div
{...getFloatingProps({
className: classNames(className, 'drop-shadow-md w-fit z-50'),
ref: refs.setFloating,
style: {
position: strategy,
top: y ?? '',
left: x ?? '',
},
})}
>
{content}
<div
ref={arrowRef}
style={{
top: arrowY ?? '',
left: arrowX ?? '',
}}
className={classNames(
'absolute -z-10',
{ hidden: !arrow },
ARROW_POSITION_CLASSES[floatingPlacement],
'h-3 w-3 rotate-45',
THEME[theme],
)}
/>
</div>
)}
</FloatingPortal>
</>
);
};
export default ToolTip;