diff --git a/src/components/Accordion/index.native.tsx b/src/components/Accordion/index.native.tsx new file mode 100644 index 000000000000..0fcaa7294dd7 --- /dev/null +++ b/src/components/Accordion/index.native.tsx @@ -0,0 +1,81 @@ +import type {ReactNode} from 'react'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {SharedValue} from 'react-native-reanimated'; +import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type AccordionProps = { + /** Giving information whether the component is open */ + isExpanded: SharedValue; + + /** Element that is inside Accordion */ + children: ReactNode; + + /** Duration of expansion animation */ + duration?: number; + + /** Additional external style */ + style?: StyleProp; + + /** Was toggle triggered */ + isToggleTriggered: SharedValue; +}; + +function Accordion({isExpanded, children, duration = 300, isToggleTriggered, style}: AccordionProps) { + const height = useSharedValue(0); + const styles = useThemeStyles(); + + const derivedHeight = useDerivedValue(() => { + if (!isToggleTriggered.get()) { + return isExpanded.get() ? height.get() : 0; + } + + return withTiming(height.get() * Number(isExpanded.get()), { + duration, + easing: Easing.inOut(Easing.quad), + }); + }); + + const derivedOpacity = useDerivedValue(() => { + if (!isToggleTriggered.get()) { + return isExpanded.get() ? 1 : 0; + } + + return withTiming(isExpanded.get() ? 1 : 0, { + duration, + easing: Easing.inOut(Easing.quad), + }); + }); + + const animatedStyle = useAnimatedStyle(() => { + if (!isToggleTriggered.get() && !isExpanded.get()) { + return { + height: 0, + opacity: 0, + }; + } + return { + height: !isToggleTriggered.get() ? height.get() : derivedHeight.get(), + opacity: derivedOpacity.get(), + }; + }); + + return ( + + { + height.set(e.nativeEvent.layout.height); + }} + style={[styles.pAbsolute, styles.l0, styles.r0, styles.t0]} + > + {children} + + + ); +} + +Accordion.displayName = 'Accordion'; + +export default Accordion; diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx new file mode 100644 index 000000000000..9715f3902c03 --- /dev/null +++ b/src/components/Accordion/index.tsx @@ -0,0 +1,78 @@ +import type {ReactNode} from 'react'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {SharedValue} from 'react-native-reanimated'; +import Animated, {Easing, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming} from 'react-native-reanimated'; + +type AccordionProps = { + /** Giving information whether the component is open */ + isExpanded: SharedValue; + + /** Element that is inside Accordion */ + children: ReactNode; + + /** Duration of expansion animation */ + duration?: number; + + /** Additional external style */ + style?: StyleProp; + + /** Was toggle triggered */ + isToggleTriggered: SharedValue; +}; + +function Accordion({isExpanded, children, duration = 300, isToggleTriggered, style}: AccordionProps) { + const height = useSharedValue(0); + + const derivedHeight = useDerivedValue(() => { + if (!isToggleTriggered.get()) { + return isExpanded.get() ? height.get() : 0; + } + + return withTiming(height.get() * Number(isExpanded.get()), { + duration, + easing: Easing.inOut(Easing.quad), + }); + }); + + const derivedOpacity = useDerivedValue(() => { + if (!isToggleTriggered.get()) { + return isExpanded.get() ? 1 : 0; + } + + return withTiming(isExpanded.get() ? 1 : 0, { + duration, + easing: Easing.inOut(Easing.quad), + }); + }); + + const animatedStyle = useAnimatedStyle(() => { + if (!isToggleTriggered.get() && !isExpanded.get()) { + return { + height: 0, + opacity: 0, + }; + } + + return { + height: !isToggleTriggered.get() ? undefined : derivedHeight.get(), + opacity: derivedOpacity.get(), + }; + }); + + return ( + + { + height.set(e.nativeEvent.layout.height); + }} + > + {children} + + + ); +} +Accordion.displayName = 'Accordion'; + +export default Accordion; diff --git a/src/libs/Navigation/linkTo/index.ts b/src/libs/Navigation/linkTo/index.ts index 93ae33a6f77e..771c62c4ca70 100644 --- a/src/libs/Navigation/linkTo/index.ts +++ b/src/libs/Navigation/linkTo/index.ts @@ -183,7 +183,13 @@ export default function linkTo(navigation: NavigationContainerRef; + /** Used to apply styles to the Accordion */ + accordionStyle?: StyleProp; + /** Whether the option is enabled or not */ isActive: boolean; @@ -81,6 +86,7 @@ function ToggleSettingOptionRow({ customTitle, subtitle, subtitleStyle, + accordionStyle, switchAccessibilityLabel, shouldPlaceSubtitleBelowSwitch, shouldEscapeText = undefined, @@ -98,6 +104,12 @@ function ToggleSettingOptionRow({ showLockIcon = false, }: ToggleSettingOptionRowProps) { const styles = useThemeStyles(); + const isExpanded = useSharedValue(isActive); + const isToggleTriggered = useSharedValue(false); + + useEffect(() => { + isExpanded.set(isActive); + }, [isExpanded, isActive]); const subtitleHtml = useMemo(() => { if (!subtitle || !shouldParseSubtitle || typeof subtitle !== 'string') { @@ -171,14 +183,23 @@ function ToggleSettingOptionRow({ { + isToggleTriggered.set(true); + onToggle(isOn); + }} isOn={isActive} disabled={disabled} showLockIcon={showLockIcon} /> {shouldPlaceSubtitleBelowSwitch && subtitle && subTitleView} - {isActive && subMenuItems} + + {subMenuItems} + );