import { useEffect, useRef, useState, useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import Overlay from 'react-overlays/Overlay'; import type { OffsetValue, UsePopperOptions, } from 'react-overlays/esm/usePopper'; import { useTimeout } from 'mastodon/../hooks/useTimeout'; import { HoverCardAccount } from 'mastodon/components/hover_card_account'; const offset = [-12, 4] as OffsetValue; const enterDelay = 750; const leaveDelay = 150; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; const isHoverCardAnchor = (element: HTMLElement) => element.matches('[data-hover-card-account]'); export const HoverCardController: React.FC = () => { const [open, setOpen] = useState(false); const [accountId, setAccountId] = useState(); const [anchor, setAnchor] = useState(null); const cardRef = useRef(null); const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); const [setScrollTimeout] = useTimeout(); const location = useLocation(); const handleClose = useCallback(() => { cancelEnterTimeout(); cancelLeaveTimeout(); setOpen(false); setAnchor(null); }, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]); useEffect(() => { handleClose(); }, [handleClose, location]); useEffect(() => { let isScrolling = false; let currentAnchor: HTMLElement | null = null; let currentTitle: string | null = null; const open = (target: HTMLElement) => { target.setAttribute('aria-describedby', 'hover-card'); setOpen(true); setAnchor(target); setAccountId(target.getAttribute('data-hover-card-account') ?? undefined); }; const close = () => { currentAnchor?.removeAttribute('aria-describedby'); currentAnchor = null; setOpen(false); setAnchor(null); setAccountId(undefined); }; const handleMouseEnter = (e: MouseEvent) => { const { target } = e; // We've exited the window if (!(target instanceof HTMLElement)) { close(); return; } // We've entered an anchor if (!isScrolling && isHoverCardAnchor(target)) { cancelLeaveTimeout(); currentAnchor?.removeAttribute('aria-describedby'); currentAnchor = target; currentTitle = target.getAttribute('title'); target.removeAttribute('title'); setEnterTimeout(() => { open(target); }, enterDelay); } // We've entered the hover card if ( !isScrolling && (target === currentAnchor || target === cardRef.current) ) { cancelLeaveTimeout(); } }; const handleMouseLeave = (e: MouseEvent) => { const { target } = e; if (!currentAnchor) { return; } if ( currentTitle && target instanceof HTMLElement && target === currentAnchor ) target.setAttribute('title', currentTitle); if (target === currentAnchor || target === cardRef.current) { cancelEnterTimeout(); setLeaveTimeout(() => { close(); }, leaveDelay); } }; const handleScrollEnd = () => { isScrolling = false; }; const handleScroll = () => { isScrolling = true; cancelEnterTimeout(); setScrollTimeout(handleScrollEnd, 100); }; const handleMouseMove = () => { delayEnterTimeout(enterDelay); }; document.body.addEventListener('mouseenter', handleMouseEnter, { passive: true, capture: true, }); document.body.addEventListener('mousemove', handleMouseMove, { passive: true, capture: false, }); document.body.addEventListener('mouseleave', handleMouseLeave, { passive: true, capture: true, }); document.addEventListener('scroll', handleScroll, { passive: true, capture: true, }); return () => { document.body.removeEventListener('mouseenter', handleMouseEnter); document.body.removeEventListener('mousemove', handleMouseMove); document.body.removeEventListener('mouseleave', handleMouseLeave); document.removeEventListener('scroll', handleScroll); }; }, [ setEnterTimeout, setLeaveTimeout, setScrollTimeout, cancelEnterTimeout, cancelLeaveTimeout, delayEnterTimeout, setOpen, setAccountId, setAnchor, ]); return ( {({ props }) => (
)}
); };