@mantine/hooks
Version:
A collection of 50+ hooks for state and UI management
200 lines (199 loc) • 6.58 kB
JavaScript
"use client";
const require_use_uncontrolled = require("../use-uncontrolled/use-uncontrolled.cjs");
let react = require("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 = (0, react.useRef)(/* @__PURE__ */ new Map());
const isGrid = typeof columns === "number" && columns > 0;
const [activeIndex, setActiveIndex] = require_use_uncontrolled.useUncontrolled({
value: focusedIndex,
defaultValue: initialIndex !== void 0 ? initialIndex : findFirstEnabled(total, isItemDisabled),
finalValue: 0,
onChange: onFocusChange
});
(0, react.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 = (0, react.useCallback)((index) => {
setActiveIndex(index);
const element = itemRefs.current.get(index);
if (element) {
element.focus();
if (activateOnFocus) element.click();
}
}, [activateOnFocus, setActiveIndex]);
const handleGridKeyDown = (0, react.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 = (0, react.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: (0, react.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
exports.useRovingIndex = useRovingIndex;
//# sourceMappingURL=use-roving-index.cjs.map