UNPKG

@mantine/hooks

Version:

A collection of 50+ hooks for state and UI management

200 lines (199 loc) 6.49 kB
"use client"; import { useUncontrolled } from "../use-uncontrolled/use-uncontrolled.mjs"; import { useCallback, useEffect, useRef } from "react"; //#region packages/@mantine/hooks/src/use-roving-index/use-roving-index.ts function findNextEnabled(current, total, isItemDisabled, loop) { for (let i = current + 1; i < total; i += 1) if (!isItemDisabled(i)) return i; if (loop) { for (let i = 0; i < current; i += 1) if (!isItemDisabled(i)) return i; } return current; } function findPreviousEnabled(current, total, isItemDisabled, loop) { for (let i = current - 1; i >= 0; i -= 1) if (!isItemDisabled(i)) return i; if (loop) { for (let i = total - 1; i > current; i -= 1) if (!isItemDisabled(i)) return i; } return current; } function findFirstEnabled(total, isItemDisabled) { for (let i = 0; i < total; i += 1) if (!isItemDisabled(i)) return i; return 0; } function findLastEnabled(total, isItemDisabled) { for (let i = total - 1; i >= 0; i -= 1) if (!isItemDisabled(i)) return i; return 0; } const defaultIsItemDisabled = () => false; function useRovingIndex(input) { const { total, orientation = "horizontal", loop = true, dir = "ltr", activateOnFocus = false, columns, focusedIndex, initialIndex, onFocusChange, isItemDisabled = defaultIsItemDisabled } = input; const itemRefs = useRef(/* @__PURE__ */ new Map()); const isGrid = typeof columns === "number" && columns > 0; const [activeIndex, setActiveIndex] = useUncontrolled({ value: focusedIndex, defaultValue: initialIndex !== void 0 ? initialIndex : findFirstEnabled(total, isItemDisabled), finalValue: 0, onChange: onFocusChange }); useEffect(() => { if (total === 0) return; if (activeIndex >= total) setActiveIndex(findLastEnabled(total, isItemDisabled)); else if (isItemDisabled(activeIndex)) setActiveIndex(findFirstEnabled(total, isItemDisabled)); }, [ total, activeIndex, isItemDisabled ]); const focusItem = useCallback((index) => { setActiveIndex(index); const element = itemRefs.current.get(index); if (element) { element.focus(); if (activateOnFocus) element.click(); } }, [activateOnFocus, setActiveIndex]); const handleGridKeyDown = useCallback((event, currentIndex) => { const row = Math.floor(currentIndex / columns); const col = currentIndex % columns; const totalRows = Math.ceil(total / columns); let nextIndex = null; const isRtl = dir === "rtl"; switch (event.key) { case "ArrowRight": { const targetCol = isRtl ? col - 1 : col + 1; if (targetCol >= 0 && targetCol < columns && row * columns + targetCol < total) { const candidate = row * columns + targetCol; if (!isItemDisabled(candidate)) nextIndex = candidate; } break; } case "ArrowLeft": { const targetCol = isRtl ? col + 1 : col - 1; if (targetCol >= 0 && targetCol < columns && row * columns + targetCol < total) { const candidate = row * columns + targetCol; if (!isItemDisabled(candidate)) nextIndex = candidate; } break; } case "ArrowDown": for (let r = row + 1; r < totalRows; r += 1) { const candidate = r * columns + col; if (candidate < total && !isItemDisabled(candidate)) { nextIndex = candidate; break; } } break; case "ArrowUp": for (let r = row - 1; r >= 0; r -= 1) { const candidate = r * columns + col; if (candidate < total && !isItemDisabled(candidate)) { nextIndex = candidate; break; } } break; case "Home": if (event.ctrlKey) nextIndex = findFirstEnabled(total, isItemDisabled); else { const rowStart = row * columns; for (let i = rowStart; i < rowStart + columns && i < total; i += 1) if (!isItemDisabled(i)) { nextIndex = i; break; } } break; case "End": if (event.ctrlKey) nextIndex = findLastEnabled(total, isItemDisabled); else { const rowStart = row * columns; const rowEnd = Math.min(rowStart + columns, total) - 1; for (let i = rowEnd; i >= rowStart; i -= 1) if (!isItemDisabled(i)) { nextIndex = i; break; } } break; } if (nextIndex !== null && nextIndex !== currentIndex) { event.preventDefault(); event.stopPropagation(); focusItem(nextIndex); } }, [ total, columns, dir, isItemDisabled, focusItem ]); const handleListKeyDown = useCallback((event, currentIndex) => { const isRtl = dir === "rtl"; let nextIndex = null; switch (event.key) { case "ArrowRight": if (orientation === "horizontal" || orientation === "both") nextIndex = isRtl ? findPreviousEnabled(currentIndex, total, isItemDisabled, loop) : findNextEnabled(currentIndex, total, isItemDisabled, loop); break; case "ArrowLeft": if (orientation === "horizontal" || orientation === "both") nextIndex = isRtl ? findNextEnabled(currentIndex, total, isItemDisabled, loop) : findPreviousEnabled(currentIndex, total, isItemDisabled, loop); break; case "ArrowDown": if (orientation === "vertical" || orientation === "both") nextIndex = findNextEnabled(currentIndex, total, isItemDisabled, loop); break; case "ArrowUp": if (orientation === "vertical" || orientation === "both") nextIndex = findPreviousEnabled(currentIndex, total, isItemDisabled, loop); break; case "Home": nextIndex = findFirstEnabled(total, isItemDisabled); break; case "End": nextIndex = findLastEnabled(total, isItemDisabled); break; } if (nextIndex !== null && nextIndex !== currentIndex) { event.preventDefault(); event.stopPropagation(); focusItem(nextIndex); } }, [ total, orientation, loop, dir, isItemDisabled, focusItem ]); return { getItemProps: useCallback((options) => { const { index, onClick, onKeyDown } = options; return { tabIndex: index === activeIndex ? 0 : -1, ref: (node) => { if (node) itemRefs.current.set(index, node); else itemRefs.current.delete(index); }, onKeyDown: (event) => { onKeyDown?.(event); if (event.defaultPrevented) return; if (isGrid) handleGridKeyDown(event, index); else handleListKeyDown(event, index); }, onClick: (event) => { onClick?.(event); setActiveIndex(index); } }; }, [ activeIndex, isGrid, handleGridKeyDown, handleListKeyDown, setActiveIndex ]), focusedIndex: activeIndex, setFocusedIndex: setActiveIndex }; } //#endregion export { useRovingIndex }; //# sourceMappingURL=use-roving-index.mjs.map