Cover PanelTransition with types

This commit is contained in:
SleepWalker 2019-06-09 11:29:54 +03:00
parent 99fc667a2b
commit afde390401
4 changed files with 141 additions and 36 deletions

View File

@ -10,8 +10,8 @@ import { omit, debounce } from 'functions';
* On each component update the `shouldMeasure` prop is being called and depending of * On each component update the `shouldMeasure` prop is being called and depending of
* the value returned will be decided whether to call `onMeasure`. * the value returned will be decided whether to call `onMeasure`.
* By default `shouldMeasure` will compare the old and new values of the `state` prop. * By default `shouldMeasure` will compare the old and new values of the `state` prop.
* Both `shouldMeasure` and `state` can be used to reduce the amount of meausres, which * Both `shouldMeasure` and `state` can be used to reduce the amount of measures, which
* will recude the count of forced reflows in browser. * will reduce the count of forced reflows in browser.
* *
* Usage: * Usage:
* <MeasureHeight * <MeasureHeight
@ -23,10 +23,10 @@ import { omit, debounce } from 'functions';
* </MeasureHeight> * </MeasureHeight>
*/ */
type ChildState = { [key: string]: any }; type ChildState = mixed;
export default class MeasureHeight extends PureComponent<{ export default class MeasureHeight extends PureComponent<{
shouldMeasure: (prevState: any, newState: any) => bool, shouldMeasure: (prevState: ChildState, newState: ChildState) => bool,
onMeasure: (height: number) => void, onMeasure: (height: number) => void,
state: ChildState state: ChildState
}> { }> {

View File

@ -1,3 +1,3 @@
// @flow // @flow
export type { State as AccountsState, Account } from './reducer';
export { default as AccountSwitcher } from './AccountSwitcher'; export { default as AccountSwitcher } from './AccountSwitcher';
export type { Account } from './reducer';

View File

@ -1,3 +1,7 @@
// @flow
import type { User } from 'components/user';
import type { AccountsState } from 'components/accounts';
import type { Node, Element } from 'react';
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -56,7 +60,64 @@ if (process.env.NODE_ENV !== 'production') {
}, {}); }, {});
} }
class PanelTransition extends Component { type ValidationError = string | {
type: string,
payload: { [key: string]: any },
};
type AnimationProps = {|
opacitySpring: number,
transformSpring: number,
|};
type AnimationContext = {
key: string,
style: AnimationProps,
data: {
Title: Node,
Body: Element<any>,
Footer: Node,
Links: Node,
hasBackButton: bool,
}
};
type Props = {
// context props
auth: {
error: string | {
type: string,
payload: {[key: string]: any},
},
isLoading: bool,
login: string,
},
user: User,
accounts: AccountsState,
setErrors: ({ [key: string]: ValidationError }) => void,
clearErrors: () => void,
resolve: () => void,
reject: () => void,
// local props
Title: Node,
Body: typeof Component,
Footer: Node,
Links: Node,
children: Node
};
type State = {
contextHeight: number,
panelId: string | void,
prevPanelId: string | void,
isHeightDirty: bool,
forceHeight: 1 | 0,
direction: 'X' | 'Y',
};
class PanelTransition extends Component<Props, State> {
static displayName = 'PanelTransition'; static displayName = 'PanelTransition';
static propTypes = { static propTypes = {
@ -106,9 +167,20 @@ class PanelTransition extends Component {
state = { state = {
contextHeight: 0, contextHeight: 0,
panelId: this.props.Body && this.props.Body.type.panelId panelId: this.props.Body && (this.props.Body: any).type.panelId,
isHeightDirty: false,
forceHeight: 0,
direction: 'X',
prevPanelId: undefined,
}; };
isHeightMeasured: bool = false;
wasAutoFocused: bool = false;
body: null | {
autoFocus: () => void,
onFormSubmit: () => void,
} = null;
timerIds = []; // this is a list of a probably running timeouts to clean on unmount timerIds = []; // this is a list of a probably running timeouts to clean on unmount
getChildContext() { getChildContext() {
@ -136,8 +208,8 @@ class PanelTransition extends Component {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const nextPanel = nextProps.Body && nextProps.Body.type.panelId; const nextPanel = nextProps.Body && (nextProps.Body: any).type.panelId;
const prevPanel = this.props.Body && this.props.Body.type.panelId; const prevPanel = this.props.Body && (this.props.Body: any).type.panelId;
if (nextPanel !== prevPanel) { if (nextPanel !== prevPanel) {
const direction = this.getDirection(nextPanel, prevPanel); const direction = this.getDirection(nextPanel, prevPanel);
@ -177,7 +249,10 @@ class PanelTransition extends Component {
throw new Error('Title, Body, Footer and Links are required'); throw new Error('Title, Body, Footer and Links are required');
} }
const {panelId, hasGoBack} = Body.type; const {panelId, hasGoBack}: {
panelId: string,
hasGoBack: bool,
} = (Body: any).type;
const formHeight = this.state[`formHeight${panelId}`] || 0; const formHeight = this.state[`formHeight${panelId}`] || 0;
@ -255,7 +330,10 @@ class PanelTransition extends Component {
onFormSubmit = () => { onFormSubmit = () => {
this.props.clearErrors(); this.props.clearErrors();
this.body.onFormSubmit();
if (this.body) {
this.body.onFormSubmit();
}
}; };
onFormInvalid = (errors) => this.props.setErrors(errors); onFormInvalid = (errors) => this.props.setErrors(errors);
@ -271,7 +349,10 @@ class PanelTransition extends Component {
* *
* @return {object} * @return {object}
*/ */
getTransitionStyles({key}, options = {}) { getTransitionStyles({key}, options = {}): {|
transformSpring: number,
opacitySpring: number,
|} {
const {isLeave = false} = options; const {isLeave = false} = options;
const {panelId, prevPanelId} = this.state; const {panelId, prevPanelId} = this.state;
@ -279,6 +360,11 @@ class PanelTransition extends Component {
const fromRight = 1; const fromRight = 1;
const currentContext = contexts.find((context) => context.includes(key)); const currentContext = contexts.find((context) => context.includes(key));
if (!currentContext) {
throw new Error(`Can not find settings for ${key} panel`);
}
let sign = currentContext.indexOf(panelId) > currentContext.indexOf(prevPanelId) let sign = currentContext.indexOf(panelId) > currentContext.indexOf(prevPanelId)
? fromRight ? fromRight
: fromLeft; : fromLeft;
@ -294,8 +380,14 @@ class PanelTransition extends Component {
}; };
} }
getDirection(next, prev) { getDirection(next, prev): 'X' | 'Y' {
return contexts.find((context) => context.includes(prev)).includes(next) ? 'X' : 'Y'; const context = contexts.find((context) => context.includes(prev));
if (!context) {
throw new Error(`Can not find context for transition ${prev} -> ${next}`);
}
return context.includes(next) ? 'X' : 'Y';
} }
onUpdateHeight = (height, key) => { onUpdateHeight = (height, key) => {
@ -340,13 +432,17 @@ class PanelTransition extends Component {
shouldMeasureHeight() { shouldMeasureHeight() {
const errorString = Object.values(this.props.auth.error || {}) const errorString = Object.values(this.props.auth.error || {})
.reduce((acc, item) => { .reduce(
if (typeof item === 'string') { // $FlowFixMe
return acc + item; (acc, item: ValidationError) => {
} if (typeof item === 'string') {
return acc + item;
}
return acc + item.type; return acc + item.type;
}, ''); },
''
);
return [ return [
errorString, errorString,
@ -356,7 +452,7 @@ class PanelTransition extends Component {
].join(''); ].join('');
} }
getHeader({key, style, data}) { getHeader({key, style, data}: AnimationContext) {
const {Title} = data; const {Title} = data;
const {transformSpring} = style; const {transformSpring} = style;
@ -366,7 +462,7 @@ class PanelTransition extends Component {
hasBackButton = hasBackButton(this.props); hasBackButton = hasBackButton(this.props);
} }
style = { const transitionStyle = {
...this.getDefaultTransitionStyles(key, style), ...this.getDefaultTransitionStyles(key, style),
opacity: 1 // reset default opacity: 1 // reset default
}; };
@ -391,7 +487,7 @@ class PanelTransition extends Component {
); );
return ( return (
<div key={`header/${key}`} style={style}> <div key={`header/${key}`} style={transitionStyle}>
{hasBackButton ? backButton : null} {hasBackButton ? backButton : null}
<div style={scrollStyle}> <div style={scrollStyle}>
{Title} {Title}
@ -400,19 +496,20 @@ class PanelTransition extends Component {
); );
} }
getBody({key, style, data}) { getBody({key, style, data}: AnimationContext) {
const {Body} = data; const {Body} = data;
const {transformSpring} = style; const {transformSpring} = style;
const {direction} = this.state; const {direction} = this.state;
let transform = this.translate(transformSpring, direction); let transform = this.translate(transformSpring, direction);
let verticalOrigin = 'top'; let verticalOrigin = 'top';
if (direction === 'Y') { if (direction === 'Y') {
verticalOrigin = 'bottom'; verticalOrigin = 'bottom';
transform = {}; transform = {};
} }
style = { const transitionStyle = {
...this.getDefaultTransitionStyles(key, style), ...this.getDefaultTransitionStyles(key, style),
top: 'auto', // reset default top: 'auto', // reset default
[verticalOrigin]: 0, [verticalOrigin]: 0,
@ -422,7 +519,7 @@ class PanelTransition extends Component {
return ( return (
<MeasureHeight <MeasureHeight
key={`body/${key}`} key={`body/${key}`}
style={style} style={transitionStyle}
state={this.shouldMeasureHeight()} state={this.shouldMeasureHeight()}
onMeasure={(height) => this.onUpdateHeight(height, key)} onMeasure={(height) => this.onUpdateHeight(height, key)}
> >
@ -435,25 +532,25 @@ class PanelTransition extends Component {
); );
} }
getFooter({key, style, data}) { getFooter({key, style, data}: AnimationContext) {
const {Footer} = data; const {Footer} = data;
style = this.getDefaultTransitionStyles(key, style); const transitionStyle = this.getDefaultTransitionStyles(key, style);
return ( return (
<div key={`footer/${key}`} style={style}> <div key={`footer/${key}`} style={transitionStyle}>
{Footer} {Footer}
</div> </div>
); );
} }
getLinks({key, style, data}) { getLinks({key, style, data}: AnimationContext) {
const {Links} = data; const {Links} = data;
style = this.getDefaultTransitionStyles(key, style); const transitionStyle = this.getDefaultTransitionStyles(key, style);
return ( return (
<div key={`links/${key}`} style={style}> <div key={`links/${key}`} style={transitionStyle}>
{Links} {Links}
</div> </div>
); );
@ -466,7 +563,14 @@ class PanelTransition extends Component {
* *
* @return {object} * @return {object}
*/ */
getDefaultTransitionStyles(key, {opacitySpring}) { getDefaultTransitionStyles(key: string, {opacitySpring}: $ReadOnly<AnimationProps>): {|
position: string,
top: number,
left: number,
width: string,
opacity: number,
pointerEvents: string,
|} {
return { return {
position: 'absolute', position: 'absolute',
top: 0, top: 0,

View File

@ -1,4 +1,5 @@
// @flow // @flow
import type { Node } from 'react';
import React, { Component } from 'react'; import React, { Component } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@ -9,9 +10,9 @@ import styles from './panel.scss';
import icons from './icons.scss'; import icons from './icons.scss';
export function Panel(props: { export function Panel(props: {
title: string, title?: string,
icon: string, icon?: string,
children: * children: Node,
}) { }) {
let { title, icon } = props; let { title, icon } = props;