Skip to content

Instantly share code, notes, and snippets.

@schriker
Last active July 26, 2024 14:53
Show Gist options
  • Select an option

  • Save schriker/75e54acca7595d10d7d3ab7c255c7445 to your computer and use it in GitHub Desktop.

Select an option

Save schriker/75e54acca7595d10d7d3ab7c255c7445 to your computer and use it in GitHub Desktop.
Accordion open by default
import { Pressable, Section, Typography } from '@components'
import EntypoIcons from '@expo/vector-icons/Entypo'
import { TypographySize } from '@types'
import { View } from 'react-native'
import Animated, { runOnUI, useAnimatedStyle, withTiming } from 'react-native-reanimated'
import styled, { useTheme } from 'styled-components/native'
import { useAccordion } from '../hooks'
type AccordionItemProps = {
index: number,
label: string,
body: string
}
export const AccordionItem: React.FunctionComponent<AccordionItemProps> = ({
label,
index,
body
}) => {
const theme = useTheme()
const { setHeight, animatedHeightStyle, animatedRef, isOpened, openOnMounted } = useAccordion()
const animatedChevronStyle = useAnimatedStyle(() => ({
transform: [
{
rotate: withTiming(`${isOpened.value ? 90 : 0}deg`, {
duration: 200
})
}
]
}))
return (
<Section marginBottom={0}>
<ContentWrapper>
<Pressable onPress={() => runOnUI(setHeight)()}>
<LabelWrapper>
<Typography
size={TypographySize.SmallTitle}
fontWeight={500}
>
{`${label} #${index + 1}`}
</Typography>
<Animated.View style={[animatedChevronStyle]}>
<EntypoIcons
name="chevron-small-right"
size={20}
color={theme.colors.typography.secondary}
/>
</Animated.View>
</LabelWrapper>
</Pressable>
<Animated.View
style={[animatedHeightStyle]}
// Open accordion onLayout if it is not already open, we need this to run onLayout to get element height
onLayout={() => {
if (!openOnMounted.value) {
openOnMounted.value = true
}
}}
>
<AccordionWrapper>
<BodyWrapper
ref={animatedRef}
collapsable={false}
>
<Typography size={TypographySize.Body}>
{body}
</Typography>
</BodyWrapper>
</AccordionWrapper>
</Animated.View>
</ContentWrapper>
</Section>
)
}
const ContentWrapper = styled(View)(() => ({
overflow: 'hidden'
}))
const AccordionWrapper = styled(View)(() => ({
position: 'absolute',
left: 0,
top: 0
}))
const LabelWrapper = styled(View)(() => ({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingLeft: 20,
paddingRight: 10,
paddingBottom: 10,
paddingTop: 10
}))
const BodyWrapper = styled(View)(() => ({
paddingLeft: 20,
paddingRight: 20,
paddingBottom: 20
}))
import { View } from 'react-native'
import { measure, useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'
export const useAccordion = () => {
const openOnMounted = useSharedValue(false)
const animatedRef = useAnimatedRef<View>()
const isOpened = useSharedValue(false)
const height = useSharedValue(0)
const animatedHeightStyle = useAnimatedStyle(() => ({
height: withTiming(height.value)
}))
const setHeight = () => {
'worklet'
height.value = !height.value
? Number(measure(animatedRef).height ?? 0)
: 0
isOpened.value = !isOpened.value
}
// We can use useAnimatedReaction to run our setHeight method when some animated value changed
useAnimatedReaction(
() => openOnMounted.value,
isOpenOnMounted => {
if (isOpenOnMounted) {
setHeight()
}
}
)
return {
animatedRef,
setHeight,
isOpened,
animatedHeightStyle,
openOnMounted
}
}
@georgiarnaudov
Copy link

Looks great, thanks! I tweaked it a bit so that I have a bit so that I can pass the defaultOpen value as an argument. Here's the final hook that worked great for me.

import {
  measure,
  useAnimatedReaction,
  useAnimatedRef,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

export const useAccordion = ({
  defaultOpen = false,
}: {defaultOpen?: boolean} = {}) => {
  const openOnMounted = useSharedValue(false);
  const aRef = useAnimatedRef();
  const isOpen = useSharedValue(false);
  const height = useSharedValue(0);

  const animatedHeightStyle = useAnimatedStyle(() => ({
    height: withTiming(height.value),
  }));

  const toggle = () => {
    'worklet';

    height.value = !height.value ? Number(measure(aRef)?.height ?? 0) : 0;

    isOpen.value = !isOpen.value;
  };

  useAnimatedReaction(
    () => openOnMounted.value,
    isOpenOnMounted => {
      if (isOpenOnMounted) {
        toggle();
      }
    },
  );

  const onLayout = () => {
    if (!openOnMounted.value && defaultOpen) {
      openOnMounted.value = defaultOpen;
    }
  };

  return {
    aRef,
    isOpen,
    animatedHeightStyle,
    toggle,
    onLayout,
  };
};

@georgiarnaudov
Copy link

Also, had to put the onLayout callback on the View that holds the aRef.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment