Skip to content
Snippets Groups Projects
collapsible.tsx 2.71 KiB
Newer Older
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
/**
 * Collapisble section
 * @param open Sets the default state of the collapsible section
 * @param header Title text
 * @param children React child elements
 * @param color Optional bottom border color
 * @param heightTransition Weather or not this section should render a smooth transition if the content height changes.
 * @constructor
 */
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;