'use client'; import * as React from 'react'; import { View } from 'react-native'; import { useId } from '../../hooks/useId'; import { useStableCallback } from '../../hooks/useStableCallback'; import { useRenderElement } from '../../use-render/useRenderElement'; import { useCollapsibleRoot } from '../../collapsible/root/useCollapsibleRoot'; import type { CollapsibleRoot, CollapsibleRootState } from '../../collapsible/root/CollapsibleRoot'; import { CollapsibleRootContext } from '../../collapsible/root/CollapsibleRootContext'; import type { AccordionRootState } from '../root/AccordionRoot'; import { useAccordionRootContext } from '../root/AccordionRootContext'; import type { ZestUIComponentProps } from '../../types'; import type { ZestChangeEventDetails } from '../../utils/createChangeEventDetails'; import type { REASONS } from '../../utils/reasons'; import { AccordionItemContext } from './AccordionItemContext'; /** * Groups an accordion header with the corresponding panel. * Renders a ``. * * Each item owns a Collapsible root context, so `Accordion.Trigger` and * `Accordion.Panel` are the Collapsible parts wearing accordion semantics. */ export function AccordionItem(componentProps: AccordionItem.Props) { const { className, disabled: disabledProp = false, onOpenChange: onOpenChangeProp, render, style, value: valueProp, ref, ...elementProps } = componentProps; const { disabled: contextDisabled, handleValueChange, state: rootState, value: openValues, } = useAccordionRootContext(); const fallbackValue = useId(); const value = (valueProp ?? fallbackValue) as Value; const disabled = disabledProp || contextDisabled; const isOpen = React.useMemo(() => { if (!openValues) { return false; } for (let i = 0; i < openValues.length; i += 1) { if (openValues[i] === value) { return true; } } return false; }, [openValues, value]); const onOpenChange = useStableCallback( (nextOpen: boolean, eventDetails: CollapsibleRoot.ChangeEventDetails) => { onOpenChangeProp?.(nextOpen, eventDetails); if (eventDetails.isCanceled) { return; } handleValueChange(value, nextOpen, eventDetails); }, ); const collapsible = useCollapsibleRoot({ open: isOpen, onOpenChange, disabled }); const collapsibleState: CollapsibleRootState = React.useMemo( () => ({ open: collapsible.open, disabled: collapsible.disabled, transitionStatus: collapsible.transitionStatus, }), [collapsible.open, collapsible.disabled, collapsible.transitionStatus], ); const collapsibleContext: CollapsibleRootContext = React.useMemo( () => ({ ...collapsible, onOpenChange, state: collapsibleState }), [collapsible, collapsibleState, onOpenChange], ); const state: AccordionItemState = React.useMemo( () => ({ ...rootState, hidden: !isOpen && !collapsible.mounted, disabled, open: isOpen, }), [collapsible.mounted, disabled, isOpen, rootState], ); const triggerId = useId(); const accordionItemContext: AccordionItemContext = React.useMemo( () => ({ open: isOpen, state, triggerId }), [isOpen, state, triggerId], ); const element = useRenderElement(View, componentProps, { state, ref, props: elementProps, }); return ( {element} ); } export interface AccordionItemState extends AccordionRootState { /** * Whether the accordion item's panel is currently hidden. */ hidden: boolean; /** * Whether the component is open. */ open: boolean; } export interface AccordionItemProps extends Omit>, 'value'> { /** * A unique value that identifies this accordion item. * If no value is provided, a unique ID will be generated automatically. */ value?: Value | undefined; /** * Whether the component should ignore user interaction. * @default false */ disabled?: boolean | undefined; /** * Event handler called when the panel is opened or closed. */ onOpenChange?: | ((open: boolean, eventDetails: AccordionItem.ChangeEventDetails) => void) | undefined; } export type AccordionItemChangeEventReason = typeof REASONS.triggerPress | typeof REASONS.none; export type AccordionItemChangeEventDetails = ZestChangeEventDetails; export namespace AccordionItem { export type State = AccordionItemState; export type Props = AccordionItemProps; export type ChangeEventReason = AccordionItemChangeEventReason; export type ChangeEventDetails = AccordionItemChangeEventDetails; }