Files
codey.lol/src/hooks/useAutoCompleteScrollFix.ts

169 lines
5.4 KiB
TypeScript
Raw Normal View History

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();
*
* <AutoComplete
* onShow={attachScrollFix}
* onHide={cleanupScrollFix}
* ...
* />
* ```
*/
export function useAutoCompleteScrollFix(maxHeight = '200px') {
const observerRef = useRef<MutationObserver | null>(null);
const wheelHandlerRef = useRef<((e: WheelEvent) => void) | null>(null);
const itemsRef = useRef<HTMLElement | null>(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);
}
};
}