Skip to content

Commit

Permalink
feat: release @animatereactnative/accordion
Browse files Browse the repository at this point in the history
  • Loading branch information
catalinmiron committed Nov 20, 2024
1 parent 9fdaae4 commit 90a4e35
Show file tree
Hide file tree
Showing 6 changed files with 15,038 additions and 33 deletions.
106 changes: 99 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,125 @@
# @animatereactnative/accordion
<div align="center">
<h1>React Native Accordion</h1>

A React Native Accordion component, powered by React Native Reanimated layout animation that's capable of rendering and animating content that has a dynamic height.
https://github.com/user-attachments/assets/3906847e-c609-4f85-b5e5-b2bb68cef901

[![NPM Version](https://img.shields.io/npm/v/@animatereactnative/accordion.svg?style=flat&color=black)](https://www.npmjs.org/package/@animatereactnative/accordion) [![runs with expo](https://img.shields.io/badge/Runs%20with%20Expo-4630EB.svg?style=flat-square&logo=EXPO&labelColor=f3f3f3&logoColor=000)](https://expo.io/) [![npm](https://img.shields.io/npm/l/@animatereactnative/accordion?style=flat-square)](https://www.npmjs.com/package/@animatereactnative/accordion) [![npm](https://img.shields.io/badge/types-included-blue?style=flat-square)](https://www.npmjs.com/package/@animatereactnative/accordion) <a href="https://twitter.com/mironcatalin"><img src="https://img.shields.io/twitter/follow/mironcatalin?label=Follow @mironcatalin&color=black" alt="Follow Miron Catalin"></a>

</div>

React Native Accordion component, a cross-platform accordion component, powered by Reanimated, that's capable of displaying dynamic height content and animate the layout changes/transitions between Collapsable and Expandable states.

If you are going to use this component along with other components (as siblings), it is recommended to use `Accordion.Sibling` and wrap the Siblings with it. This is because the exposed `Sibling` component will use Layout animations as well so there are no layout shifts or sudden movements, keeping everything smooth.

- 🔋 Powered by Reanimated
- 📱 Works with Expo
- ✅ Cross-platform (iOS, Android, Web - wip)
- ⚡️ 60-120fps
- 🪝 Works with any React Native element/component
- 🎼 Composition ready
- ⌨️ Written in TypeScript

## Installation

```sh
npm install @animatereactnative/accordion
```

## Usage
> Also, you need to install [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated), and follow their installation instructions.
## Usage

```js
import { multiply } from '@animatereactnative/accordion';
import { Accordion } from '@animatereactnative/accordion';

// ...

const result = await multiply(3, 7);
export function Example() {
return (
<Accordion.Accordion>
<Accordion.Header>
<Text>AnimateReactNative.com</Text>
<Accordion.HeaderIcon>
<ChevronUp />
</Accordion.HeaderIcon>
</Accordion.Header>

<Accordion.Collapsed>
<Text>Visible !expanded</Text>
</Accordion.Collapsed>
<Accordion.Always>
<Text>Always visible</Text>
</Accordion.Always>

<Accordion.Expanded>
<Text>Expanded content</Text>
{loading && <ActivityIndicator />}
{data & <MyList data={data} />}
</Accordion.Expanded>
</Accordion.Accordion>
);
}
```

## Props

```
Accordion = {
/**
* The main component that will handle the state of the accordion.
*
* @param isOpen boolean
* @param onChange (value: boolean) => void
*/
Accordion
/**
* The header of the accordion.
*/
Header,
/**
* The component that will wrap any children and it will apply a rotation to it.
*
* @param children
* @param rotation clockwise | counter-clockwise
*/
HeaderIcon
/**
* This is the content that will be displayed when the accordion is open
*/
Expanded,
/**
* This is the content that will be displayed when the accordion is closed
*/
Collapsed,
/**
* This is the content that will always be displayed
*/
Always,
/**
*
* This is a component that will add the layout transition to any
* sibling components. Useful when you are rendering other components
* that are not direct children of the Accordion component.
*/
Sibling,
};
```

## Contributing

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

## License

MIT
[MIT](./LICENSE)

---

Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
<p align="center">
<a href="https://www.animatereactnative.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.animatereactnative.com/animatereactnative_dark.svg">
<img alt="AnimateReactNative.com - Premium and Custom React Native animations." src="https://www.animatereactnative.com/animatereactnative_logo.svg" width="50%">
</picture>
</a>
</p>
4 changes: 4 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
},
"dependencies": {
"@expo/metro-runtime": "~4.0.0",
"@tanstack/react-query": "^5.61.0",
"expo": "~52.0.8",
"expo-status-bar": "~2.0.0",
"lucide-react-native": "^0.460.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.2",
"react-native-reanimated": "~3.16.1",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13"
},
"devDependencies": {
Expand Down
213 changes: 190 additions & 23 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,197 @@
import { useState, useEffect } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { multiply } from '@animatereactnative/accordion';
import { Accordion } from '@animatereactnative/accordion';
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query';
import { ChevronUp } from 'lucide-react-native';
import { useEffect, useState } from 'react';
import {
ActivityIndicator,
Image,
ScrollView,
StatusBar,
Text,
View,
} from 'react-native';
import Animated, { FadeInDown } from 'react-native-reanimated';

export default function App() {
const [result, setResult] = useState<number | undefined>();
export const queryClient = new QueryClient();
const _spacing = 20;

useEffect(() => {
multiply(3, 7).then(setResult);
}, []);
async function wait(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

const usage = `<Accordion.Accordion>
<Accordion.Header>
<Text>Accordion header</Text>
<Accordion.HeaderIcon>
<ChevronUp />
</Accordion.HeaderIcon>
</Accordion.Header>
<Accordion.Collapsed>
<Text>Visible !expanded</Text>
</Accordion.Collapsed>
<Accordion.Expanded>
<Text>Collapsed content</Text>
{loading && <ActivityIndicator />}
{data & <MyList data={data}/>}
</Accordion.Expanded>
</Accordion.Accordion>`;

type Quote = {
id: number;
quote: string;
author: string;
};

export default function AccordionExample() {
return (
<View style={styles.container}>
<Text>Result: {result}</Text>
</View>
<QueryClientProvider client={queryClient}>
<View
style={{ flex: 1, justifyContent: 'center', backgroundColor: '#000' }}

Check warning on line 59 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { flex: 1, justifyContent: 'center', backgroundColor: '#000' }
>
<ScrollView contentContainerStyle={{ paddingTop: 100 }}>

Check warning on line 61 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { paddingTop: 100 }
<StatusBar hidden />
<View style={{ gap: _spacing / 2, paddingHorizontal: _spacing }}>
<DummyAccordionWithData type="<Accordion /> component" />
<DummyAccordionWithData type="Done with Reanimated" />
<DummyAccordionWithData type="AnimateReactNative.com" />
<Accordion.Sibling
style={{

Check warning on line 68 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { backgroundColor: '#3332' }
padding: _spacing,
backgroundColor: '#3332',
borderRadius: _spacing,
}}
>
<Text
style={{

Check warning on line 75 in example/src/App.tsx

View workflow job for this annotation

GitHub Actions / lint

Inline style: { color: '#fff', opacity: 0.6, fontSize: 12, fontFamily: 'Menlo' }
color: '#fff',
opacity: 0.6,
fontSize: 12,
fontFamily: 'Menlo',
}}
>
{usage}
</Text>
<Image
source={{
uri: 'https://github.com/animate-react-native/.github/blob/main/animatereactnative.com-dark.png?raw=true',
}}
style={{
width: '70%',
height: 100,
resizeMode: 'contain',
alignSelf: 'center',
}}
/>
</Accordion.Sibling>
</View>
</ScrollView>
</View>
</QueryClientProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 60,
height: 60,
marginVertical: 20,
},
});
function DummyAccordionWithData({ type = 'default' }) {
const [isActive, setIsActive] = useState(false);
const { data, isLoading } = useQuery({
queryKey: ['dummydata', isActive, type],
enabled: isActive,
queryFn: async () => {
const data = (await fetch(
`https://dummyjson.com/quotes?limit=${
Math.floor(Math.random() * 4) + 4
}&skip=${Math.floor(Math.random() * 20)}`
).then((res) => res.json())) as { quotes: Quote[] };
await wait(1000);

return data;
},
});

useEffect(() => {
if (!isActive) {
queryClient.resetQueries({
queryKey: ['dummydata', true, type],
});
}
}, [isActive, type]);

return (
<Accordion.Accordion
onChange={(value) => setIsActive(value)}
style={{ gap: _spacing / 2 }}
>
<Accordion.Header>
<View
style={{
backgroundColor: 'gold',
padding: _spacing / 2,
borderRadius: _spacing / 2,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Text style={{ color: '#111', flexShrink: 1 }}>{type}</Text>
<Accordion.HeaderIcon>
<ChevronUp size={20} color="#333" />
</Accordion.HeaderIcon>
</View>
</Accordion.Header>
<Accordion.Collapsed>
<Text style={{ color: 'white', fontSize: 12 }}>
Hidden when expanded
</Text>
</Accordion.Collapsed>
<Accordion.Always>
<Text style={{ color: 'white', fontSize: 10 }}>
Get more details about {type}
</Text>
</Accordion.Always>
<Accordion.Expanded
style={{
backgroundColor: '#333',
width: '100%',
borderRadius: _spacing / 2,
padding: _spacing / 2,
}}
>
{isLoading && <ActivityIndicator color={'white'} />}
{!isLoading && data?.quotes && (
<View style={{ gap: _spacing / 2 }}>
{data?.quotes?.map((quote, index) => (
<Animated.View
key={quote.id}
entering={FadeInDown.springify()
.damping(80)
.stiffness(200)
.delay(index * 75)}
>
<Text
style={{
color: '#fff',
opacity: 0.7,
fontSize: 12,
fontFamily: 'Menlo',
}}
>
{quote.quote}
</Text>
<Text style={{ color: '#fff' }}>{quote.author}</Text>
</Animated.View>
))}
</View>
)}
</Accordion.Expanded>
</Accordion.Accordion>
);
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"react": "18.3.1",
"react-native": "0.76.2",
"react-native-builder-bob": "^0.32.1",
"react-native-reanimated": "^3.16.2",
"release-it": "^17.10.0",
"typescript": "^5.2.2"
},
Expand All @@ -88,7 +89,8 @@
},
"peerDependencies": {
"react": "*",
"react-native": "*"
"react-native": "*",
"react-native-reanimated": ">=2.2.0"
},
"workspaces": [
"example"
Expand Down
Loading

0 comments on commit 90a4e35

Please sign in to comment.