Skip to content

Commit 7737875

Browse files
committed
feat: animate List.Accordion expand and collapse
1 parent 85e2a1e commit 7737875

2 files changed

Lines changed: 362 additions & 194 deletions

File tree

‎src/components/List/ListAccordion.tsx‎

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,25 @@ import type {
1212
ViewStyle,
1313
} from 'react-native';
1414

15+
import Animated, {
16+
Easing,
17+
ReduceMotion,
18+
useAnimatedStyle,
19+
useSharedValue,
20+
withTiming,
21+
} from 'react-native-reanimated';
22+
import type {
23+
EntryAnimationsValues,
24+
ExitAnimationsValues,
25+
} from 'react-native-reanimated';
26+
1527
import { ListAccordionGroupContext } from './ListAccordionGroup';
1628
import { ListTokens } from './tokens';
1729
import type { ListChildProps, Style } from './utils';
1830
import { ListRowContext, getAccordionColors, getLeftStyles } from './utils';
1931
import { useLocale } from '../../core/locale';
2032
import { useInternalTheme } from '../../core/theming';
33+
import { useReduceMotion } from '../../theme/accessibility/ReduceMotionContext';
2134
import type { ThemeProp } from '../../theme/types';
2235
import MaterialCommunityIcon from '../MaterialCommunityIcon';
2336
import TouchableRipple from '../TouchableRipple/TouchableRipple';
@@ -239,6 +252,62 @@ const ListAccordion = ({
239252
? groupContext.expandedId === id
240253
: expandedInternal;
241254

255+
const reduceMotion = useReduceMotion();
256+
const reanimatedReduceMotion = reduceMotion
257+
? ReduceMotion.Always
258+
: ReduceMotion.Never;
259+
260+
const timingConfig = React.useMemo(
261+
() => ({
262+
duration: theme.motion.duration.medium2,
263+
easing: Easing.bezier(...theme.motion.easing.emphasized),
264+
reduceMotion: reanimatedReduceMotion,
265+
}),
266+
[
267+
theme.motion.duration.medium2,
268+
theme.motion.easing.emphasized,
269+
reanimatedReduceMotion,
270+
]
271+
);
272+
273+
const chevronProgress = useSharedValue(isExpanded ? 1 : 0);
274+
275+
React.useEffect(() => {
276+
chevronProgress.value = withTiming(isExpanded ? 1 : 0, timingConfig);
277+
}, [isExpanded, chevronProgress, timingConfig]);
278+
279+
const chevronStyle = useAnimatedStyle(() => ({
280+
transform: [{ rotate: `${chevronProgress.value * 180}deg` }],
281+
}));
282+
283+
const expandAnimation = React.useCallback(
284+
(values: EntryAnimationsValues) => {
285+
'worklet';
286+
return {
287+
initialValues: { height: 0, opacity: 0 },
288+
animations: {
289+
height: withTiming(values.targetHeight, timingConfig),
290+
opacity: withTiming(1, timingConfig),
291+
},
292+
};
293+
},
294+
[timingConfig]
295+
);
296+
297+
const collapseAnimation = React.useCallback(
298+
(values: ExitAnimationsValues) => {
299+
'worklet';
300+
return {
301+
initialValues: { height: values.currentHeight, opacity: 1 },
302+
animations: {
303+
height: withTiming(0, timingConfig),
304+
opacity: withTiming(0, timingConfig),
305+
},
306+
};
307+
},
308+
[timingConfig]
309+
);
310+
242311
const {
243312
descriptionColor,
244313
titleTextColor,
@@ -341,21 +410,28 @@ const ListAccordion = ({
341410
isExpanded: isExpanded,
342411
})
343412
) : (
344-
<MaterialCommunityIcon
345-
name={isExpanded ? 'chevron-up' : 'chevron-down'}
346-
color={trailingIconColor}
347-
size={24}
348-
direction={direction}
349-
/>
413+
<Animated.View style={chevronStyle}>
414+
<MaterialCommunityIcon
415+
name="chevron-down"
416+
color={trailingIconColor}
417+
size={24}
418+
direction={direction}
419+
/>
420+
</Animated.View>
350421
)}
351422
</View>
352423
</View>
353424
</TouchableRipple>
354425
</ListRowContext.Provider>
355426
</View>
356427

357-
{isExpanded
358-
? React.Children.map(children, (child) => {
428+
{isExpanded ? (
429+
<Animated.View
430+
entering={expandAnimation}
431+
exiting={collapseAnimation}
432+
style={styles.expandedContent}
433+
>
434+
{React.Children.map(children, (child) => {
359435
if (
360436
left &&
361437
React.isValidElement<ListChildProps>(child) &&
@@ -369,8 +445,9 @@ const ListAccordion = ({
369445
}
370446

371447
return child;
372-
})
373-
: null}
448+
})}
449+
</Animated.View>
450+
) : null}
374451
</View>
375452
);
376453
};
@@ -405,6 +482,9 @@ const styles = StyleSheet.create({
405482
child: {
406483
paddingLeft: 40,
407484
},
485+
expandedContent: {
486+
overflow: 'hidden',
487+
},
408488
content: {
409489
flex: 1,
410490
justifyContent: 'center',

0 commit comments

Comments
 (0)