import { useCallback, useRef } from 'react'; /** * Hook to fix scrolling issues in PrimeReact AutoComplete dropdown panels. * * Fixes two issues: * 1. Mouse wheel scrolling not working properly within the dropdown * 2. Keyboard navigation (arrow keys) not scrolling the highlighted item into view * * Usage: * ```tsx * const { attachScrollFix, cleanupScrollFix } = useAutoCompleteScrollFix(); * * * ``` */ export function useAutoCompleteScrollFix(maxHeight = '200px') { const observerRef = useRef(null); const wheelHandlerRef = useRef<((e: WheelEvent) => void) | null>(null); const itemsRef = useRef(null); const cleanupScrollFix = useCallback(() => { if (observerRef.current) { observerRef.current.disconnect(); observerRef.current = null; } if (itemsRef.current && wheelHandlerRef.current) { itemsRef.current.removeEventListener('wheel', wheelHandlerRef.current as EventListener); } wheelHandlerRef.current = null; itemsRef.current = null; }, []); const attachScrollFix = useCallback(() => { // Clean up any existing handlers first cleanupScrollFix(); setTimeout(() => { const panel = document.querySelector('.p-autocomplete-panel'); const items = panel?.querySelector('.p-autocomplete-items') as HTMLElement | null; if (!items) return; itemsRef.current = items; // Apply scrollable styles items.style.maxHeight = maxHeight; items.style.overflowY = 'auto'; items.style.overscrollBehavior = 'contain'; // Mouse wheel scroll handler - prevent page scroll when at boundaries const wheelHandler = (e: WheelEvent) => { const delta = e.deltaY; const atTop = items.scrollTop === 0; const atBottom = items.scrollTop + items.clientHeight >= items.scrollHeight; if ((delta < 0 && atTop) || (delta > 0 && atBottom)) { e.preventDefault(); } else { e.stopPropagation(); } }; wheelHandlerRef.current = wheelHandler; items.addEventListener('wheel', wheelHandler, { passive: false }); // MutationObserver to watch for highlighted item changes (keyboard navigation) // This ensures the highlighted item scrolls into view when using arrow keys const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const target = mutation.target as HTMLElement; if (target.classList.contains('p-highlight')) { // Scroll the highlighted item into view target.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); break; } } } }); // Observe all autocomplete items for class changes const allItems = items.querySelectorAll('.p-autocomplete-item'); allItems.forEach((item) => { observer.observe(item, { attributes: true, attributeFilter: ['class'] }); }); observerRef.current = observer; }, 0); }, [maxHeight, cleanupScrollFix]); return { attachScrollFix, cleanupScrollFix }; } /** * Standalone function for components that don't use hooks (e.g., in event handlers). * Returns a cleanup function that should be called when the panel hides. */ export function attachAutoCompleteScrollFix(maxHeight = '200px'): () => void { let observer: MutationObserver | null = null; let wheelHandler: ((e: WheelEvent) => void) | null = null; let items: HTMLElement | null = null; setTimeout(() => { const panel = document.querySelector('.p-autocomplete-panel'); items = panel?.querySelector('.p-autocomplete-items') as HTMLElement | null; if (!items) return; // Apply scrollable styles items.style.maxHeight = maxHeight; items.style.overflowY = 'auto'; items.style.overscrollBehavior = 'contain'; // Mouse wheel scroll handler wheelHandler = (e: WheelEvent) => { const delta = e.deltaY; const atTop = items!.scrollTop === 0; const atBottom = items!.scrollTop + items!.clientHeight >= items!.scrollHeight; if ((delta < 0 && atTop) || (delta > 0 && atBottom)) { e.preventDefault(); } else { e.stopPropagation(); } }; items.addEventListener('wheel', wheelHandler, { passive: false }); // MutationObserver for keyboard navigation scroll observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const target = mutation.target as HTMLElement; if (target.classList.contains('p-highlight')) { target.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); break; } } } }); const allItems = items.querySelectorAll('.p-autocomplete-item'); allItems.forEach((item) => { observer!.observe(item, { attributes: true, attributeFilter: ['class'] }); }); }, 0); // Return cleanup function return () => { if (observer) { observer.disconnect(); } if (items && wheelHandler) { items.removeEventListener('wheel', wheelHandler as EventListener); } }; }