UNPKG

@gravity-ui/uikit

Version:

Gravity UI base styling and components

92 lines (91 loc) 4.15 kB
import * as React from 'react'; import { useResizeObserver } from "../../useResizeObserver/index.js"; import { useMatchMedia } from "../useMatchMedia/index.js"; export function useAnimateHeight({ ref, enabled: enabledProp, }) { const isPrefersReducedMotion = useMatchMedia({ media: '(prefers-reduced-motion: reduce)' }); const enabled = enabledProp && !isPrefersReducedMotion; const previousHeight = React.useRef(null); const isTransitioningHeight = React.useRef(false); const overflowY = React.useRef(''); const animationFrame = React.useRef(-1); React.useEffect(() => { let mutationObserver; const node = ref?.current; if (node && enabled) { mutationObserver = new MutationObserver((mutations) => { if (!mutations.length || !isTransitioningHeight.current) return; // If node content changes mid animation, we reset height to immediately animate towards the new height previousHeight.current = calculateNodeHeight(node); isTransitioningHeight.current = false; node.style.height = ''; node.style.overflowY = overflowY.current; cancelAnimationFrame(animationFrame.current); }); mutationObserver.observe(node, { childList: true, subtree: true, }); } return () => { mutationObserver?.disconnect(); }; }, [ref, enabled]); React.useEffect(() => { if (!enabled) { previousHeight.current = null; isTransitioningHeight.current = false; } }, [enabled]); const handleResize = React.useCallback((resizeInfo) => { const node = ref?.current; if (!node || isTransitioningHeight.current || !enabled) { return; } const contentHeight = calculateNodeHeight(node); if (!previousHeight.current && !overflowY.current) { previousHeight.current = contentHeight; overflowY.current = node.style.overflowY; return; } // Skip animation if height hasn't changed (avoids heading margin collapsing issues) if (previousHeight.current && contentHeight === previousHeight.current) { return; } // Avoid "ResizeObserver loop completed with undelivered notifications" error resizeInfo.observer?.unobserve(node); // Set previous height first for the transition to work, because it doesn't work with 'auto' node.style.height = `${previousHeight.current}px`; isTransitioningHeight.current = true; node.style.overflowY = 'clip'; const handleTransitionEnd = (event) => { if (event.propertyName !== 'height') { return; } node.removeEventListener('transitionend', handleTransitionEnd); // ResizeObserver final resize event fires before this, so we have to delay with timeout setTimeout(() => { node.style.height = ''; node.style.overflowY = overflowY.current; isTransitioningHeight.current = false; }, 0); }; node.addEventListener('transitionend', handleTransitionEnd); animationFrame.current = requestAnimationFrame(() => { resizeInfo.observer?.observe(node); node.style.height = `${contentHeight}px`; previousHeight.current = contentHeight; }); }, [ref, enabled]); useResizeObserver({ ref: enabled ? ref : undefined, onResize: handleResize }); } function calculateNodeHeight(node) { const computedStyle = window.getComputedStyle(node, null); if (computedStyle.getPropertyValue('box-sizing') === 'border-box') { return node.clientHeight; } const paddingTop = parseInt(computedStyle.getPropertyValue('padding-top'), 10); const paddingBottom = parseInt(computedStyle.getPropertyValue('padding-bottom'), 10); return node.clientHeight - paddingTop - paddingBottom; } //# sourceMappingURL=useAnimateHeight.js.map