UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

187 lines (186 loc) 6.12 kB
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; }