import { type CSSProperties, type MouseEvent, type PointerEvent, useEffect, useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; import { BRAND_ASSETS } from '../../uiAssets'; import { clampFloatingFeedbackPosition, FLOATING_FEEDBACK_DESKTOP_QUERY, FLOATING_FEEDBACK_DRAG_CLICK_SUPPRESS_MS, FLOATING_FEEDBACK_DRAG_CLICK_THRESHOLD, FLOATING_FEEDBACK_FORM_URL, FLOATING_FEEDBACK_INITIAL_LEFT, FLOATING_FEEDBACK_MARGIN, type FloatingFeedbackPosition, resolveFloatingFeedbackContactPanelOffset, resolveInitialFloatingFeedbackPosition, } from './floatingFeedbackEntryModel'; type FloatingFeedbackDragState = { pointerId: number; source: 'mouse' | 'pointer'; startClientX: number; startClientY: number; startLeft: number; startTop: number; hasMoved: boolean; }; function isSameFloatingFeedbackPointer( currentPointerId: number, nextPointerId: number, ) { if (!Number.isFinite(currentPointerId) || !Number.isFinite(nextPointerId)) { return true; } return currentPointerId === nextPointerId; } function shouldUseMouseDragFallback() { return ( typeof window !== 'undefined' && typeof window.PointerEvent !== 'function' ); } function getViewportSize() { return { width: window.innerWidth || document.documentElement.clientWidth, height: window.innerHeight || document.documentElement.clientHeight, }; } function getInitialDesktopVisibility() { if ( typeof window === 'undefined' || typeof window.matchMedia !== 'function' ) { return false; } return window.matchMedia(FLOATING_FEEDBACK_DESKTOP_QUERY).matches; } export function FloatingFeedbackEntry() { const [isDesktopVisible, setIsDesktopVisible] = useState( getInitialDesktopVisibility, ); const [isPointerOverEntry, setIsPointerOverEntry] = useState(false); const [isDragging, setIsDragging] = useState(false); const [position, setPosition] = useState(() => typeof window === 'undefined' ? { left: FLOATING_FEEDBACK_INITIAL_LEFT, top: FLOATING_FEEDBACK_MARGIN } : resolveInitialFloatingFeedbackPosition(getViewportSize()), ); const dragStateRef = useRef(null); const suppressClickUntilRef = useRef(0); useEffect(() => { if ( typeof window === 'undefined' || typeof window.matchMedia !== 'function' ) { return; } const mediaQuery = window.matchMedia(FLOATING_FEEDBACK_DESKTOP_QUERY); const updateVisibility = (event?: MediaQueryListEvent) => { setIsDesktopVisible(event?.matches ?? mediaQuery.matches); }; updateVisibility(); if (typeof mediaQuery.addEventListener === 'function') { mediaQuery.addEventListener('change', updateVisibility); return () => mediaQuery.removeEventListener('change', updateVisibility); } mediaQuery.addListener(updateVisibility); return () => mediaQuery.removeListener(updateVisibility); }, []); useEffect(() => { const clampToViewport = () => { setPosition((current) => clampFloatingFeedbackPosition(current, getViewportSize()), ); }; window.addEventListener('resize', clampToViewport); return () => window.removeEventListener('resize', clampToViewport); }, []); const updateDrag = ( pointerId: number, source: FloatingFeedbackDragState['source'], clientX: number, clientY: number, ) => { const dragState = dragStateRef.current; if ( !dragState || dragState.source !== source || !isSameFloatingFeedbackPointer(dragState.pointerId, pointerId) ) { return; } const deltaX = clientX - dragState.startClientX; const deltaY = clientY - dragState.startClientY; if ( !dragState.hasMoved && Math.hypot(deltaX, deltaY) >= FLOATING_FEEDBACK_DRAG_CLICK_THRESHOLD ) { dragState.hasMoved = true; } setPosition( clampFloatingFeedbackPosition( { left: dragState.startLeft + deltaX, top: dragState.startTop + deltaY, }, getViewportSize(), ), ); }; const stopDragById = ( pointerId: number, source: FloatingFeedbackDragState['source'], ) => { const dragState = dragStateRef.current; if ( !dragState || dragState.source !== source || !isSameFloatingFeedbackPointer(dragState.pointerId, pointerId) ) { return; } suppressClickUntilRef.current = dragState.hasMoved ? Date.now() + FLOATING_FEEDBACK_DRAG_CLICK_SUPPRESS_MS : 0; dragStateRef.current = null; setIsDragging(false); }; useEffect(() => { const handlePointerMove = (event: globalThis.PointerEvent) => { const dragState = dragStateRef.current; if ( !dragState || !isSameFloatingFeedbackPointer(dragState.pointerId, event.pointerId) ) { return; } updateDrag(event.pointerId, 'pointer', event.clientX, event.clientY); }; const stopPointerDrag = (event: globalThis.PointerEvent) => { stopDragById(event.pointerId, 'pointer'); }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', stopPointerDrag); window.addEventListener('pointercancel', stopPointerDrag); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', stopPointerDrag); window.removeEventListener('pointercancel', stopPointerDrag); }; }, []); useEffect(() => { const handleMouseMove = (event: globalThis.MouseEvent) => { const dragState = dragStateRef.current; if (!dragState || dragState.source !== 'mouse') { return; } updateDrag(Number.NaN, 'mouse', event.clientX, event.clientY); }; const stopMouseDrag = () => { stopDragById(Number.NaN, 'mouse'); }; window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', stopMouseDrag); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', stopMouseDrag); }; }, []); const startPointerDrag = (event: PointerEvent) => { if (event.button > 0) { return; } event.currentTarget.setPointerCapture?.(event.pointerId); setIsDragging(true); dragStateRef.current = { pointerId: event.pointerId, source: 'pointer', startClientX: event.clientX, startClientY: event.clientY, startLeft: position.left, startTop: position.top, hasMoved: false, }; }; const startMouseDrag = (event: MouseEvent) => { if (!shouldUseMouseDragFallback() || event.button > 0) { return; } setIsDragging(true); dragStateRef.current = { pointerId: Number.NaN, source: 'mouse', startClientX: event.clientX, startClientY: event.clientY, startLeft: position.left, startTop: position.top, hasMoved: false, }; }; const openFeedbackForm = () => { if (suppressClickUntilRef.current >= Date.now()) { suppressClickUntilRef.current = 0; dragStateRef.current = null; return; } window.open(FLOATING_FEEDBACK_FORM_URL, '_blank', 'noopener,noreferrer'); }; if (!isDesktopVisible || typeof document === 'undefined') { return null; } const shouldShowContactPanel = isPointerOverEntry && !isDragging; const contactPanelOffset = resolveFloatingFeedbackContactPanelOffset( position, getViewportSize(), ); return createPortal(
{shouldShowContactPanel ? ( ) : null}
, document.body, ); }