import React, { FC, ReactNode, useEffect, useRef, useState } from 'react' import styled, { css, SimpleInterpolation } from 'styled-components' import { fold, fromNullable, getOrElse } from 'fp-ts/lib/Option' import { pipe } from 'fp-ts/lib/pipeable' import { baseFocusStyles, Colors, flexFlow, getColor, } from '@monorail/helpers/exports' import { Div } from '@monorail/StyleHelpers' import { ButtonDisplay } from '@monorail/visualComponents/buttons/buttonTypes' import { IconButton } from '@monorail/visualComponents/buttons/IconButton' import { Icon } from '@monorail/visualComponents/icon/Icon' import { ScrollAnimation } from '@monorail/visualComponents/layout/ScrollAnimation' /* * Styled Components */ const CollapsibleWrapper = styled.div<{ isContentScrollable: boolean }>` display: flex; flex-wrap: nowrap; flex-direction: column; overflow: hidden; flex-grow: 0; flex-shrink: ${props => (props.isContentScrollable ? 1 : 0)}; flex-basis: auto; min-height: 40px; ` const CollapsibleHeader = styled.div<{ iconPosition: IconPosition expanded: boolean clickTarget: ClickTarget }>` ${flexFlow('row')} width: 100%; box-sizing: border-box; height: 40px; ${props => props.iconPosition === 'right' && `justify-content: space-between;`} align-items: center; margin: 0; padding: 0; position: relative; ${props => props.clickTarget === 'icon' && `padding: 16px; background: ${ props.expanded ? getColor(Colors.SidebarActive) : getColor(Colors.White) };`} ` const CollapsibleHeaderButton = styled.button<{ expanded: boolean iconPosition: IconPosition }>` ${flexFlow('row')} align-items: center; background: ${props => props.expanded ? getColor(Colors.SidebarActive) : getColor(Colors.White)}; border: none; cursor: pointer; justify-content: ${props => props.iconPosition === 'left' ? `flex-start` : `space-between`}; padding: 12px 16px; user-select: auto; width: 100%; &:hover { background: ${props => props.expanded ? getColor(Colors.SidebarActive) : getColor(Colors.SidebarBg)}; } ${baseFocusStyles()} ` const CollapsibleContent = styled.div` display: flex; flex-direction: column; flex: 1; flex-basis: auto; overflow: hidden; background: ${getColor(Colors.White)}; padding-left: ${props => (props.iconPosition === 'left' ? 28 : 0)}px; ${props => props.cssOverrides} ` const getChildrenHeights = (node: HTMLDivElement) => { let h = 0 if (node.hasChildNodes()) { // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < node.children.length; i++) { h = h + node.children[i].scrollHeight } } return h } /* * Types */ /** * Create a Collapsible * * @param header - header content * @param content - collapsible content * @param expanded? - collapsible content is expanded/visible, default is false * @param onClick? - any extra functionality that should happen on header click * (`isExpanded` is passed to the function passed in for outside * control of expanded state) * @param iconPosition? - header icon position (left or right) * @param iconCss? - any css overrides for the icon * @param maxDuration? - max duration for transition in ms * @param msPerPx? - ms / px for transition * @param contentCss? - any css overrides for the content container * @param clickTarget? - hit target for expand/collapse action (header or icon) */ export type IconPosition = 'left' | 'right' export type ClickTarget = 'header' | 'icon' export type CollapsibleProps = { header: ReactNode content: ReactNode expanded?: boolean onClick?: ( event: React.MouseEvent, isExpanded: boolean, ) => void iconPosition?: IconPosition iconCss?: SimpleInterpolation contentCss?: SimpleInterpolation labelId?: string sectionId?: string maxDuration?: number msPerPx?: number clickTarget?: ClickTarget isContentScrollable?: boolean defaultOpen?: boolean } type CollapsibleContentProps = { iconPosition: IconPosition cssOverrides?: SimpleInterpolation } /* * Components */ export const Collapsible: FC = (props: CollapsibleProps) => { const { header, content, expanded, onClick, iconPosition = 'left', iconCss, labelId, sectionId, maxDuration = 300, contentCss, msPerPx = 4, clickTarget = 'header', isContentScrollable = false, defaultOpen = false, ...domProps } = props const [localExpanded, setLocalExpanded] = useState(defaultOpen) const [contentHeight, setContentHeight] = useState(0) const contentRef = useRef(null) const isExpanded = pipe( fromNullable(expanded), getOrElse(() => localExpanded), ) useEffect(() => { if (isExpanded && contentRef?.current) { setContentHeight(getChildrenHeights(contentRef.current)) } }, [isExpanded, content]) const renderIcon = () => { const icon = iconPosition === 'left' ? isExpanded ? 'arrow_drop_down' : 'arrow_right' : isExpanded ? 'expand_less' : 'expand_more' return clickTarget === 'header' ? ( ) : ( ) } const handleToggleExpand = ( event: React.MouseEvent, ) => pipe( fromNullable(onClick), fold( () => setLocalExpanded(!isExpanded), parentOnClick => pipe( fromNullable(expanded), fold( () => setLocalExpanded(!isExpanded), _ => parentOnClick(event, !isExpanded), ), ), ), ) return ( {clickTarget === 'header' ? ( {iconPosition === 'left' && renderIcon()} {header} {iconPosition !== 'left' && renderIcon()} ) : ( <> {iconPosition === 'left' && renderIcon()} {header} {iconPosition !== 'left' && renderIcon()} )}
{content}
) }