Accordion

Next

A accordion component for React Native applications.

Docs
nativeuiui

Preview

Loading preview…
registry/accordion/accordion.tsx
import * as React from 'react';
import { Pressable, View, Text, LayoutAnimation, Platform, UIManager } from 'react-native';
import { Feather } from '@expo/vector-icons';
import { cn } from '@/lib/utils';

// Enable layout animation for Android
if (Platform.OS === 'android') {
  if (UIManager.setLayoutAnimationEnabledExperimental) {
    UIManager.setLayoutAnimationEnabledExperimental(true);
  }
}

interface AccordionContextValue {
  value: string[];
  onValueChange: (itemValue: string) => void;
  type: 'single' | 'multiple';
}

const AccordionContext = React.createContext<AccordionContextValue | null>(null);

export interface AccordionProps {
  type?: 'single' | 'multiple';
  collapsible?: boolean;
  value?: string[];
  onValueChange?: (value: string[]) => void;
  defaultValue?: string[];
  className?: string;
  children: React.ReactNode;
}

const Accordion = ({
  type = 'single',
  collapsible = false,
  value,
  onValueChange,
  defaultValue,
  className,
  children,
}: AccordionProps) => {
  const [state, setState] = React.useState<string[]>(value || defaultValue || []);

  const isControlled = value !== undefined;
  const accordionValue = isControlled ? value : state;

  const handleValueChange = React.useCallback((itemValue: string) => {
    const isSelected = accordionValue.includes(itemValue);

    let newValue: string[] = [];

    if (type === 'single') {
      if (isSelected) {
        newValue = collapsible ? [] : [itemValue];
      } else {
        newValue = [itemValue];
      }
    } else {
      if (isSelected) {
        newValue = accordionValue.filter((v) => v !== itemValue);
      } else {
        newValue = [...accordionValue, itemValue];
      }
    }

    if (!isControlled) {
      setState(newValue);
    }

    onValueChange?.(newValue);
  }, [accordionValue, collapsible, isControlled, onValueChange, type]);

  return (
    <AccordionContext.Provider value={{ value: accordionValue, onValueChange: handleValueChange, type }}>
      <View className={cn("w-full", className)}>
        {children}
      </View>
    </AccordionContext.Provider>
  );
};

interface AccordionItemProps {
  value: string;
  className?: string;
  children: React.ReactNode;
}

const AccordionItem = ({ value, className, children }: AccordionItemProps) => {
  const context = React.useContext(AccordionContext);

  if (!context) {
    throw new Error('AccordionItem must be used within an Accordion');
  }

  const isExpanded = context.value.includes(value);

  return (
    <View className={cn("border-b border-border", className)}>
      {React.Children.map(children, (child) => {
        if (React.isValidElement(child)) {
          return React.cloneElement(child as React.ReactElement<any>, {
            value,
            isExpanded,
          });
        }
        return child;
      })}
    </View>
  );
};

interface AccordionTriggerProps {
  className?: string;
  children: React.ReactNode;
  value?: string;
  isExpanded?: boolean;
}

const AccordionTrigger = ({
  className,
  children,
  value,
  isExpanded,
}: AccordionTriggerProps) => {
  const context = React.useContext(AccordionContext);

  if (!context || value === undefined) {
    return null;
  }

  const iconRotation = isExpanded ? 180 : 0;

  const handlePress = () => {
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    context.onValueChange(value);
  };

  return (
    <Pressable
      onPress={handlePress}
      className={cn(
        "flex-row items-center justify-between py-4",
        className
      )}
      accessibilityRole="button"
      accessibilityState={{ expanded: isExpanded }}
      accessibilityHint="Toggle accordion section"
    >
      <View className="flex-1">
        {typeof children === 'string' ? (
          <Text className="text-base font-medium text-foreground">{children}</Text>
        ) : (
          children
        )}
      </View>
      <View style={{ transform: [{ rotate: `${iconRotation}deg` }] }}>
        <Feather name="chevron-down" size={20} color="#888" />
      </View>
    </Pressable>
  );
};

interface AccordionContentProps {
  className?: string;
  children: React.ReactNode;
  value?: string;
  isExpanded?: boolean;
}

const AccordionContent = ({
  className,
  children,
  value,
  isExpanded,
}: AccordionContentProps) => {
  if (!isExpanded) {
    return null;
  }

  return (
    <View
      className={cn("pb-4 pt-0", className)}
    >
      {typeof children === 'string' ? (
        <Text className="text-base text-muted-foreground">{children}</Text>
      ) : (
        children
      )}
    </View>
  );
};

Accordion.displayName = 'Accordion';
AccordionItem.displayName = 'AccordionItem';
AccordionTrigger.displayName = 'AccordionTrigger';
AccordionContent.displayName = 'AccordionContent';

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 

Installation

npx shadcn@latest add @nativeui/accordion

Usage

import { Accordion } from "@/components/ui/accordion"
<Accordion />
Accordion - shadcn index