import * as React from 'react' import * as RadixAccordion from '@radix-ui/react-accordion' import { ChevronDown, ChevronRight } from 'lucide-react' import { forwardRef, useCallback, useMemo, useRef } from 'react' import { styled } from 'styled-components' import type { AccordionItemProps } from './types' import { AccordionType, AccordionChevronPosition } from './types' import type { AccordionTokenType } from './accordion.tokens' import Block from '../Primitives/Block/Block' import PrimitiveText from '../Primitives/PrimitiveText/PrimitiveText' import { useResponsiveTokens } from '../../hooks/useResponsiveTokens' import { useBreakpoints } from '../../hooks/useBreakPoints' import { BREAKPOINTS } from '../../breakpoints/breakPoints' import { ChevronAnimation, ChevronAnimationDirection, ChevronAnimationVariant, ChevronAnimationSize, } from '../animations/ChevronAnimation' import { FOUNDATION_THEME } from '../../tokens' const StyledAccordionItem = styled(RadixAccordion.Item)<{ $accordionType: AccordionType $isDisabled: boolean $accordionToken: AccordionTokenType $isSmallScreen: boolean }>((props) => ({ border: props.$accordionToken.trigger.border[props.$accordionType].default, borderRadius: props.$accordionToken.borderRadius[props.$accordionType], overflow: 'visible', ...(props.$isDisabled && props.$accordionType === AccordionType.BORDER && { backgroundColor: props.$accordionToken.trigger.backgroundColor[ props.$accordionType ].disabled, }), ...(props.$isSmallScreen && props.$accordionType === AccordionType.BORDER && { '&:first-child': { borderBottomLeftRadius: 'unset', borderBottomRightRadius: 'unset', }, '&:last-child': { borderTop: 'none', borderTopLeftRadius: 'unset', borderTopRightRadius: 'unset', }, '&:not(:first-child):not(:last-child)': { borderTop: 'none', borderRadius: 'unset', }, }), ...(props.$isSmallScreen && props.$accordionType === AccordionType.NO_BORDER && { '&': { border: 'none', borderRadius: 'unset', borderBottom: props.$accordionToken.trigger.border[props.$accordionType] .default, }, '&:last-child': { ...(props.$isDisabled && { borderBottom: 'none', }), }, }), })) const StyledAccordionHeader = styled(RadixAccordion.Header)({ display: 'flex', }) const StyledAccordionTrigger = styled(RadixAccordion.Trigger)<{ $accordionType: AccordionType $isDisabled: boolean $accordionToken: AccordionTokenType $isSmallScreen: boolean $isFirst?: boolean $isLast?: boolean $isIntermediate?: boolean }>((props) => ({ display: 'flex', width: '100%', textAlign: 'left', transition: 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)', cursor: 'pointer', border: 'none', outline: 'none', padding: props.$accordionToken.trigger.padding[props.$accordionType], borderRadius: props.$accordionToken.borderRadius[props.$accordionType], backgroundColor: props.$accordionToken.trigger.backgroundColor[props.$accordionType] .default, ...(props.$isDisabled && { backgroundColor: props.$accordionToken.trigger.backgroundColor[props.$accordionType] .disabled, cursor: 'not-allowed', }), ...(!props.$isSmallScreen && { '&[data-state="open"]': { backgroundColor: props.$accordionToken.trigger.backgroundColor[ props.$accordionType ].open, ...(props.$accordionType === AccordionType.BORDER && { borderBottomLeftRadius: '0', borderBottomRightRadius: '0', }), }, }), ...(!props.$isSmallScreen && { '&:hover:not(:disabled)': { backgroundColor: props.$accordionToken.trigger.backgroundColor[ props.$accordionType ].hover, }, }), '&:focus-visible': { outline: `2px solid ${FOUNDATION_THEME.colors.primary[500]}`, outlineOffset: FOUNDATION_THEME.unit[2], }, ...(props.$accordionType === AccordionType.NO_BORDER && props.$isSmallScreen && props.$isDisabled && { ...(props.$isFirst && { borderRadius: 'unset', borderTopLeftRadius: props.$accordionToken.borderRadius[props.$accordionType], borderTopRightRadius: props.$accordionToken.borderRadius[props.$accordionType], }), ...(props.$isLast && { borderRadius: 'unset', borderBottomLeftRadius: props.$accordionToken.borderRadius[props.$accordionType], borderBottomRightRadius: props.$accordionToken.borderRadius[props.$accordionType], }), ...(props.$isIntermediate && { borderRadius: 'unset', }), }), })) const StyledAccordionContent = styled(RadixAccordion.Content)<{ $accordionType: AccordionType $accordionToken: AccordionTokenType }>((props) => ({ overflow: 'hidden', transition: 'height 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)', '&[data-state="open"]': { animation: 'accordion-down 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)', ...(props.$accordionType === AccordionType.BORDER && { borderTopLeftRadius: '0', borderTopRightRadius: '0', }), }, '&[data-state="closed"]': { animation: 'accordion-up 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, '@keyframes accordion-down': { from: { height: 0, }, to: { height: 'var(--radix-accordion-content-height)', }, }, '@keyframes accordion-up': { from: { height: 'var(--radix-accordion-content-height)', }, to: { height: 0, }, }, '@media (prefers-reduced-motion: reduce)': { transition: 'none', animation: 'none', }, })) const StyledSeparator = styled.hr<{ $accordionType: AccordionType $accordionToken: AccordionTokenType }>((props) => ({ margin: 0, border: 'none', height: FOUNDATION_THEME.border.width[1], backgroundColor: props.$accordionToken.separator.color[props.$accordionType], })) const AccordionItem = forwardRef< HTMLDivElement, AccordionItemProps & { accordionType?: AccordionType chevronPosition?: AccordionChevronPosition isFirst?: boolean isLast?: boolean isIntermediate?: boolean currentValue?: string | string[] } >( ( { value, title, children, subtext, leftSlot, rightSlot, subtextSlot, triggerSlot, triggerSlotWidth, isDisabled = false, chevronPosition = AccordionChevronPosition.RIGHT, accordionType = AccordionType.NO_BORDER, isFirst, isLast, isIntermediate, currentValue, ...props }, ref ) => { const accordionToken = useResponsiveTokens('ACCORDION') const { breakPointLabel } = useBreakpoints(BREAKPOINTS) const isSmallScreen = breakPointLabel === 'sm' const triggerRef = useRef(null) const isExpanded = useMemo(() => { if (currentValue === undefined) return false return Array.isArray(currentValue) ? currentValue.includes(value) : currentValue === value }, [currentValue, value]) const toggle = useCallback(() => { if (!isDisabled) { triggerRef.current?.click() } }, [isDisabled]) const slotWidth = useMemo(() => { if (triggerSlotWidth !== undefined) { return typeof triggerSlotWidth === 'number' ? `${triggerSlotWidth}px` : triggerSlotWidth } return ( accordionToken.trigger.slot?.maxWidth || FOUNDATION_THEME.unit[24] ) }, [triggerSlotWidth, accordionToken.trigger.slot?.maxWidth]) const hasInteractiveSlot = useMemo(() => { if (!triggerSlot) return false if (typeof triggerSlot === 'function') return true if (!React.isValidElement(triggerSlot)) return false const p = triggerSlot.props as Record return 'onChange' in p || 'onCheckedChange' in p }, [triggerSlot]) const renderedTriggerSlot = useMemo(() => { if (!triggerSlot) return null if (typeof triggerSlot === 'function') { return triggerSlot({ isExpanded, toggle, value, isDisabled }) } if (React.isValidElement(triggerSlot)) { const elementProps = triggerSlot.props as Record< string, unknown > const isInteractive = 'onChange' in elementProps || 'onCheckedChange' in elementProps if (!isInteractive) return triggerSlot const enhanced: Record = { ...elementProps, checked: elementProps.checked ?? isExpanded, disabled: elementProps.disabled ?? isDisabled, } if ('onChange' in elementProps) { const orig = elementProps.onChange as | ((...a: unknown[]) => void) | undefined enhanced.onChange = (...args: unknown[]) => { orig?.(...args) toggle() } } if ('onCheckedChange' in elementProps) { const orig = elementProps.onCheckedChange as | ((...a: unknown[]) => void) | undefined enhanced.onCheckedChange = (...args: unknown[]) => { orig?.(...args) toggle() } } return React.cloneElement( triggerSlot as React.ReactElement, enhanced ) } return triggerSlot }, [triggerSlot, isExpanded, toggle, value, isDisabled]) const getChevronIcon = () => { const iconColor = isDisabled ? FOUNDATION_THEME.colors.gray[300] : FOUNDATION_THEME.colors.gray[500] return ( {chevronPosition === AccordionChevronPosition.RIGHT ? ( ) : ( )} ) } const renderChevronSlot = () => ( ) return ( {chevronPosition === AccordionChevronPosition.LEFT && renderChevronSlot()} {leftSlot && chevronPosition !== AccordionChevronPosition.LEFT && ( )?.['aria-label'] ? undefined : 'true' } > {leftSlot} )} {title} {rightSlot && ( )?.['aria-label'] ? undefined : 'true' } > {rightSlot} )} {(subtext || subtextSlot) && ( {subtext && !isSmallScreen && ( {subtext} )} {subtextSlot && ( {subtextSlot} )} )} {chevronPosition === AccordionChevronPosition.RIGHT && ( {renderChevronSlot()} )} {accordionType === AccordionType.BORDER && ( )} {children} ) } ) AccordionItem.displayName = 'AccordionItem' export default AccordionItem