import { type RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState, useUnmountCleanup, } from '../../../lib/teact/teact'; import { setExtraStyles } from '../../../lib/teact/teact-dom'; import { requestForcedReflow, requestNextMutation } from '../../../lib/fasterdom/fasterdom'; import useTimeout from '../../../hooks/schedulers/useTimeout'; import useLastCallback from '../../../hooks/useLastCallback'; import useResizeObserver from '../../../hooks/useResizeObserver'; import useThrottledCallback from '../../../hooks/useThrottledCallback'; export interface PaneState { element?: HTMLElement; height: number; isOpen?: boolean; } // Max slide transition duration const CLOSE_DURATION = 450; const RESIZE_THROTTLE = 100; export default function useHeaderPane({ ref: providedRef, isOpen, isDisabled, withResizeObserver, onStateChange, } : { ref?: RefObject; isOpen?: boolean; isDisabled?: boolean; withResizeObserver?: boolean; onStateChange?: (state: PaneState) => void; }) { const [shouldRender, setShouldRender] = useState(isOpen); // eslint-disable-next-line no-null/no-null const localRef = useRef(null); const ref = providedRef || localRef; const lastHeightRef = useRef(0); const reset = useLastCallback(() => { setShouldRender(true); onStateChange?.({ element: undefined, height: 0, isOpen: false, }); }); useEffect(() => { if (isDisabled) { reset(); } }, [isDisabled]); useEffect(() => { if (isOpen) { setShouldRender(true); } }, [isOpen]); useUnmountCleanup(reset); useTimeout(() => { setShouldRender(false); onStateChange?.({ height: 0, isOpen: false, }); }, !isOpen ? CLOSE_DURATION : undefined); // Should be `useCallback` to trigger effect on deps change const handleUpdate = useCallback(() => { const element = ref.current; if (isDisabled || !element || !shouldRender) return; if (!isOpen) { onStateChange?.({ element, height: 0, isOpen: false, }); return; } requestForcedReflow(() => { const currentHeight = element.offsetHeight; lastHeightRef.current = currentHeight; return () => { onStateChange?.({ element, height: currentHeight, isOpen, }); }; }); }, [isOpen, shouldRender, isDisabled, ref, onStateChange]); const handleResize = useThrottledCallback(() => { const element = ref.current; if (!element) return; const newHeight = element.offsetHeight; if (newHeight === lastHeightRef.current) { return; } handleUpdate(); }, [handleUpdate, ref], RESIZE_THROTTLE, true); useLayoutEffect(handleUpdate, [handleUpdate]); useResizeObserver(ref, handleResize, !withResizeObserver || !shouldRender); return { shouldRender, ref, }; } export function applyAnimationState(list: PaneState[], noTransition = false) { let cumulativeHeight = 0; for (let i = 0; i < list.length; i++) { const state = list[i]; const element = state.element; if (!element) { continue; } const shiftPx = `${cumulativeHeight}px`; const apply = () => { setExtraStyles(element, { transform: `translateY(${state.isOpen ? shiftPx : `calc(${shiftPx} - 100%)`})`, zIndex: String(-i), transition: noTransition ? 'none' : '', }); }; if (!element.dataset.isPanelOpen && state.isOpen && !noTransition) { // Start animation right above its final position setExtraStyles(element, { transform: `translateY(calc(${shiftPx} - 100%))`, zIndex: String(-i), transition: 'none', }); element.dataset.isPanelOpen = 'true'; requestNextMutation(apply); } else { apply(); } cumulativeHeight += state.height; } }