import React, { useState, useRef, useEffect, useCallback, FC } from 'react'; import classNames from 'classnames'; export interface CollapsibleProps { /** 隐藏时是否渲染 dom 结构,特殊情况下必须渲染,例如包含 Form,否则取不到值 */ forceRender?: boolean; /** 是否显示 */ open: boolean; /** 自定义 css 动画样式 */ transition?: { duration?: string; timingFunction?: string; }; } type AnimationState = 'idle' | 'measuring' | 'animating'; const Collapsible: FC = ({ forceRender, open, transition, children }) => { const [height, setHeight] = useState(0); const [isOpen, setIsOpen] = useState(open); const [animationState, setAnimationState] = useState('idle'); const collapsibleContainer = useRef(null); const isFullyOpen = animationState === 'idle' && open && isOpen; const isFullyClosed = animationState === 'idle' && !open && !isOpen; const content = forceRender || !isFullyClosed ? children : null; const wrapperClassName = classNames({ collapsible: true, 'collapsible-is-fully-closed': isFullyClosed, forceRender, }); const collapsibleStyles = { ...(transition && { transitionDuration: `${transition.duration}`, transitionTimingFunction: `${transition.timingFunction}`, }), ...{ // stylelint-disable maxHeight: isFullyOpen ? 'none' : `${height}px`, overflow: isFullyOpen ? 'visible' : 'hidden', }, }; const handleCompleteAnimation = useCallback( ({ target }: React.TransitionEvent) => { if (target === collapsibleContainer.current) { setAnimationState('idle'); setIsOpen(open); } }, [open], ); useEffect(() => { if (open !== isOpen) { setAnimationState('measuring'); } }, [open, isOpen]); useEffect(() => { if (!open || !collapsibleContainer.current) return; setHeight(collapsibleContainer.current.scrollHeight); }, []); useEffect(() => { if (!collapsibleContainer.current) return; // eslint-disable-next-line default-case switch (animationState) { case 'idle': break; case 'measuring': setHeight(collapsibleContainer.current.scrollHeight); setAnimationState('animating'); break; case 'animating': setHeight(open ? collapsibleContainer.current.scrollHeight : 0); } }, [animationState, open, isOpen]); return (
{content}
); }; export default Collapsible;