src/collapse/collapse-content.tsx (74 lines of code) (raw):

import React, {useState, useEffect, useRef, useContext, type PropsWithChildren} from 'react'; import classNames from 'classnames'; import dataTests from '../global/data-tests'; import {getRect} from '../global/dom'; import {toPx} from './utils'; import CollapseContext from './collapse-context'; import {COLLAPSE_CONTENT_TEST_ID, COLLAPSE_CONTENT_CONTAINER_TEST_ID} from './consts'; import styles from './collapse.css'; const DURATION_FACTOR = 0.5; const DEFAULT_HEIGHT = 0; const VISIBLE = 1; const HIDDEN = 0; interface Props { minHeight?: number; className?: string; 'data-test'?: string | null | undefined; } /** * @name CollapseContent */ export const CollapseContent: React.FC<PropsWithChildren<Props>> = ({ children, minHeight = DEFAULT_HEIGHT, 'data-test': dataTest, }) => { const {collapsed, duration, id, disableAnimation} = useContext(CollapseContext); const containerRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement | null>(null); const [initialContentHeight] = useState<number>(minHeight); const [contentHeight, setContentHeight] = useState<number>(DEFAULT_HEIGHT); const nextHeight = collapsed ? initialContentHeight : contentHeight; const height = toPx(nextHeight); const [shouldHideContent, setShouldHideContent] = useState<boolean>(collapsed && minHeight <= DEFAULT_HEIGHT); useEffect(() => { function onTransitionEnd() { if (initialContentHeight <= DEFAULT_HEIGHT) { setShouldHideContent(collapsed); } } const container = containerRef.current; container?.addEventListener('transitionend', onTransitionEnd); return () => { container?.removeEventListener('transitionend', onTransitionEnd); }; }, [collapsed, initialContentHeight]); if (!collapsed && shouldHideContent) { setShouldHideContent(false); } useEffect(() => { if (contentRef.current) { const observer = new ResizeObserver(() => { setContentHeight(getRect(contentRef.current).height); }); observer.observe(contentRef.current); } }, []); const calculatedDuration = duration + contentHeight * DURATION_FACTOR; const style = { '--duration': `${calculatedDuration}ms`, height, opacity: collapsed && !minHeight ? HIDDEN : VISIBLE, }; const fadeShouldBeVisible = Boolean(minHeight && collapsed); const shouldRenderContent = disableAnimation ? !collapsed : !shouldHideContent; return ( <div ref={containerRef} id={`collapse-content-${id}`} data-test={dataTests(COLLAPSE_CONTENT_CONTAINER_TEST_ID)} className={classNames(styles.container, {[styles.transition]: !disableAnimation})} style={style} > <div ref={contentRef} data-test={dataTests(COLLAPSE_CONTENT_TEST_ID, dataTest)}> {shouldRenderContent ? children : null} </div> {fadeShouldBeVisible && <div className={styles.fade} />} </div> ); }; export default CollapseContent;