Компоненты/Аккордеон

Аккордеон

31 января 2025

Настраиваемый аккордеон с плавным раскрытием и сворачиванием

Установка

npx astra-rn add accordion
pnpm dlx astra-rn add accordion
yarn dlx astra-rn add accordion
bunx --bun astra-rn add accordion

Файлы

index.tsx
Назад3D-карусельДалееРельс действий

Вручную

Поставьте зависимости:

npm install @expo/vector-icons expo-blur expo-haptics react-native-reanimated
pnpm add @expo/vector-icons expo-blur expo-haptics react-native-reanimated
yarn add @expo/vector-icons expo-blur expo-haptics react-native-reanimated
bun add @expo/vector-icons expo-blur expo-haptics react-native-reanimated

Скопируйте эти файлы в src/shared/components/molecules/accordion/:

Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.

Использование

import { View, Text, StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import { SymbolView } from "expo-symbols";
import { Accordion } from "@/shared/components/molecules/accordion";
import { Fragment, useRef, useState } from "react";
import { Stack } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "title", label: "Заголовок", type: "text", default: "FAQ", maxLength: 30 },
  { key: "subtitle", label: "Подзаголовок", type: "text", default: "Common questions", maxLength: 40 },
  { key: "cardColor", label: "Фон карточки", type: "color", default: "#141414" },
  { key: "borderColor", label: "Цвет рамки", type: "color", default: "#222222" },
  { key: "questionColor", label: "Цвет вопроса", type: "color", default: "#ffffff" },
  { key: "answerColor", label: "Цвет ответа", type: "color", default: "#888888" },
  { key: "chevronColor", label: "Цвет стрелки", type: "color", default: "#666666" },
  { key: "symbolColor", label: "Цвет иконок", type: "color", default: "#888888" },
  { key: "icon", label: "Индикатор", type: "select", options: [{ value: "chevron", label: "Стрелка" }, { value: "cross", label: "Крестик" }], default: "chevron" },
  { key: "spacing", label: "Отступ", type: "slider", min: 0, max: 24, step: 1, unit: "px", default: 8 },
  { key: "questionSize", label: "Размер вопроса", type: "slider", min: 12, max: 22, step: 1, unit: "px", default: 15 },
  { key: "pop", label: "Пружинка", type: "toggle", default: false },
  { key: "step", label: "Шаг автопоказа", type: "slider", min: 400, max: 3000, step: 50, unit: "ms", default: 900 },
] as const;

const FAQS = [
  {
    id: "1",
    question: "How do I get started?",
    answer:
      "Simply create an account and follow the onboarding steps. It only takes a minute.",
    icon: "questionmark.circle.fill",
  },
  {
    id: "2",
    question: "Is my data secure?",
    answer:
      "Yes, we use end-to-end encryption and never share your data with third parties.",
    icon: "lock.fill",
  },
  {
    id: "3",
    question: "Can I cancel anytime?",
    answer:
      "Absolutely. No contracts, no hidden fees. Cancel whenever you want.",
    icon: "xmark.circle.fill",
  },
];

export default function App() {
  const [fontLoaded] = useFonts({
    SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"),
    SfProRoundedBold: require("~/assets/fonts/SF-Pro-Rounded-Bold.otf"),
    SfProRoundedMedium: require("~/assets/fonts/SF-Pro-Rounded-Medium.otf"),
  });

  const custom = useCustomize(CUSTOMIZE);
  const stepRef = useRef<number>(custom.step);
  stepRef.current = custom.step;

  const darkTheme = {
    backgroundColor: custom.cardColor,
    borderColor: custom.borderColor,
    textColor: custom.questionColor,
    iconColor: custom.chevronColor,
  };

  const insets = useSafeAreaInsets();
  const [openItems, setOpenItems] = useState<string[]>([]);

  useAutoplay(async ({ wait }) => {
    setOpenItems([]);
    await wait(500);
    for (const faq of FAQS) {
      setOpenItems([faq.id]);
      await wait(stepRef.current);
    }
    await wait(400);
  });

  return (
    <Fragment>
      <Stack.Screen
        options={{
          headerBackVisible: true,
          headerShown: true,
          headerTitle: "",
          headerTransparent: true,
        }}
      />
      <GestureHandlerRootView
        style={[
          styles.container,
          {
            paddingTop: insets.top,
          },
        ]}
      >
        <StatusBar style="light" />

        <View style={styles.content}>
          <Text
            style={[
              styles.title,
              fontLoaded && { fontFamily: "SfProRoundedBold" },
            ]}
          >
            {custom.title}
          </Text>
          <Text
            style={[
              styles.subtitle,
              fontLoaded && { fontFamily: "SfProRounded" },
            ]}
          >
            {custom.subtitle}
          </Text>

          <View style={styles.accordionWrapper}>
            <Accordion
              theme={{
                backgroundColor: darkTheme.backgroundColor,
                borderColor: darkTheme.borderColor,
                iconColor: darkTheme.iconColor,
                headlineColor: darkTheme.textColor,
                subtitleColor: darkTheme.textColor,
              }}
              spacing={custom.spacing}
              value={openItems}
              onValueChange={setOpenItems}
            >
              {FAQS.map((faq) => (
                <Accordion.Item
                  key={faq.id}
                  value={faq.id}
                  icon={custom.icon as "chevron" | "cross"}
                  pop={custom.pop}
                >
                  <Accordion.Trigger>
                    <View style={styles.triggerContent}>
                      <SymbolView
                        name={faq.icon as any}
                        size={18}
                        tintColor={custom.symbolColor}
                      />
                      <Text
                        style={[
                          styles.question,
                          {
                            color: custom.questionColor,
                            fontSize: custom.questionSize,
                          },
                          fontLoaded && { fontFamily: "SfProRoundedMedium" },
                        ]}
                      >
                        {faq.question}
                      </Text>
                    </View>
                  </Accordion.Trigger>
                  <Accordion.Content>
                    <Text
                      style={[
                        styles.answer,
                        { color: custom.answerColor },
                        fontLoaded && { fontFamily: "SfProRounded" },
                      ]}
                    >
                      {faq.answer}
                    </Text>
                  </Accordion.Content>
                </Accordion.Item>
              ))}
            </Accordion>
          </View>
        </View>
      </GestureHandlerRootView>
    </Fragment>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0a",
  },
  content: {
    flex: 1,
    paddingHorizontal: 20,
    justifyContent: "center",
  },
  title: {
    fontSize: 28,
    fontWeight: "700",
    color: "#fff",
    marginBottom: 4,
  },
  subtitle: {
    fontSize: 15,
    color: "#555",
    marginBottom: 32,
  },
  accordionWrapper: {
    gap: 8,
  },
  triggerContent: {
    flexDirection: "row",
    alignItems: "center",
    gap: 12,
    flex: 1,
  },
  question: {
    fontSize: 15,
    fontWeight: "600",
    color: "#fff",
    flex: 1,
  },
  answer: {
    fontSize: 14,
    color: "#888",
    lineHeight: 22,
  },
});

Пропсы

Пропс
Описание
children

Тип React.ReactNode

type?

Тип AccordionType

theme?

Тип AccordionTheme

spacing?

Тип number

value?

Тип string[]

onValueChange?

Тип (value: string[]) => void

Необязательные value (массив открытых value пунктов) и onValueChange делают аккордеон управляемым — открытием можно управлять из кода. Без value аккордеон сам хранит состояние, как раньше.

AccordionItemProps

Пропс
Описание
children

Тип React.ReactNode

value

Тип string

pop?

Тип boolean

icon?

Тип AccordionIcons

popScale?

Тип number

isLast?

Тип boolean