@carbon/react
Version:
React components for the Carbon Design System
99 lines (97 loc) • 3.46 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { useResizeObserver } from "./useResizeObserver.js";
import { usePreviousValue } from "./usePreviousValue.js";
import { useEffect, useMemo, useRef, useState } from "react";
//#region src/internal/useOverflowItems.ts
/**
* Copyright IBM Corp. 2025, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* Manages overflow items in a container by automatically hiding items that don't fit.
* @param items - Array of items to manage for overflow, each must have an `id` property.
* @param containerRef - React ref to the container element that holds the items.
* @param offsetRef - Optional ref to an offset element (like a "more" button) whose width is reserved when calculating available space.
* @param maxItems - Optional maximum number of visible items. If undefined, only container space constrains visibility.
* @param onChange - Optional callback called when hidden items change. Receives array of currently hidden items.
* @returns Object with `visibleItems` (items to display), `hiddenItems` (items that don't fit), and `itemRefHandler` (function to attach refs to items for width measurement).
*/
const useOverflowItems = (items, containerRef, offsetRef, maxItems, onChange) => {
const itemsRef = useRef(null);
const [maxWidth, setMaxWidth] = useState(0);
const overflowItems = useMemo(() => Array.isArray(items) ? items : [], [items]);
const handleResize = () => {
if (containerRef.current) {
const offset = offsetRef?.current?.offsetWidth || 0;
setMaxWidth(containerRef.current.offsetWidth - offset);
}
};
useResizeObserver({
ref: containerRef,
onResize: handleResize
});
const getMap = () => {
if (!itemsRef.current) itemsRef.current = /* @__PURE__ */ new Map();
return itemsRef.current;
};
const itemRefHandler = (id, node) => {
const map = getMap();
if (node) {
const style = getComputedStyle?.(node);
const totalWidth = node.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
map.set(id, totalWidth);
}
return () => {
map.delete(id);
};
};
const getVisibleItems = () => {
if (!containerRef) return overflowItems;
const map = getMap();
let maxReached = false;
let accumulatedWidth = 0;
return overflowItems.slice(0, maxItems).reduce((prev, cur) => {
if (maxReached) return prev;
const itemWidth = map.get(cur.id) || 0;
if (accumulatedWidth + itemWidth <= maxWidth) {
accumulatedWidth += itemWidth;
prev.push(cur);
} else maxReached = true;
return prev;
}, []);
};
const visibleItems = useMemo(() => {
return getVisibleItems();
}, [
overflowItems,
maxWidth,
maxItems
]);
const hiddenItems = useMemo(() => {
return overflowItems.slice(visibleItems.length);
}, [overflowItems, visibleItems]);
const previousHiddenItems = usePreviousValue(hiddenItems);
useEffect(() => {
if (previousHiddenItems && onChange) {
if (hiddenItems.length !== previousHiddenItems.length || hiddenItems.some((item, index) => item !== previousHiddenItems[index])) onChange(hiddenItems);
}
}, [
hiddenItems,
previousHiddenItems,
onChange
]);
return {
visibleItems,
itemRefHandler,
hiddenItems
};
};
//#endregion
export { useOverflowItems as default };