summaryrefslogtreecommitdiffhomepage
path: root/gui/packages/components/src
diff options
context:
space:
mode:
Diffstat (limited to 'gui/packages/components/src')
-rw-r--r--gui/packages/components/src/Accordion.tsx151
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);
+ });
+ }
}