import React, { useCallback, useMemo, useRef, useState } from 'react'; import { View, Pressable, Animated } from 'react-native'; import { useFirstFocusReadyContext, usePaywallContext } from '../../context/PaywallContext'; import { applyStyles, childSpacingStyle, focusedStyleOverrides, parseSizeOrPercent } from '../../utils/styles'; import { collapseHeaderA11yProps, collapseBodyA11yProps } from '../../utils/rendering'; import { TemplateRenderer } from '../TemplateRenderer'; import type { TComponent, TCollapseContainer } from '@namiml/sdk-core'; import { FocusedStyleProvider, FocusScope, useFocusableState, useFocusEnabled, useRegisterPreferredFocus } from '../../context/FocusContext'; import { useTVPreferredFocus } from '../../utils/tvFocus'; interface Props { component: TCollapseContainer; scaleFactor: number; onClose?: () => void; parentDirection?: string; } export const NamiCollapseContainer: React.FC = ({ component, scaleFactor, onClose, parentDirection }) => { const ctx = usePaywallContext(); const focusReadyCtx = useFirstFocusReadyContext(); const [focusWithin, setFocusWithin] = useState(false); const focusEnabled = useFocusEnabled(); const initialFocus = Boolean((component as any).focused); const { isFocused: selfFocused, onFocus, onBlur } = useFocusableState( initialFocus, focusEnabled, ); const isFocused = selfFocused || focusWithin; const collapseId = component.id ?? ''; const openIds = (ctx.state as any).openHeaderIds ?? []; const defaultOpen = component.open === 'open'; const isOpen = openIds.includes(collapseId) || (defaultOpen && !openIds.length); const animValue = useRef(new Animated.Value(isOpen ? 1 : 0)).current; const pressableRef = useRef(null); useTVPreferredFocus( pressableRef, focusEnabled && initialFocus, ); useRegisterPreferredFocus( pressableRef.current, focusEnabled && initialFocus, ); const toggle = useCallback(() => { ctx.setOpenHeaderIds(collapseId); Animated.timing(animValue, { toValue: isOpen ? 0 : 1, duration: 250, useNativeDriver: false, }).start(); }, [collapseId, isOpen, animValue, ctx]); const handleFocus = useCallback(() => { onFocus(); focusReadyCtx.notifyFirstFocusReady( ctx.state.selectedPaywall?.id ?? 'unknown', ctx.state.currentPage ?? 'unknown', ctx.state.formFactor, ); }, [focusReadyCtx, onFocus, ctx.state.selectedPaywall?.id, ctx.state.currentPage, ctx.state.formFactor]); const containerStyle = useMemo(() => { const base = applyStyles(component, scaleFactor, false, parentDirection); if (isFocused) { Object.assign(base, focusedStyleOverrides(component, scaleFactor)); } return base; }, [component, scaleFactor, isFocused, parentDirection]); // TCollapseContainer has `collapseHeader: TContainer` (single container for header) // and `components: TComponent[]` for the body const headerContainer = component.collapseHeader; const bodyComponents = component.components ?? []; const direction = component.direction ?? 'vertical'; const maxHeight = animValue.interpolate({ inputRange: [0, 1], outputRange: [0, 1000], }); const bodySizeStyle: any = {}; const bodyW = parseSizeOrPercent(component.width ?? component.fixedWidth, scaleFactor); const bodyH = parseSizeOrPercent(component.height ?? component.fixedHeight, scaleFactor); if (bodyW != null) bodySizeStyle.width = bodyW as any; if (bodyH != null) bodySizeStyle.height = bodyH as any; const headerSpacing = childSpacingStyle(1, { spacing: component.spacing, direction: component.direction }, scaleFactor); return ( [{ opacity: pressed ? 0.7 : 1 }]} > {headerContainer ? ( ) : null} {bodyComponents.map((child: TComponent, i: number) => ( ))} ); };