Skip to content

Instantly share code, notes, and snippets.

@gregberge
Created January 10, 2024 15:04
Show Gist options
  • Save gregberge/224d47a34acf1c3549df0a587c069100 to your computer and use it in GitHub Desktop.
Save gregberge/224d47a34acf1c3549df0a587c069100 to your computer and use it in GitHub Desktop.
import { useEffect, useMemo, useRef, useState } from "react";
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
import { usePrevious } from "./usePrevious";
import { useResizeObserver } from "./useResizeObserver";
export type UseElementOverflowProps<T> = {
/** The array of elements to render */
items: Array<T>;
};
export type UseElementsOverflowOutput<T> = {
/** The ref of the parent element */
containerRef: React.RefObject<HTMLElement>;
/** The ref to attach to the element containing items */
listRef: React.RefObject<HTMLElement>;
/** The style of the list. */
listStyle: React.CSSProperties;
/** The list of items to display */
items: Array<T>;
/** The list of items that overflows */
overflowItems: Array<T>;
};
/**
* This hook is used to render a list of elements and an overflow element.
* The overflow element is rendered when the list of elements doesn't fit in the parent element.
* @example
* const OverflowList = () => {
* const { items, overflowItems, containerRef, listRef, listStyle } =
* useElementsOverflow({
* items: fruits,
* });
*
* return (
* <div
* ref={containerRef as React.RefObject<HTMLDivElement>}
* className="w-screen overflow-hidden"
* >
* <div
* ref={listRef as React.RefObject<HTMLDivElement>}
* className="flex gap-2"
* style={listStyle}
* >
* {items.map((item, index) => (
* <div key={index}>{item}</div>
* ))}
* {overflowItems.length > 0 && <span>+{overflowItems.length}</span>}
* </div>
* </div>
* );
* };
*/
export const useElementsOverflow = <T>(
props: UseElementOverflowProps<T>
): UseElementsOverflowOutput<T> => {
const totalItems = props.items.length;
const previousTotalItems = usePrevious(totalItems);
const [state, setState] = useState<{
count: number;
visible: boolean;
containerWidth: null | number;
}>({
count: totalItems,
visible: false,
containerWidth: null,
});
useEffect(() => {
if (previousTotalItems === undefined) return;
if (totalItems !== previousTotalItems) {
setState((state) => ({
...state,
count: totalItems,
}));
}
}, [totalItems, previousTotalItems]);
const listRef = useRef<HTMLElement>(null);
const containerRef = useRef<HTMLElement>(null);
const setupResizeObserver = useResizeObserver((entry) => {
if (entry.contentRect.width !== state.containerWidth) {
setState((state) => ({
...state,
count: totalItems,
containerWidth: entry.contentRect.width,
}));
}
});
useEffect(() => {
if (totalItems === 0) return;
setupResizeObserver(containerRef.current);
return setupResizeObserver(null);
}, [totalItems, setupResizeObserver]);
useIsomorphicLayoutEffect(() => {
if (!listRef.current || !containerRef.current || !totalItems) return;
const containerWidth = containerRef.current.clientWidth;
const listWidth = listRef.current.scrollWidth;
if (listWidth > containerWidth && state.count > 0) {
setState((state) => ({
...state,
count: state.count - 1,
}));
} else if (!state.visible) {
setState((state) => ({
...state,
visible: true,
containerWidth,
}));
}
}, [totalItems, state]);
const [items, overflowItems] = useMemo(() => {
const items = props.items.slice(0, state.count);
const overflowItems = props.items.slice(state.count);
return [items, overflowItems];
}, [state.count, props.items]);
return {
containerRef,
listRef,
listStyle: { visibility: state.visible ? "visible" : "hidden" },
items,
overflowItems,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment