@wix/design-system
Version:
@wix/design-system
130 lines • 5.53 kB
JavaScript
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