UNPKG

@wix/design-system

Version:

@wix/design-system

130 lines 5.53 kB
import { useCallback, useRef, useState } from 'react'; const useScrollKeyboardNavigation = ({ containerRef, items, getItemDomId, multiple, onToggle, }) => { const lastActiveItemIdRef = useRef(null); const isMouseFocusRef = useRef(false); const [activeItemId, setActiveItemId] = useState(null); // Called on mousedown to mark that next focus is from mouse and clear active item const handleMouseDown = useCallback(() => { isMouseFocusRef.current = true; setActiveItemId(null); }, []); // Get first non-disabled item id const getFirstItemId = useCallback(() => { const firstEnabled = items.find(item => !item.disabled); return firstEnabled?.id ?? null; }, [items]); // Scroll active item into view using DOM id (only scrolls container, not window) const scrollItemIntoView = useCallback((itemId) => { const container = containerRef.current; const domId = getItemDomId?.(itemId); const element = domId ? document.getElementById(domId) : null; if (!element || !container) return; const containerRect = container.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); // Check if element is above visible area if (elementRect.top < containerRect.top) { container.scrollTop -= containerRect.top - elementRect.top; } // Check if element is below visible area else if (elementRect.bottom > containerRect.bottom) { container.scrollTop += elementRect.bottom - containerRect.bottom; } }, [containerRef, getItemDomId]); const handleKeyDown = useCallback((e) => { const container = containerRef.current; if (!container) { return; } const onContainer = document.activeElement === container; // Arrow keys: navigate between items (virtual focus) if (onContainer && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { e.preventDefault(); e.stopPropagation(); const enabledItems = items.filter(item => !item.disabled); if (enabledItems.length === 0) return; const currentIndex = enabledItems.findIndex(item => item.id === activeItemId); const direction = e.key === 'ArrowDown' ? 1 : -1; let nextIndex; if (currentIndex === -1) { // No active item, start from first or last nextIndex = direction === 1 ? 0 : enabledItems.length - 1; } else { nextIndex = currentIndex + direction; } // Stop at boundaries if (nextIndex < 0 || nextIndex >= enabledItems.length) { return; } const nextItem = enabledItems[nextIndex]; setActiveItemId(nextItem.id); scrollItemIntoView(nextItem.id); // Single select: selection follows focus if (!multiple && onToggle) { onToggle(nextItem); } return; } // Enter/Space: toggle selection on active item if (onContainer && (e.key === 'Enter' || e.key === ' ')) { if (activeItemId !== null) { e.preventDefault(); e.stopPropagation(); const activeItem = items.find(item => item.id === activeItemId); if (activeItem && !activeItem.disabled && onToggle) { onToggle(activeItem); } } return; } }, [containerRef, items, activeItemId, scrollItemIntoView, multiple, onToggle]); const handleFocus = useCallback(() => { // If focus came from mouse click, don't set active item if (isMouseFocusRef.current) { isMouseFocusRef.current = false; return; } // Restore saved item if returning from keyboard navigation if (lastActiveItemIdRef.current !== null) { const restoredId = lastActiveItemIdRef.current; setActiveItemId(restoredId); lastActiveItemIdRef.current = null; // Single select: also select the restored item if (!multiple && onToggle) { const restoredItem = items.find(item => item.id === restoredId); if (restoredItem && !restoredItem.disabled) { onToggle(restoredItem); } } return; } // Tab into container via keyboard - set first item as active const firstId = getFirstItemId(); if (firstId !== null) { setActiveItemId(firstId); // Single select: also select the first item if (!multiple && onToggle) { const firstItem = items.find(item => item.id === firstId); if (firstItem && !firstItem.disabled) { onToggle(firstItem); } } } }, [getFirstItemId, items, multiple, onToggle]); const handleBlur = useCallback(() => { // Save and clear active item when container loses focus lastActiveItemIdRef.current = activeItemId; setActiveItemId(null); }, [activeItemId]); return { handleKeyDown, handleFocus, handleBlur, handleMouseDown, activeItemId, }; }; export default useScrollKeyboardNavigation; //# sourceMappingURL=useScrollKeyboardNavigation.js.map