diff --git a/src/display/components/collapsible.css b/src/display/components/collapsible.css new file mode 100644 index 0000000000000000000000000000000000000000..8bc1d8f7618e32d10fb5304057510d2e831e9bc3 --- /dev/null +++ b/src/display/components/collapsible.css @@ -0,0 +1,76 @@ +.collapsible-content { + overflow: hidden; + transition: height 0.2s ease-in-out; + border-bottom: 1px solid #dee2e6 !important; +} + +.collapsible-content-padding { + padding: 20px 20px 20px 30px; +} + +.collapsible-rotate-center { + -moz-transition: all 0.2s linear; + -webkit-transition: all 0.2s linear; + font-size: 4em; + transition: all 0.2s linear; +} +.collapsible-rotate-center.down { + transform: rotate(-90deg); +} +.collapsible-rotate-center.up { + transform: rotate(90deg); +} + +.collapsible-button { + position: relative; + display: flex; + align-items: center; + width: 100%; + font-size: 1.2rem; + background-color: transparent; + border-bottom: 1px solid gray; + color: black; + padding-left: 25px; +} + +.collapsible-button:not(.collapsed) { + background-color: aliceblue; + color: royalblue; +} + +.collapsible-button.collapsed:hover { + background-color: transparent; +} + +.collapsible-button:hover { + background-color: aliceblue; +} + +.collapsible-button.collapsed:focus { + border-color: lightblue; + background-color: white; +} + +.collapsible-button.collapsed:focus { + border-color: lightblue; +} + +.collapsible-button::after { + content: "\276E"; + margin-left: auto; + transition: all 0.2s linear; + transform: rotate(90deg); +} + +.collapsible-button:not(.collapsed)::after { + transform: rotate(-90deg); +} + +.collapsible-card { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); + transition: 0.3s; +} + +.collapsible-card:hover { + box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); +} diff --git a/src/display/components/collapsible.tsx b/src/display/components/collapsible.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b4dd7e419a06dd66c7a881b7ba5e4ff589543a90 --- /dev/null +++ b/src/display/components/collapsible.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useRef, useState } from "react"; +import "./collapsible.css"; + +interface CollapsibleProps { + open?: boolean; + header: string | React.ReactNode; + children?: React.ReactNode | React.ReactNode[]; +} + +// Implementation details at nhttps://medium.com/edonec/build-a-react-collapsible-component-from-scratch-using-react-hooks-typescript-73dfd02c9208 +function Collapsible({ open, header, children }: 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]); + + return ( + <> + <div className={"collapsible-card"}> + <div> + <button + type="button" + className={`collapsible-button ${ + isOpen ? "" : "collapsed" + }`} + onClick={toggleOpen} + > + {header} + </button> + </div> + <div className={"collapsible-content"} style={{ height }}> + <div ref={ref}> + <div className={"collapsible-content-padding"}> + {children} + </div> + </div> + </div> + </div> + </> + ); +} + +export default Collapsible;