diff options
Diffstat (limited to 'gui/packages/components/src')
| -rw-r--r-- | gui/packages/components/src/Accordion.tsx | 151 |
1 files changed, 75 insertions, 76 deletions
diff --git a/gui/packages/components/src/Accordion.tsx b/gui/packages/components/src/Accordion.tsx index 370cce7e28..7019c9cb9c 100644 --- a/gui/packages/components/src/Accordion.tsx +++ b/gui/packages/components/src/Accordion.tsx @@ -2,41 +2,46 @@ import * as React from 'react'; import { Animated, Component, Styles, Types, UserInterface, View } from 'reactxp'; interface IProps { - height: number | 'auto'; - animationDuration?: number; + expanded: boolean; + animationDuration: number; style?: Types.AnimatedViewStyleRuleSet; children?: React.ReactNode; } interface IState { - animatedValue: Animated.Value | null; + applyAnimatedStyle: boolean; + mountChildren: boolean; } const containerOverflowStyle = Styles.createViewStyle({ overflow: 'hidden' }); export default class Accordion extends Component<IProps, IState> { public static defaultProps = { - height: 'auto', + expanded: true, animationDuration: 350, }; public state: IState = { - animatedValue: null, + applyAnimatedStyle: false, + mountChildren: false, }; + private heightValue = Animated.createValue(0); + private animatedStyle = Styles.createAnimatedViewStyle({ + height: this.heightValue, + }); + private containerRef = React.createRef<Animated.View>(); - private contentHeight = 0; - private animation: Types.Animated.CompositeAnimation | null = null; + private contentRef = React.createRef<View>(); + private animation?: Types.Animated.CompositeAnimation = undefined; constructor(props: IProps) { super(props); - // set the initial height if it's known - if (typeof props.height === 'number') { - this.state = { - animatedValue: Animated.createValue(props.height), - }; - } + this.state = { + applyAnimatedStyle: !props.expanded, + mountChildren: props.expanded, + }; } public componentWillUnmount() { @@ -45,95 +50,89 @@ export default class Accordion extends Component<IProps, IState> { } } - public shouldComponentUpdate(nextProps: IProps, nextState: IState) { - return ( - nextState.animatedValue !== this.state.animatedValue || - nextProps.height !== this.props.height || - nextProps.children !== this.props.children - ); - } - - public componentDidUpdate(prevProps: IProps, prevState: IState) { - if (prevProps.height !== this.props.height) { - this.animateHeightChanges(); + public componentDidUpdate(oldProps: IProps, oldState: IState) { + if (this.props.expanded !== oldProps.expanded) { + // make sure the children are mounted first before expanding the accordion + if (this.props.expanded && !this.state.mountChildren) { + this.setState({ mountChildren: true }); + } else { + this.animate(this.props.expanded); + } + } else if (this.state.mountChildren && !oldState.mountChildren) { + // run animations once the children are mounted + this.animate(this.props.expanded); } } public render() { - const { style, height, children, animationDuration, ...otherProps } = this.props; + const { style, children, expanded, animationDuration, ...otherProps } = this.props; const containerStyles = [style]; - if (this.state.animatedValue !== null) { - const animatedStyle = Styles.createAnimatedViewStyle({ - height: this.state.animatedValue, - }); - - containerStyles.push(containerOverflowStyle, animatedStyle); + if (this.state.applyAnimatedStyle) { + containerStyles.push(containerOverflowStyle, this.animatedStyle); } return ( - <Animated.View - {...otherProps} - style={containerStyles} - ref={ - /* Fix: cast to any because reactxp has out of date annotations - See: https://github.com/Microsoft/reactxp/issues/784 - */ - this.containerRef as any - }> - <View onLayout={this.contentLayoutDidChange}>{children}</View> + <Animated.View {...otherProps} style={containerStyles} ref={this.containerRef}> + <View ref={this.contentRef}>{this.state.mountChildren && children}</View> </Animated.View> ); } - private async animateHeightChanges() { + private async animate(expand: boolean) { const containerView = this.containerRef.current; - if (!containerView) { + const contentView = this.contentRef.current; + if (!containerView || !contentView) { return; } if (this.animation) { this.animation.stop(); - this.animation = null; + this.animation = undefined; } - try { - const layout = await UserInterface.measureLayoutRelativeToWindow(containerView); - const fromValue = this.state.animatedValue || Animated.createValue(layout.height); - const toValue = this.props.height === 'auto' ? this.contentHeight : this.props.height; + const containerLayout = await UserInterface.measureLayoutRelativeToWindow(containerView); + const contentLayout = await UserInterface.measureLayoutRelativeToAncestor( + contentView, + containerView, + ); - // calculate the animation duration based on travel distance - const multiplier = Math.abs(toValue - layout.height) / Math.max(1, this.contentHeight); - const duration = Math.ceil(this.props.animationDuration! * multiplier); + // the content is expanded when the animated style is not applied, + // so reset the initial animated value to the current layout's height. + if (!this.state.applyAnimatedStyle) { + this.heightValue.setValue(containerLayout.height); + } - const animation = Animated.timing(fromValue, { - toValue, - easing: Animated.Easing.InOut(), - duration, - useNativeDriver: true, - }); + const toValue = expand ? contentLayout.height : 0; - this.animation = animation; - this.setState({ animatedValue: fromValue }, () => { - animation.start(this.onAnimationEnd); - }); - } catch (error) { - // TODO: log error - } - } + // calculate the animation duration based on travel distance + const multiplier = + Math.abs(toValue - containerLayout.height) / Math.max(1, contentLayout.height); + const duration = Math.ceil(this.props.animationDuration * multiplier); + + const animation = Animated.timing(this.heightValue, { + toValue, + easing: Animated.Easing.InOut(), + duration, + useNativeDriver: true, + }); - private onAnimationEnd = ({ finished }: Types.Animated.EndResult) => { - if (finished) { - this.animation = null; + this.animation = animation; - // reset height after transition to let element layout naturally - // if animation finished without interruption - if (this.props.height === 'auto') { - this.setState({ animatedValue: null }); + const onAnimationEnd = ({ finished }: Types.Animated.EndResult) => { + if (finished) { + this.animation = undefined; + + // reset the height after transition to let element layout naturally + // if animation finished without interruption + if (expand) { + this.setState({ applyAnimatedStyle: false }); + } } - } - }; + }; - private contentLayoutDidChange = ({ height }: Types.ViewOnLayoutEvent) => - (this.contentHeight = height); + this.setState({ applyAnimatedStyle: true }, () => { + animation.start(onAnimationEnd); + }); + } } |
