import { c, classy, cssVars, space } from '@onfido/castor'; import type { IconName } from '@onfido/castor-icons'; import { Icon } from '@onfido/castor-react'; import React, { useEffect, useLayoutEffect, useRef, useState, type FC, } from 'react'; export type AccordionItem = { name: string; isOpen?: boolean; title: React.ReactElement; iconName?: IconName; content: React.ReactElement; secondaryContent?: React.ReactElement; }; export type AccordionStyles = { padding?: number; }; interface AccordionBaseProps { list: AccordionItem[]; onChange?: (accordionItem: AccordionItem) => void; onlyOneOpen?: boolean; separated?: boolean; withSeparator?: boolean; styles?: AccordionStyles; className?: string; } interface AccordionPropsWithoutOnlyOneOpen extends Omit { onlyOneOpen?: never; } export type AccordionProps = AccordionBaseProps['separated'] extends true ? AccordionPropsWithoutOnlyOneOpen : AccordionBaseProps; const defaultStyles = { padding: 3, }; const useWindowSize = () => { const [size, setSize] = useState([0, 0]); useLayoutEffect(() => { const updateSize = () => { setSize([window.innerWidth, window.innerHeight]); }; window.addEventListener('resize', updateSize); updateSize(); return () => window.removeEventListener('resize', updateSize); }, []); return size; }; export const Accordion: FC = ({ list: listProps, onChange, onlyOneOpen = false, separated = false, withSeparator = false, styles: stylesProps = defaultStyles, className = '', }) => { const initHeights: number[] = []; const [list, setList] = useState(listProps); const [heights, setHeights] = useState(initHeights); const [ready, setReady] = useState(false); const init: HTMLDivElement[] = []; const content = useRef(init); const [windowWidth] = useWindowSize(); const [animatedItems, setAnimatedItems] = useState( new Array(list.length).fill(false) ); const _styles = { ...defaultStyles, ...stylesProps, }; const updateHeights = () => setHeights( content.current.map((item) => { console.log(item.clientHeight); return item.clientHeight; }) ); useEffect(() => { setReady(false); }, [windowWidth]); useEffect(() => { if (!ready) { updateHeights(); } }, [ready]); useEffect(() => { if (content.current.length) { updateHeights(); } }, [content]); useEffect(() => { if (heights.length === list.length) { setReady(true); } }, [heights]); const handleClick = (index: number) => () => { let newList = [...list]; if (onlyOneOpen && !separated) { newList = newList.map((item, newIndex) => ({ ...item, isOpen: newIndex !== index ? false : !item.isOpen, })); } else { newList[index].isOpen = !list[index].isOpen; } setList(newList); if (onChange) onChange(newList[index]); }; const handleRef = (index: number) => (el: HTMLDivElement) => { content.current[index] = el; }; const style = cssVars({ padding: space(_styles.padding) }); const animate = (index: number, started: boolean) => () => { const newAnimatedItems = [...animatedItems]; newAnimatedItems[index] = started; setAnimatedItems(newAnimatedItems); }; useLayoutEffect(() => { list.forEach((_, index) => { const el = content.current[index]; el.addEventListener('transitionstart', animate(index, true)); el.addEventListener('transitionend', animate(index, false)); }); return () => list.forEach((_, index) => { const el = content.current[index]; el.removeEventListener('transitionstart', animate(index, true)); el.removeEventListener('transitionend', animate(index, false)); }); }, []); return (
{list.map((item, index) => (
{(!ready || content.current[index].clientHeight !== 0 || animatedItems[index]) && (
{item.content}
)}
))}
); };