/** * Copyright (c) Paymium. * * This source code is licensed under the MIT license found in the * LICENSE file in the root of this projects source tree. */ import { composeStyles, createStyles, CrossedMethods, inlineStyle, } from '@crossed/styled'; import { createContext, forwardRef, type MutableRefObject, useCallback, useContext, useEffect, useRef, } from 'react'; import { ChevronDown } from '@crossed/unicons'; import { ScrollView, ScrollViewProps, View, type ViewProps, } from 'react-native'; import { useUncontrolled, withStaticProperties } from '@crossed/core'; import { Floating, FloatingTriggerProps } from '../overlay/Floating'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated'; import { useFloatingContext } from '../overlay/Floating/context'; const accordionStyles = createStyles((t) => ({ root: { base: { borderBottomWidth: 1, borderColor: t.colors.border.primary, borderStyle: 'solid', }, }, trigger: { base: { flexDirection: 'row', justifyContent: 'space-between', }, }, panel: { web: { base: { overflow: 'hidden', transition: 'height 170ms ease-out' }, }, }, item: { base: { borderTopWidth: 1, borderColor: t.colors.border.primary, borderStyle: 'solid', }, web: { base: { transition: 'height 1000ms ease' }, }, }, })); export type AccordionProps = ViewProps & { allowMultiple?: boolean; defaultValues?: string[]; values?: string[]; onChange?: (_p: string[]) => void; }; export type AccordionRootContext = Required< Pick > & { setValues: (_value: string[]) => void; }; const rootContext = createContext( {} as AccordionRootContext ); export type AccordionItemProps = ViewProps & { value: string }; export type ItemContext = Pick & { buttonId: MutableRefObject; panelId: MutableRefObject; }; const itemContext = createContext({} as ItemContext); const Root = (props: AccordionProps) => { const { children, allowMultiple = false, defaultValues, values: valueProps, onChange, } = props; const [values, setValues] = useUncontrolled({ defaultValue: defaultValues ?? [], value: valueProps, onChange, }); return ( {children} ); }; Root.displayName = 'Accordion'; const AccordionItem = ({ children, value }: AccordionItemProps) => { const { setValues, values, allowMultiple } = useContext(rootContext); const buttonId = useRef(); const panelId = useRef(); const handleChange = useCallback(() => { setValues( allowMultiple ? values.includes(value) ? values.filter((e) => e !== value) : [...values, value] : [value] ); }, [setValues, value, values, allowMultiple]); return ( {children} ); }; AccordionItem.displayName = 'Accordion.Item'; export type AccordionTriggerProps = FloatingTriggerProps; const AccordionTrigger = forwardRef( ({ style, ...props }, ref) => ( ) ); AccordionTrigger.displayName = 'Accordion.Trigger'; /** * AccordionPanelProps defines the properties for the AccordionPanel component. * * The AccordionPanel component is typically used within an Accordion component * to display a collapsible and expandable content panel. This type extends * React's PropsWithChildren, allowing it to include children components or elements. * * Properties: * @typedef {Object} AccordionPanelProps * * @property {CrossedMethods} [style] - An optional style property that allows * for the application of custom styling methods. This style attribute can be * manipulated using methods defined in CrossedMethods, which might include utility * methods for advanced styling techniques or conditional styles based on theme * or state. */ export type AccordionPanelProps = Omit & { style?: CrossedMethods; }; const AccordionPanel = ({ children, style, ...props }: AccordionPanelProps) => { const openSharedValue = useSharedValue(false); const { open } = useFloatingContext(); const height = useSharedValue(0); const handleLayout = useCallback< NonNullable >( (_w, h) => { height.value = h; }, [height] ); useEffect(() => { openSharedValue.value = open; }, [open]); const animatedStyle = useAnimatedStyle(() => { return { height: withTiming(openSharedValue.value ? height.value : 0), }; }, [height, openSharedValue]); return ( ({ base: { position: 'relative' }, })), style )} > {children} ); }; AccordionPanel.displayName = 'Accordion.Panel'; const AccordionIcon = ({ style }: { style?: CrossedMethods }) => { const { value } = useContext(itemContext); const { values } = useContext(rootContext); const isOpen = values.includes(value); const animatedStyle = useAnimatedStyle(() => { return { transform: [{ rotate: withTiming(isOpen ? '180deg' : '0deg') }], }; }, [isOpen]); return ( ); }; AccordionIcon.displayName = 'Accordion.Icon'; export const Accordion = withStaticProperties(Root, { Item: AccordionItem, Panel: AccordionPanel, Trigger: AccordionTrigger, Icon: AccordionIcon, }); export { AccordionItem, AccordionPanel, AccordionTrigger, AccordionIcon };