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
JavaScript
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);