@itwin/itwinui-react
Version:
A react component library for iTwinUI
187 lines (186 loc) • 6.12 kB
JavaScript
import React from 'react';
import { useMergedRefs } from '../hooks/useMergedRefs.js';
import { Box } from './Box.js';
import { useLayoutEffect } from '../hooks/useIsomorphicLayoutEffect.js';
import { useSafeContext } from '../hooks/useSafeContext.js';
import { isUnitTest } from '../functions/dev.js';
import { useResizeObserver } from '../hooks/useResizeObserver.js';
let OverflowContainerMain = React.forwardRef((props, forwardedRef) => {
let { itemsCount, children, overflowOrientation, ...rest } = props;
let [containerRef, visibleCount] = useOverflow(
itemsCount,
overflowOrientation,
);
let overflowContainerContextValue = React.useMemo(
() => ({
visibleCount,
itemsCount,
}),
[itemsCount, visibleCount],
);
return React.createElement(
OverflowContainerContext.Provider,
{
value: overflowContainerContextValue,
},
React.createElement(
Box,
{
ref: useMergedRefs(forwardedRef, containerRef),
...rest,
},
children,
),
);
});
let OverflowContainerOverflowNode = (props) => {
let { children } = props;
let { visibleCount, itemsCount } = useOverflowContainerContext();
let isOverflowing = visibleCount < itemsCount;
return isOverflowing ? children : null;
};
let OverflowContainerComponent = React.forwardRef((props, forwardedRef) => {
let { itemsCount, overflowOrientation = 'horizontal', ...rest } = props;
let [size, setSize] = React.useState(null);
let [resizeRef] = useResizeObserver(setSize);
let ref = useMergedRefs(resizeRef, forwardedRef);
let key = `${itemsCount}${
'vertical' === overflowOrientation ? size?.height : size?.width
}`;
return React.createElement(OverflowContainerMain, {
...rest,
key: key,
ref: ref,
itemsCount: itemsCount,
overflowOrientation: overflowOrientation,
});
});
export const OverflowContainer = Object.assign(OverflowContainerComponent, {
OverflowNode: OverflowContainerOverflowNode,
useContext: useOverflowContainerContext,
});
let OverflowContainerContext = React.createContext(void 0);
if ('development' === process.env.NODE_ENV)
OverflowContainerContext.displayName = 'OverflowContainerContext';
let useOverflow = (itemsCount, orientation = 'horizontal') => {
let [guessState, dispatch] = React.useReducer(
overflowGuessReducer,
{
itemsCount,
},
overflowGuessReducerInitialState,
);
let containerRef = React.useRef(null);
let isGuessing = React.useRef(false);
useLayoutEffect(() => {
let { minGuess, maxGuess, isStabilized, visibleCount } = guessState;
if (isStabilized) return;
guessVisibleCount();
function guessVisibleCount() {
if (isStabilized || isGuessing.current || isUnitTest) return;
try {
isGuessing.current = true;
if (null == containerRef.current) return;
let dimension = 'horizontal' === orientation ? 'Width' : 'Height';
let availableSize = containerRef.current[`offset${dimension}`];
let requiredSize = containerRef.current[`scroll${dimension}`];
let isOverflowing = availableSize < requiredSize;
if (
0 === itemsCount ||
(1 === visibleCount && isOverflowing) ||
(visibleCount === itemsCount && !isOverflowing) ||
(maxGuess - minGuess === 1 && visibleCount === minGuess)
)
return void dispatch({
type: 'stabilize',
});
if (maxGuess === visibleCount && !isOverflowing)
return void dispatch({
type: 'shiftGuessRangeForward',
});
isOverflowing
? dispatch({
type: 'decreaseMaxGuess',
currentState: guessState,
})
: dispatch({
type: 'increaseMinGuess',
currentState: guessState,
});
} finally {
isGuessing.current = false;
}
}
}, [guessState, itemsCount, orientation]);
return [containerRef, guessState.visibleCount];
};
let STARTING_MAX_ITEMS_COUNT = 32;
let overflowGuessReducerInitialState = ({ itemsCount }) => {
let initialVisibleCount = Math.min(itemsCount, STARTING_MAX_ITEMS_COUNT);
return isUnitTest
? {
isStabilized: true,
minGuess: null,
maxGuess: null,
itemsCount,
visibleCount: itemsCount,
}
: {
isStabilized: false,
minGuess: 0,
maxGuess: initialVisibleCount,
itemsCount,
visibleCount: initialVisibleCount,
};
};
let overflowGuessReducer = (state, action) => {
let getSafeVisibleCount = ({ visibleCount, itemsCount }) =>
Math.min(itemsCount, visibleCount);
switch (action.type) {
case 'decreaseMaxGuess':
case 'increaseMinGuess':
if (state.isStabilized) return state;
let newMinGuess = state.minGuess;
let newMaxGuess = state.maxGuess;
if ('decreaseMaxGuess' === action.type)
newMaxGuess = action.currentState.visibleCount;
else newMinGuess = action.currentState.visibleCount;
let newVisibleCount = Math.floor((newMinGuess + newMaxGuess) / 2);
return {
...state,
isStabilized: false,
minGuess: newMinGuess,
maxGuess: newMaxGuess,
visibleCount: getSafeVisibleCount({
visibleCount: newVisibleCount,
itemsCount: state.itemsCount,
}),
};
case 'shiftGuessRangeForward':
if (state.isStabilized) return state;
let doubleOfMaxGuess = 2 * state.maxGuess;
return {
...state,
isStabilized: false,
minGuess: state.maxGuess,
maxGuess: doubleOfMaxGuess,
visibleCount: getSafeVisibleCount({
visibleCount: doubleOfMaxGuess,
itemsCount: state.itemsCount,
}),
};
case 'stabilize':
return {
...state,
isStabilized: true,
minGuess: null,
maxGuess: null,
};
default:
return state;
}
};
function useOverflowContainerContext() {
let overflowContainerContext = useSafeContext(OverflowContainerContext);
return overflowContainerContext;
}