'use client'; import * as React from 'react'; import { View } from 'react-native'; import { useControlled } from '../../hooks/useControlled'; import { useStableCallback } from '../../hooks/useStableCallback'; import { useRenderElement } from '../../use-render/useRenderElement'; import type { ZestUIComponentProps } from '../../types'; import type { ZestChangeEventDetails } from '../../utils/createChangeEventDetails'; import type { REASONS } from '../../utils/reasons'; import { AccordionRootContext } from './AccordionRootContext'; /** * Groups all parts of the accordion. * Renders a ``. * * Upstream's `orientation` and `loopFocus` props are omitted: both are * deprecated there (roving focus was removed) and neither has meaning without * a keyboard. */ export function AccordionRoot(componentProps: AccordionRoot.Props) { const { className, defaultValue: defaultValueProp, disabled = false, keepMounted = false, multiple = false, onValueChange, render, style, value: valueProp, ref, ...elementProps } = componentProps; const normalizeValue = React.useCallback((val: any) => { if (val === undefined || val === null) return undefined; if (Array.isArray(val)) return val; return [val]; }, []); const normalizedValueProp = React.useMemo( () => normalizeValue(valueProp), [valueProp, normalizeValue], ); const normalizedDefaultValueProp = React.useMemo( () => normalizeValue(defaultValueProp), [defaultValueProp, normalizeValue], ); // Memoized to allow omitting both defaultValue and value, which would // otherwise trigger a warning in useControlled. const defaultValue = React.useMemo(() => { if (valueProp === undefined) { return normalizedDefaultValueProp ?? []; } return undefined; }, [valueProp, normalizedDefaultValueProp]); const [value, setValue] = useControlled>({ controlled: normalizedValueProp, default: defaultValue, name: 'Accordion', state: 'value', }); const handleValueChange = useStableCallback( (newValue: Value, nextOpen: boolean, eventDetails: AccordionRoot.ChangeEventDetails) => { if (!multiple) { const nextValue = value[0] === newValue ? [] : [newValue]; onValueChange?.(nextValue, eventDetails); if (eventDetails.isCanceled) { return; } setValue(nextValue); } else if (nextOpen) { const nextOpenValues = value.slice(); nextOpenValues.push(newValue); onValueChange?.(nextOpenValues, eventDetails); if (eventDetails.isCanceled) { return; } setValue(nextOpenValues); } else { const nextOpenValues = value.filter((v) => v !== newValue); onValueChange?.(nextOpenValues, eventDetails); if (eventDetails.isCanceled) { return; } setValue(nextOpenValues); } }, ); const state: AccordionRootState = React.useMemo( () => ({ value, disabled }), [value, disabled], ); const contextValue: AccordionRootContext = React.useMemo( () => ({ disabled, handleValueChange, keepMounted, state, value }), [disabled, handleValueChange, keepMounted, state, value], ); const element = useRenderElement(View, componentProps, { state, ref, props: elementProps, }); return ( {element} ); } export type AccordionValue = Value[]; export interface AccordionRootState { /** * The current value. */ value: AccordionValue; /** * Whether the component should ignore user interaction. */ disabled: boolean; } export interface AccordionRootProps extends Omit>, 'value'> { /** * The controlled value of the item(s) that should be expanded. * Accepts a single value or an array of values. * * To render an uncontrolled accordion, use the `defaultValue` prop instead. */ value?: Value | AccordionValue | undefined; /** * The uncontrolled value of the item(s) that should be initially expanded. * Accepts a single value or an array of values. * * To render a controlled accordion, use the `value` prop instead. */ defaultValue?: Value | AccordionValue | undefined; /** * Whether the component should ignore user interaction. * @default false */ disabled?: boolean | undefined; /** * Whether to keep the element rendered while the panel is closed. * @default false */ keepMounted?: boolean | undefined; /** * Event handler called when an accordion item is expanded or collapsed. * Provides the new value as an argument. */ onValueChange?: | ((value: AccordionValue, eventDetails: AccordionRoot.ChangeEventDetails) => void) | undefined; /** * Whether multiple items can be open at the same time. * @default false */ multiple?: boolean | undefined; } export type AccordionRootChangeEventReason = typeof REASONS.triggerPress | typeof REASONS.none; export type AccordionRootChangeEventDetails = ZestChangeEventDetails; export namespace AccordionRoot { export type Value = AccordionValue; export type State = AccordionRootState; export type Props = AccordionRootProps; export type ChangeEventReason = AccordionRootChangeEventReason; export type ChangeEventDetails = AccordionRootChangeEventDetails; }