UNPKG

react-native-ui-lib

Version:

[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://stand-with-ukraine.pp.ua)

240 lines (228 loc) • 7.14 kB
import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { StyleSheet, Animated, Easing, LayoutAnimation } from 'react-native'; import { Colors } from "../../style"; import View from "../view"; import TouchableOpacity from "../touchableOpacity"; import Button, { ButtonSize } from "../button"; import Card from "../card"; import { Constants, asBaseComponent } from "../../commons/new"; import { useDidUpdate } from "../../hooks"; const PEEP = 8; const DURATION = 300; const MARGIN_BOTTOM = 24; const buttonStartValue = 0.8; const icon = require("./assets/arrow-down.png"); /** * @description: Stack aggregator component * @modifiers: margin, padding * @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/StackAggregatorScreen.tsx */ const StackAggregator = props => { const { children, containerStyle, buttonProps, collapsed = true, disablePresses = false, onItemPress, contentContainerStyle, itemBorderRadius = 0, onCollapseWillChange, onCollapseChanged } = props; const itemsCount = React.Children.count(children); const [firstItemHeight, setFirstItemHeight] = useState(); const [isCollapsed, setIsCollapsed] = useState(collapsed); useEffect(() => { setIsCollapsed(collapsed); }, [collapsed]); useDidUpdate(() => { onCollapseWillChange?.(isCollapsed); animate(); onCollapseChanged?.(isCollapsed); }, [isCollapsed, onCollapseWillChange, onCollapseChanged]); /** Animations */ const animatedScale = new Animated.Value(isCollapsed ? buttonStartValue : 1); const animatedOpacity = new Animated.Value(isCollapsed ? buttonStartValue : 1); const animatedContentOpacity = useMemo(() => { return new Animated.Value(isCollapsed ? 0 : 1); }, [isCollapsed]); const easeOut = Easing.bezier(0, 0, 0.58, 1); const getItemScale = useCallback(index => { if (isCollapsed) { if (index === itemsCount - 2) { return 0.95; } if (index === itemsCount - 1) { return 0.9; } } return 1; }, [isCollapsed, itemsCount]); const getAnimatedScales = useCallback(() => { return React.Children.map(children, (_item, index) => { return new Animated.Value(getItemScale(index)); }); }, [children, getItemScale]); const animatedScaleArray = useMemo(() => { return getAnimatedScales(); }, [getAnimatedScales]); const animate = () => { return Promise.all([animateValues(), animateCards()]); }; const animateValues = () => { const newValue = isCollapsed ? buttonStartValue : 1; return new Promise(resolve => { Animated.parallel([Animated.timing(animatedOpacity, { duration: DURATION, toValue: Number(newValue), useNativeDriver: true }), Animated.timing(animatedScale, { toValue: Number(newValue), easing: easeOut, duration: DURATION, useNativeDriver: true }), Animated.timing(animatedContentOpacity, { toValue: Number(isCollapsed ? 0 : 1), easing: easeOut, duration: DURATION, useNativeDriver: true })]).start(resolve); }); }; const animateCards = () => { const promises = []; for (let index = 0; index < itemsCount; index++) { const newScale = getItemScale(index); promises.push(new Promise(resolve => { Animated.timing(animatedScaleArray[index], { toValue: newScale, easing: easeOut, duration: DURATION, useNativeDriver: true }).start(resolve); })); } return Promise.all(promises); }; /** Actions */ const close = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setIsCollapsed(true); }; const open = () => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setIsCollapsed(false); }; /** Styles */ const getTop = index => { let start = 0; if (index === itemsCount - 2) { start += PEEP; } if (index === itemsCount - 1) { start += PEEP * 2; } return start; }; const getItemStyle = index => { if (isCollapsed) { return { position: index !== 0 ? 'absolute' : undefined, top: getTop(index) }; } return { marginBottom: MARGIN_BOTTOM, marginTop: index === 0 ? 40 : undefined }; }; const touchableOpacityStyle = useMemo(() => { return [styles.touchable, { height: firstItemHeight ? firstItemHeight + PEEP * 2 : undefined, zIndex: itemsCount }]; }, [firstItemHeight, itemsCount]); /** Events */ const _onItemPress = index => { if (!disablePresses) { onItemPress?.(index); } }; const onLayout = event => { const height = event.nativeEvent.layout.height; if (height) { setFirstItemHeight(height); } }; /** Renders */ const renderItem = (item, index) => { return <Animated.View onLayout={index === 0 ? onLayout : undefined} style={[Constants.isIOS && styles.containerShadow, contentContainerStyle, //@ts-expect-error 'position' doesn't match AnimatedInterpolation type getItemStyle(index), { borderRadius: Constants.isIOS ? itemBorderRadius : undefined, alignSelf: 'center', zIndex: itemsCount - index, transform: [{ scaleX: animatedScaleArray[index] }], width: Constants.windowWidth - 40, height: isCollapsed ? firstItemHeight : undefined }]} collapsable={false}> <Card style={styles.card} onPress={() => _onItemPress(index)} borderRadius={itemBorderRadius} elevation={5}> <Animated.View style={index !== 0 ? { opacity: animatedContentOpacity } : undefined} collapsable={false}> {item} </Animated.View> </Card> </Animated.View>; }; return <View style={containerStyle}> <View style={styles.subContainer}> <Animated.View style={{ position: 'absolute', right: 0, opacity: animatedOpacity, transform: [{ scale: animatedScale }] }}> <Button label={'Show less'} iconSource={icon} link size={ButtonSize.small} {...buttonProps} marginH-24 marginB-20 onPress={close} style={styles.button} /> </Animated.View> {React.Children.map(children, (item, index) => { return renderItem(item, index); })} {isCollapsed && <TouchableOpacity onPress={open} activeOpacity={1} style={touchableOpacityStyle} />} </View> </View>; }; export default asBaseComponent(StackAggregator); StackAggregator.displayName = 'StackAggregator'; const styles = StyleSheet.create({ subContainer: { marginBottom: PEEP * 3 }, touchable: { position: 'absolute', width: '100%' }, containerShadow: { backgroundColor: Colors.white, shadowColor: Colors.grey40, shadowOpacity: 0.25, shadowRadius: 12, shadowOffset: { height: 5, width: 0 } }, card: { overflow: 'hidden', flexShrink: 1, backgroundColor: 'transparent' }, button: { zIndex: 100 } });