import React, { useEffect, useRef, useState } from "react"; import "./collapsible.css"; interface CollapsibleProps { open?: boolean; header: string | React.ReactNode; children?: React.ReactNode | React.ReactNode[]; color?: string; heightTransition?: boolean; } // Implementation details partially from https://medium.com/edonec/build-a-react-collapsible-component-from-scratch-using-react-hooks-typescript-73dfd02c9208 function Collapsible({ open, header, children, color = "gray", heightTransition = true, }: CollapsibleProps) { const [isOpen, setIsOpen] = useState(open); const [height, setHeight] = useState<number | undefined>( open ? undefined : 0 ); const toggleOpen = () => { setIsOpen((prev) => !prev); }; const ref = useRef<HTMLDivElement>(null); useEffect(() => { if (!height || !isOpen || !ref.current) return undefined; const resizeObserver = new ResizeObserver((el) => { setHeight(el[0].contentRect.height); }); resizeObserver.observe(ref.current); return () => { resizeObserver.disconnect(); }; }, [height, isOpen]); useEffect(() => { if (isOpen) setHeight(ref.current?.getBoundingClientRect().height); else setHeight(0); }, [isOpen]); const borderBottom = "1px solid " + color; return ( <> <div className={"collapsible-card"}> <div> <button type="button" className={`collapsible-button ${ isOpen ? "" : "collapsed" }`} onClick={toggleOpen} style={{ borderBottom }} > {header} </button> </div> <div className={`collapsible-content ${heightTransition ? "collapsible-transition-height" : ""}`} style={{ height }}> <div ref={ref}> <div className={"collapsible-content-padding"}> {children} </div> </div> </div> </div> </> ); } export default Collapsible;