94 lines
2.3 KiB
TypeScript
94 lines
2.3 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useState } from 'react';
|
|
import {
|
|
useFloating,
|
|
autoUpdate,
|
|
offset,
|
|
flip,
|
|
shift,
|
|
arrow,
|
|
useHover,
|
|
useFocus,
|
|
useClick,
|
|
useDismiss,
|
|
useRole,
|
|
useInteractions,
|
|
FloatingPortal,
|
|
FloatingArrow,
|
|
} from '@floating-ui/react';
|
|
import styles from './InfoPopover.module.css';
|
|
|
|
export interface InfoPopoverProps {
|
|
label?: string;
|
|
plain?: string;
|
|
detail?: string;
|
|
ariaLabel?: string;
|
|
}
|
|
|
|
export function InfoPopover({ label, plain, detail, ariaLabel }: InfoPopoverProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const arrowRef = useRef<SVGSVGElement>(null);
|
|
|
|
const { refs, floatingStyles, context } = useFloating({
|
|
open,
|
|
onOpenChange: setOpen,
|
|
placement: 'top',
|
|
whileElementsMounted: autoUpdate,
|
|
middleware: [
|
|
offset(8),
|
|
flip({ fallbackAxisSideDirection: 'start' }),
|
|
shift({ padding: 8 }),
|
|
arrow({ element: arrowRef, padding: 8 }),
|
|
],
|
|
});
|
|
|
|
// Hover (desktop) with a short open delay, keyboard focus, tap (touch),
|
|
// outside-press + Escape to dismiss. Floating UI disables hover on touch,
|
|
// so tap and hover never double-fire.
|
|
const hover = useHover(context, { delay: { open: 100, close: 0 } });
|
|
const focus = useFocus(context);
|
|
const click = useClick(context);
|
|
const dismiss = useDismiss(context);
|
|
const role = useRole(context, { role: 'tooltip' });
|
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
hover,
|
|
focus,
|
|
click,
|
|
dismiss,
|
|
role,
|
|
]);
|
|
|
|
if (!plain) return null;
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
ref={refs.setReference}
|
|
className={styles.icon}
|
|
aria-label={ariaLabel ?? 'More information'}
|
|
aria-expanded={open}
|
|
{...getReferenceProps()}
|
|
>
|
|
?
|
|
</button>
|
|
{open && (
|
|
<FloatingPortal>
|
|
<div
|
|
ref={refs.setFloating}
|
|
className={styles.tooltip}
|
|
style={floatingStyles}
|
|
{...getFloatingProps()}
|
|
>
|
|
<FloatingArrow ref={arrowRef} context={context} className={styles.arrow} />
|
|
{label && <span className={styles.label}>{label}</span>}
|
|
<span className={styles.plain}>{plain}</span>
|
|
{detail && <span className={styles.detail}>{detail}</span>}
|
|
</div>
|
|
</FloatingPortal>
|
|
)}
|
|
</>
|
|
);
|
|
}
|