import React, { useState, forwardRef, useEffect, useMemo } from 'react'; import { View, TouchableOpacity, LayoutChangeEvent } from 'react-native'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated'; import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'; import { accordionStyles } from './Accordion.styles'; import Text from '../Text'; import type { AccordionProps, AccordionItem as AccordionItemType } from './types'; import { getNativeAccessibilityProps } from '../utils/accessibility'; import type { IdealystElement } from '../utils/refTypes'; interface AccordionItemProps { item: AccordionItemType; isExpanded: boolean; onToggle: () => void; size: AccordionProps['size']; type: AccordionProps['type']; isLast: boolean; testID?: string; } const AccordionItem: React.FC = ({ item, isExpanded, onToggle, size, type, isLast, testID, }) => { const contentHeight = useSharedValue(0); const iconRotation = useSharedValue(0); const [measuredHeight, setMeasuredHeight] = useState(0); // Apply item-specific variants (for size, expanded, disabled) accordionStyles.useVariants({ size, expanded: isExpanded, disabled: Boolean(item.disabled), }); // Get dynamic styles - call as functions to get theme-reactive styles const itemStyle = (accordionStyles.item as any)({ type, isLast }); const headerStyle = (accordionStyles.header as any)({}); const iconStyle = (accordionStyles.icon as any)({}); const titleStyle = (accordionStyles.title as any)({}); const contentInnerStyle = (accordionStyles.contentInner as any)({}); // Animate height and icon rotation when expanded state changes useEffect(() => { contentHeight.value = withTiming( isExpanded ? measuredHeight : 0, { duration: 250, easing: Easing.bezier(0.4, 0.0, 0.2, 1), // Material Design standard easing } ); iconRotation.value = withTiming( isExpanded ? 180 : 0, { duration: 200, easing: Easing.bezier(0.4, 0.0, 0.2, 1), } ); }, [isExpanded, measuredHeight]); const animatedContentStyle = useAnimatedStyle(() => ({ height: contentHeight.value, overflow: 'hidden', })); const animatedIconStyle = useAnimatedStyle(() => ({ transform: [{ rotate: `${iconRotation.value}deg` }], })); const handleContentLayout = (event: LayoutChangeEvent) => { const { height } = event.nativeEvent.layout; if (height > 0 && height !== measuredHeight) { setMeasuredHeight(height); } }; return ( {item.title} {/* Hidden view for measuring content height */} {typeof item.content === 'string' ? ( {item.content} ) : ( item.content )} {/* Animated visible content */} {typeof item.content === 'string' ? ( {item.content} ) : ( item.content )} ); }; const Accordion = forwardRef(({ items, allowMultiple = false, defaultExpanded = [], type = 'standard', size = 'md', // Spacing variants from ContainerStyleProps gap, padding, paddingVertical, paddingHorizontal, margin, marginVertical, marginHorizontal, style, testID, id, // Accessibility props accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole, }, ref) => { const [expandedItems, setExpandedItems] = useState(defaultExpanded); // Generate native accessibility props const nativeA11yProps = useMemo(() => { return getNativeAccessibilityProps({ accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole: accessibilityRole ?? 'none', }); }, [accessibilityLabel, accessibilityHint, accessibilityDisabled, accessibilityHidden, accessibilityRole]); // Apply variants accordionStyles.useVariants({ type, size, gap, padding, paddingVertical, paddingHorizontal, margin, marginVertical, marginHorizontal, }); const toggleItem = (itemId: string, disabled?: boolean) => { if (disabled) return; setExpandedItems((prev) => { const isExpanded = prev.includes(itemId); if (allowMultiple) { return isExpanded ? prev.filter((id) => id !== itemId) : [...prev, itemId]; } else { return isExpanded ? [] : [itemId]; } }); }; const containerStyle = (accordionStyles.container as any)({}); return ( {items.map((item, index) => ( toggleItem(item.id, item.disabled)} size={size} type={type} isLast={index === items.length - 1} testID={`${testID}-item-${item.id}`} /> ))} ); }); Accordion.displayName = 'Accordion'; export default Accordion;