mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-29 05:31:51 +02:00
Add redirect drop metadata and warning UI (#418)
This commit is contained in:
@@ -20,8 +20,15 @@ const hiddenStyles: CSSProperties = {
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
type TooltipPosition = 'top' | 'bottom';
|
||||
|
||||
interface TooltipOpenState {
|
||||
styles: CSSProperties;
|
||||
position: TooltipPosition;
|
||||
}
|
||||
|
||||
export function Tooltip({ children, className, content, tabIndex, size = 'md' }: TooltipProps) {
|
||||
const [isOpen, setIsOpen] = useState<CSSProperties>();
|
||||
const [openState, setOpenState] = useState<TooltipOpenState | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const showTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
@@ -29,16 +36,25 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
const handleOpenImmediate = () => {
|
||||
if (triggerRef.current == null || tooltipRef.current == null) return;
|
||||
clearTimeout(showTimeout.current);
|
||||
setIsOpen(undefined);
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const viewportHeight = document.documentElement.clientHeight;
|
||||
|
||||
const margin = 8;
|
||||
const spaceAbove = Math.max(0, triggerRect.top - margin);
|
||||
const spaceBelow = Math.max(0, viewportHeight - triggerRect.bottom - margin);
|
||||
const preferBottom = spaceAbove < tooltipRect.height + margin && spaceBelow > spaceAbove;
|
||||
const position: TooltipPosition = preferBottom ? 'bottom' : 'top';
|
||||
|
||||
const styles: CSSProperties = {
|
||||
bottom: docRect.height - triggerRect.top,
|
||||
left: Math.max(0, triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2),
|
||||
maxHeight: triggerRect.top,
|
||||
maxHeight: position === 'top' ? spaceAbove : spaceBelow,
|
||||
...(position === 'top'
|
||||
? { bottom: viewportHeight - triggerRect.top }
|
||||
: { top: triggerRect.bottom }),
|
||||
};
|
||||
setIsOpen(styles);
|
||||
|
||||
setOpenState({ styles, position });
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
@@ -48,16 +64,16 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
|
||||
const handleClose = () => {
|
||||
clearTimeout(showTimeout.current);
|
||||
setIsOpen(undefined);
|
||||
setOpenState(null);
|
||||
};
|
||||
|
||||
const handleToggleImmediate = () => {
|
||||
if (isOpen) handleClose();
|
||||
if (openState) handleClose();
|
||||
else handleOpenImmediate();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (isOpen && e.key === 'Escape') {
|
||||
if (openState && e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
@@ -71,10 +87,10 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
<Portal name="tooltip">
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={isOpen ?? hiddenStyles}
|
||||
style={openState?.styles ?? hiddenStyles}
|
||||
id={id.current}
|
||||
role="tooltip"
|
||||
aria-hidden={!isOpen}
|
||||
aria-hidden={openState == null}
|
||||
onMouseEnter={handleOpenImmediate}
|
||||
onMouseLeave={handleClose}
|
||||
className="p-2 fixed z-50 text-sm transition-opacity grid grid-rows-[minmax(0,1fr)]"
|
||||
@@ -88,14 +104,17 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
<Triangle className="text-border mb-2" />
|
||||
<Triangle
|
||||
className="text-border"
|
||||
position={openState?.position === 'bottom' ? 'top' : 'bottom'}
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
{/** biome-ignore lint/a11y/useSemanticElements: Needs to be usable in other buttons */}
|
||||
<span
|
||||
ref={triggerRef}
|
||||
role="button"
|
||||
aria-describedby={isOpen ? id.current : undefined}
|
||||
aria-describedby={openState ? id.current : undefined}
|
||||
tabIndex={tabIndex ?? -1}
|
||||
className={classNames(className, 'flex-grow-0 flex items-center')}
|
||||
onClick={handleToggleImmediate}
|
||||
@@ -111,7 +130,9 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
);
|
||||
}
|
||||
|
||||
function Triangle({ className }: { className?: string }) {
|
||||
function Triangle({ className, position }: { className?: string; position: 'top' | 'bottom' }) {
|
||||
const isBottom = position === 'bottom';
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
@@ -120,15 +141,19 @@ function Triangle({ className }: { className?: string }) {
|
||||
shapeRendering="crispEdges"
|
||||
className={classNames(
|
||||
className,
|
||||
'absolute z-50 border-t-[2px] border-surface-highlight',
|
||||
'-bottom-[calc(0.5rem-3px)] left-[calc(50%-0.4rem)]',
|
||||
'h-[0.5rem] w-[0.8rem]',
|
||||
'absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]',
|
||||
isBottom
|
||||
? 'border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2'
|
||||
: 'border-b-[2px] border-surface-highlight -top-[calc(0.5rem-3px)] mt-2',
|
||||
)}
|
||||
>
|
||||
<title>Triangle</title>
|
||||
<polygon className="fill-surface-highlight" points="0,0 30,0 15,10" />
|
||||
<polygon
|
||||
className="fill-surface-highlight"
|
||||
points={isBottom ? '0,0 30,0 15,10' : '0,10 30,10 15,0'}
|
||||
/>
|
||||
<path
|
||||
d="M0 0 L15 9 L30 0"
|
||||
d={isBottom ? 'M0 0 L15 9 L30 0' : 'M0 10 L15 1 L30 10'}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
|
||||
Reference in New Issue
Block a user