UNPKG

react-native-ui-lib

Version:

<p align="center"> <img src="https://user-images.githubusercontent.com/1780255/105469025-56759000-5ca0-11eb-993d-3568c1fd54f4.png" height="250px" style="display:block"/> </p> <p align="center">UI Toolset & Components Library for React Native</p> <p a

312 lines (278 loc) • 8.15 kB
import _pt from "prop-types"; import React, { PureComponent } 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"; 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 */ class StackAggregator extends PureComponent { static propTypes = { /** * The initial state of the stack */ collapsed: _pt.bool, /** * Component Children */ children: _pt.oneOfType([_pt.element, _pt.arrayOf(_pt.element)]).isRequired, /** * The items border radius */ itemBorderRadius: _pt.number, /** * A callback for item press */ onItemPress: _pt.func, /** * A callback for collapse state will change (value is future collapsed state) */ onCollapseWillChange: _pt.func, /** * A callback for collapse state change (value is collapsed state) */ onCollapseChanged: _pt.func, /** * A setting that disables pressability on cards */ disablePresses: _pt.bool }; static displayName = 'StackAggregator'; itemsCount = React.Children.count(this.props.children); easeOut = Easing.bezier(0, 0, 0.58, 1); static defaultProps = { disablePresses: false, collapsed: true, itemBorderRadius: 0 }; constructor(props) { super(props); this.state = { collapsed: props.collapsed, firstItemHeight: undefined }; this.animatedScale = new Animated.Value(this.state.collapsed ? buttonStartValue : 1); this.animatedOpacity = new Animated.Value(this.state.collapsed ? buttonStartValue : 1); this.animatedContentOpacity = new Animated.Value(this.state.collapsed ? 0 : 1); this.animatedScaleArray = this.getAnimatedScales(); } componentDidUpdate(_prevProps, prevState) { if (prevState.collapsed !== this.state?.collapsed) { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); } } getAnimatedScales = () => { return React.Children.map(this.props.children, (_item, index) => { return new Animated.Value(this.getItemScale(index)); }); }; getItemScale = index => { if (this.state.collapsed) { if (index === this.itemsCount - 2) { return 0.95; } if (index === this.itemsCount - 1) { return 0.9; } } return 1; }; animate = async () => { return Promise.all([this.animateValues(), this.animateCards()]); }; animateValues() { const { collapsed } = this.state; const newValue = collapsed ? buttonStartValue : 1; return new Promise(resolve => { Animated.parallel([Animated.timing(this.animatedOpacity, { duration: DURATION, toValue: Number(newValue), useNativeDriver: true }), Animated.timing(this.animatedScale, { toValue: Number(newValue), easing: this.easeOut, duration: DURATION, useNativeDriver: true }), Animated.timing(this.animatedContentOpacity, { toValue: Number(collapsed ? 0 : 1), easing: this.easeOut, duration: DURATION, useNativeDriver: true })]).start(resolve); }); } animateCards() { const promises = []; for (let index = 0; index < this.itemsCount; index++) { const newScale = this.getItemScale(index); promises.push(new Promise(resolve => { Animated.timing(this.animatedScaleArray[index], { toValue: Number(newScale), easing: this.easeOut, duration: DURATION, useNativeDriver: true }).start(resolve); })); } return Promise.all(promises); } close = () => { this.setState({ collapsed: true }, async () => { this.props.onCollapseWillChange?.(true); if (this.props.onCollapseChanged) { await this.animate(); this.props.onCollapseChanged(true); } else { this.animate(); } }); }; open = () => { this.setState({ collapsed: false }, async () => { this.props.onCollapseWillChange?.(false); if (this.props.onCollapseChanged) { await this.animate(); this.props.onCollapseChanged(false); } else { this.animate(); } }); }; getTop(index) { let start = 0; if (index === this.itemsCount - 2) { start += PEEP; } if (index === this.itemsCount - 1) { start += PEEP * 2; } return start; } getStyle(index) { const { collapsed } = this.state; const top = this.getTop(index); if (collapsed) { return { position: index !== 0 ? 'absolute' : undefined, top }; } return { marginBottom: MARGIN_BOTTOM, marginTop: index === 0 ? 40 : undefined }; } onLayout = event => { const height = event.nativeEvent.layout.height; if (height) { this.setState({ firstItemHeight: height }); } }; onItemPress = index => { this.props.onItemPress?.(index); }; renderItem = (item, index) => { const { contentContainerStyle, itemBorderRadius } = this.props; const { firstItemHeight, collapsed } = this.state; return <Animated.View key={index} onLayout={index === 0 ? this.onLayout : undefined} style={[Constants.isIOS && styles.containerShadow, this.getStyle(index), { borderRadius: Constants.isIOS ? itemBorderRadius : undefined, alignSelf: 'center', zIndex: this.itemsCount - index, transform: [{ scaleX: this.animatedScaleArray[index] }], width: Constants.screenWidth - 40, height: collapsed ? firstItemHeight : undefined }]} collapsable={false}> <Card style={[contentContainerStyle, styles.card]} onPress={() => this.props.disablePresses && this.onItemPress(index)} borderRadius={itemBorderRadius} elevation={5}> <Animated.View style={index !== 0 ? { opacity: this.animatedContentOpacity } : undefined} collapsable={false}> {item} </Animated.View> </Card> </Animated.View>; }; render() { const { children, containerStyle, buttonProps } = this.props; const { collapsed, firstItemHeight } = this.state; return <View style={containerStyle}> <View style={{ marginBottom: PEEP * 3 }}> <Animated.View style={{ position: 'absolute', right: 0, opacity: this.animatedOpacity, transform: [{ scale: this.animatedScale }] }}> <Button label={'Show less'} iconSource={icon} link size={ButtonSize.small} {...buttonProps} marginH-24 marginB-20 onPress={this.close} /> </Animated.View> {React.Children.map(children, (item, index) => { return this.renderItem(item, index); })} {collapsed && <TouchableOpacity onPress={this.open} activeOpacity={1} style={[styles.touchable, { height: firstItemHeight ? firstItemHeight + PEEP * 2 : undefined, zIndex: this.itemsCount }]} />} </View> </View>; } } const styles = StyleSheet.create({ 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 } }); export default asBaseComponent(StackAggregator);