import { forwardRef, useMemo, type ReactNode } from 'react'; import { Text, View, type TextProps, type ViewProps } from 'react-native'; import { ListItemRoot as ListItemRootPrimitive, Slot, dataAttributes, mergeDataAttributes, type IListItemProps, type ListItemCrossAlign, } from '@cdx-ui/primitives'; import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils'; import { listItemContentVariants, listItemDescriptionVariants, listItemLeadingSlotVariants, listItemMetaVariants, listItemRootVariants, listItemSectionHeaderLabelVariants, listItemSectionHeaderVariants, listItemTitleVariants, listItemTrailingSlotVariants, type ListItemVariantProps, } from './styles'; const SCOPE = 'LIST_ITEM'; const useListItemStyleContext = (): Pick => { const ctx = useStyleContext(SCOPE) as ListItemVariantProps | undefined; return { crossAlign: ctx?.crossAlign ?? 'center' }; }; // ============================================================================= // ROOT // ============================================================================= export interface ListItemProps extends Omit, ListItemVariantProps { className?: string; } const ListItemRoot = forwardRef( ( { size = 'default', showSeparator = true, crossAlign: crossAlignProp = 'center', className, style, ...props }, ref, ) => { const crossAlign: ListItemCrossAlign = crossAlignProp ?? 'center'; const parent = useParentContext(); const ctx = useMemo(() => ({ ...parent, [SCOPE]: { crossAlign } }), [parent, crossAlign]); const computedClassName = cn( listItemRootVariants({ size, showSeparator, crossAlign }), className, ); return ( ); }, ); ListItemRoot.displayName = 'ListItem'; // ============================================================================= // SLOTS & TEXT // ============================================================================= const leadingSlotStyle = { flexShrink: 0 as const }; export interface ListItemLeadingSlotProps extends ViewProps { asChild?: boolean; className?: string; /** * Decorative leading content defaults to hidden from the accessibility tree. * Set to `false` when the leading region contains meaningful controls. * @default true */ 'aria-hidden'?: boolean; } const ListItemLeadingSlot = forwardRef( ({ asChild, className, style, 'aria-hidden': ariaHidden, ...props }, ref) => { const accessibilityElementsHidden = ariaHidden !== false; const Comp = asChild ? Slot : View; return ( ); }, ); ListItemLeadingSlot.displayName = 'ListItem.LeadingSlot'; const contentStyle = { flex: 1, flexDirection: 'column' as const, minWidth: 0, }; export interface ListItemContentProps extends ViewProps { asChild?: boolean; className?: string; } const ListItemContent = forwardRef( ({ asChild, className, style, ...props }, ref) => { const { crossAlign } = useListItemStyleContext(); const Comp = asChild ? Slot : View; return ( ); }, ); ListItemContent.displayName = 'ListItem.Content'; export interface ListItemTitleProps extends TextProps { asChild?: boolean; className?: string; } const ListItemTitle = forwardRef( ({ asChild, className, numberOfLines = 1, style, ...props }, ref) => { const Comp = asChild ? Slot : Text; return ( ); }, ); ListItemTitle.displayName = 'ListItem.Title'; export interface ListItemDescriptionProps extends TextProps { asChild?: boolean; className?: string; } const ListItemDescription = forwardRef( ({ asChild, className, style, ...props }, ref) => { const Comp = asChild ? Slot : Text; return ( ); }, ); ListItemDescription.displayName = 'ListItem.Description'; export interface ListItemMetaProps extends TextProps { asChild?: boolean; className?: string; } const ListItemMeta = forwardRef( ({ asChild, className, numberOfLines = 1, style, ...props }, ref) => { const Comp = asChild ? Slot : Text; return ( ); }, ); ListItemMeta.displayName = 'ListItem.Meta'; export interface ListItemTrailingSlotProps extends ViewProps { asChild?: boolean; className?: string; } const ListItemTrailingSlot = forwardRef( ({ asChild, className, style, ...props }, ref) => { const { crossAlign } = useListItemStyleContext(); const Comp = asChild ? Slot : View; return ( ); }, ); ListItemTrailingSlot.displayName = 'ListItem.TrailingSlot'; const wrapSectionHeaderLabel = (children: ReactNode): ReactNode => { if (typeof children === 'string' || typeof children === 'number') { return {children}; } return children; }; // ============================================================================= // SECTION HEADER // ============================================================================= // TODO: ListItem.SectionHeader doesn't follow composition patterns (composing text, trailing prop, etc). const sectionHeaderRow = { flexDirection: 'row' as const, alignItems: 'center' as const, justifyContent: 'space-between' as const, gap: 8, }; const sectionHeaderLabelCell = { flex: 1, minWidth: 0, /** Keeps the label column from stretching to a taller trailing sibling (e.g. Chip). */ alignSelf: 'center' as const, }; const sectionHeaderTrailingCell = { flexShrink: 0, /** Prevents the trailing slot from stretching vertically and “filling” the row. */ alignSelf: 'center' as const, }; export interface ListItemSectionHeaderProps extends ViewProps { asChild?: boolean; className?: string; /** Primary section label (e.g. date heading). */ children: ReactNode; /** Optional right-aligned adornment (e.g. section Chip). */ trailing?: ReactNode; /** * Whether to expose the top divider marker for styling. * @default true */ showDivider?: boolean; } const ListItemSectionHeader = forwardRef( ( { asChild, children, className, showDivider = true, style, trailing, accessibilityRole = 'header', ...props }, ref, ) => { const Comp = asChild ? Slot : View; return ( {wrapSectionHeaderLabel(children)} {trailing ? {trailing} : null} ); }, ); ListItemSectionHeader.displayName = 'ListItem.SectionHeader'; // ============================================================================= // COMPOUND EXPORT // ============================================================================= type ListItemCompound = typeof ListItemRoot & { LeadingSlot: typeof ListItemLeadingSlot; Content: typeof ListItemContent; Title: typeof ListItemTitle; Description: typeof ListItemDescription; Meta: typeof ListItemMeta; TrailingSlot: typeof ListItemTrailingSlot; SectionHeader: typeof ListItemSectionHeader; }; export const ListItem = Object.assign(ListItemRoot, { LeadingSlot: ListItemLeadingSlot, Content: ListItemContent, Title: ListItemTitle, Description: ListItemDescription, Meta: ListItemMeta, TrailingSlot: ListItemTrailingSlot, SectionHeader: ListItemSectionHeader, }) as ListItemCompound; export type { ListItemCrossAlign, ListItemVariantProps }; export { listItemContentVariants, listItemDescriptionVariants, listItemLeadingSlotVariants, listItemMetaVariants, listItemRootVariants, listItemSectionHeaderLabelVariants, listItemSectionHeaderVariants, listItemTitleVariants, listItemTrailingSlotVariants, } from './styles';