accounts-frontend/src/components/auth/PanelTransition.jsx

342 lines
11 KiB
React
Raw Normal View History

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { TransitionMotion, spring } from 'react-motion';
import ReactHeight from 'react-height';
import { Panel, PanelBody, PanelFooter, PanelHeader } from 'components/ui/Panel';
import { Form } from 'components/ui/Form';
import {helpLinks as helpLinksStyles} from 'components/auth/helpLinks.scss';
import panelStyles from 'components/ui/panel.scss';
import icons from 'components/ui/icons.scss';
2016-03-02 02:06:14 +05:30
import authFlow from 'services/authFlow';
import * as actions from './actions';
2016-03-03 10:53:17 +05:30
const opacitySpringConfig = {stiffness: 300, damping: 20};
const transformSpringConfig = {stiffness: 500, damping: 50};
const changeContextSpringConfig = {stiffness: 500, damping: 20};
class PanelTransition extends Component {
static displayName = 'PanelTransition';
static propTypes = {
auth: PropTypes.shape({
error: PropTypes.string,
login: PropTypes.shape({
login: PropTypes.string,
password: PropTypes.string
})
}).isRequired,
setError: React.PropTypes.func.isRequired,
clearErrors: React.PropTypes.func.isRequired,
path: PropTypes.string.isRequired,
Title: PropTypes.element.isRequired,
Body: PropTypes.element.isRequired,
Footer: PropTypes.element.isRequired,
Links: PropTypes.element.isRequired
};
state = {
height: {},
contextHeight: 0
};
componentWillReceiveProps(nextProps) {
var nextPath = nextProps.path;
var previousPath = this.props.path;
if (nextPath !== previousPath) {
var direction = this.getDirection(nextPath, previousPath);
var forceHeight = direction === 'Y' && nextPath !== previousPath ? 1 : 0;
this.props.clearErrors();
this.setState({
direction,
forceHeight,
previousPath
});
if (forceHeight) {
setTimeout(() => {
this.setState({forceHeight: 0});
}, 100);
}
}
}
render() {
const {height, canAnimateHeight, contextHeight, forceHeight} = this.state;
const {path, Title, Body, Footer, Links} = this.props;
return (
<TransitionMotion
2016-03-03 10:53:17 +05:30
styles={[
{key: path, data: {Title, Body, Footer, Links, hasBackButton: Title.type.goBack}, style: {
transformSpring: spring(0, transformSpringConfig),
opacitySpring: spring(1, opacitySpringConfig)
2016-03-03 10:53:17 +05:30
}},
{key: 'common', style: {
heightSpring: spring(forceHeight || height[path] || 0, transformSpringConfig),
switchContextHeightSpring: spring(forceHeight || contextHeight, changeContextSpringConfig)
2016-03-03 10:53:17 +05:30
}}
]}
willEnter={this.willEnter}
willLeave={this.willLeave}
>
{(items) => {
2016-03-03 10:53:17 +05:30
const panels = items.filter(({key}) => key !== 'common');
const common = items.filter(({key}) => key === 'common')[0];
const contentHeight = {
overflow: 'hidden',
2016-03-03 10:53:17 +05:30
height: forceHeight ? common.switchContextHeightSpring : 'auto'
};
const bodyHeight = {
position: 'relative',
2016-03-03 10:53:17 +05:30
height: `${canAnimateHeight ? common.style.heightSpring : height[path]}px`
};
return (
<Form id={path} onSubmit={this.onFormSubmit} onInvalid={this.onFormInvalid}>
<Panel>
<PanelHeader>
2016-03-03 10:53:17 +05:30
{panels.map((config) => this.getHeader(config))}
</PanelHeader>
<div style={contentHeight}>
<ReactHeight onHeightReady={this.onUpdateContextHeight}>
<PanelBody>
<div style={bodyHeight}>
2016-03-03 10:53:17 +05:30
{panels.map((config) => this.getBody(config))}
</div>
</PanelBody>
<PanelFooter>
2016-03-03 10:53:17 +05:30
{panels.map((config) => this.getFooter(config))}
</PanelFooter>
</ReactHeight>
</div>
</Panel>
<div className={helpLinksStyles}>
2016-03-03 10:53:17 +05:30
{panels.map((config) => this.getLinks(config))}
</div>
</Form>
);
}}
</TransitionMotion>
);
}
onFormSubmit = () => {
this.body.onFormSubmit();
};
onFormInvalid = (errorMessage) => {
this.props.setError(errorMessage);
};
2016-03-03 10:53:17 +05:30
willEnter = (config) => this.getTransitionStyles(config);
willLeave = (config) => this.getTransitionStyles(config, {isLeave: true});
/**
2016-03-03 10:53:17 +05:30
* @param {Object} config
* @param {Object} [options]
* @param {Object} [options.isLeave=false] - true, if this is a leave transition
*
* @return {Object}
*/
2016-03-03 10:53:17 +05:30
getTransitionStyles({key}, options = {}) {
const {isLeave = false} = options;
2016-03-03 10:53:17 +05:30
const map = {
'/login': -1,
'/register': -1,
'/password': 1,
'/activation': 1,
'/oauth/permissions': -1,
'/password-change': 1,
'/forgot-password': 1
};
2016-03-03 10:53:17 +05:30
const sign = map[key];
const transform = sign * 100;
return {
2016-03-03 10:53:17 +05:30
transformSpring: isLeave ? spring(transform, transformSpringConfig) : transform,
opacitySpring: isLeave ? spring(0, opacitySpringConfig) : 1
};
}
getDirection(next, prev) {
var not = (path) => prev !== path && next !== path;
var map = {
'/login': not('/password') && not('/forgot-password') ? 'Y' : 'X',
'/password': not('/login') && not('/forgot-password') ? 'Y' : 'X',
'/register': not('/activation') ? 'Y' : 'X',
'/activation': not('/register') ? 'Y' : 'X',
'/oauth/permissions': 'Y',
'/password-change': 'Y',
'/forgot-password': not('/password') && not('/login') ? 'Y' : 'X'
};
return map[next];
}
onUpdateHeight = (height) => {
const canAnimateHeight = Object.keys(this.state.height).length > 1 || this.state.height[[this.props.path]];
this.setState({
canAnimateHeight,
height: {
...this.state.height,
[this.props.path]: height
}
});
};
onUpdateContextHeight = (height) => {
this.setState({
contextHeight: height
});
};
onGoBack = (event) => {
event.preventDefault();
2016-03-02 02:06:14 +05:30
authFlow.goBack();
};
2016-03-03 10:53:17 +05:30
getHeader({key, style, data}) {
const {Title, hasBackButton} = data;
const {transformSpring} = style;
2016-03-03 10:53:17 +05:30
style = {
...this.getDefaultTransitionStyles(key, style),
opacity: 1 // reset default
};
2016-03-03 10:53:17 +05:30
const scrollStyle = this.translate(transformSpring, 'Y');
2016-03-03 10:53:17 +05:30
const sideScrollStyle = {
position: 'relative',
zIndex: 2,
...this.translate(-Math.abs(transformSpring))
};
2016-03-03 10:53:17 +05:30
const backButton = (
<button style={sideScrollStyle} type="button" onClick={this.onGoBack} className={panelStyles.headerControl}>
<span className={icons.arrowLeft} />
</button>
);
return (
<div key={`header${key}`} style={style}>
{hasBackButton ? backButton : null}
<div style={scrollStyle}>
{React.cloneElement(Title, this.props)}
</div>
</div>
);
}
2016-03-03 10:53:17 +05:30
getBody({key, style, data}) {
const {Body} = data;
const {transformSpring} = style;
const {direction} = this.state;
2016-03-03 10:53:17 +05:30
let transform = this.translate(transformSpring, direction);
let verticalOrigin = 'top';
if (direction === 'Y') {
verticalOrigin = 'bottom';
transform = {};
}
2016-03-03 10:53:17 +05:30
style = {
...this.getDefaultTransitionStyles(key, style),
top: 'auto', // reset default
[verticalOrigin]: 0,
...transform
};
return (
<ReactHeight key={`body${key}`} style={style} onHeightReady={this.onUpdateHeight}>
{React.cloneElement(Body, {
...this.props,
ref: (body) => {
this.body = body;
}
})}
</ReactHeight>
);
}
2016-03-03 10:53:17 +05:30
getFooter({key, style, data}) {
const {Footer} = data;
2016-03-03 10:53:17 +05:30
style = this.getDefaultTransitionStyles(key, style);
return (
<div key={`footer${key}`} style={style}>
{React.cloneElement(Footer, this.props)}
</div>
);
}
2016-03-03 10:53:17 +05:30
getLinks({key, style, data}) {
const {Links} = data;
2016-03-03 10:53:17 +05:30
style = this.getDefaultTransitionStyles(key, style);
return (
<div key={`links${key}`} style={style}>
{React.cloneElement(Links, this.props)}
</div>
);
}
/**
2016-03-03 10:53:17 +05:30
* @param {string} key
* @param {Object} props
* @param {number} props.opacitySpring
*
* @return {Object}
*/
2016-03-03 10:53:17 +05:30
getDefaultTransitionStyles(key, {opacitySpring}) {
return {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
opacity: opacitySpring,
2016-03-03 10:53:17 +05:30
pointerEvents: key === this.props.path ? 'auto' : 'none'
};
}
/**
* @param {number} value
* @param {string} direction='X' - X|Y
* @param {string} unit='%' - %|px etc
*
* @return {Object}
*/
translate(value, direction = 'X', unit = '%') {
return {
WebkitTransform: `translate${direction}(${value}${unit})`,
transform: `translate${direction}(${value}${unit})`
};
}
}
export default connect((state) => ({
user: state.user,
auth: state.auth,
2016-03-02 02:06:14 +05:30
path: state.routing.location.pathname,
resolve: authFlow.resolve.bind(authFlow),
reject: authFlow.reject.bind(authFlow)
}), {
clearErrors: actions.clearErrors,
setError: actions.setError
})(PanelTransition);