You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

233 lines
6.8 KiB

// @flow
/**
* React Flip Move | propConverter
* (c) 2016-present Joshua Comeau
*
* Abstracted away a bunch of the messy business with props.
* - props flow types and defaultProps
* - Type conversion (We accept 'string' and 'number' values for duration,
* delay, and other fields, but we actually need them to be ints.)
* - Children conversion (we need the children to be an array. May not always
* be, if a single child is passed in.)
* - Resolving animation presets into their base CSS styles
*/
/* eslint-disable block-scoped-var */
import React, {
Component,
Children,
Element,
} from 'react';
import {
statelessFunctionalComponentSupplied,
invalidTypeForTimingProp,
invalidEnterLeavePreset,
deprecatedDisableAnimations,
} from './error-messages';
import {
appearPresets,
enterPresets,
leavePresets,
defaultPreset,
disablePreset,
} from './enter-leave-presets';
import { isElementAnSFC, omit } from './helpers';
import type {
Animation,
AnimationProp,
Presets,
FlipMoveProps,
FlipMoveDefaultProps,
ConvertedProps,
DelegatedProps,
} from './typings';
declare var process: {
env: {
NODE_ENV: 'production' | 'development',
},
};
let nodeEnv: string;
try {
nodeEnv = process.env.NODE_ENV;
} catch (e) {
nodeEnv = 'development';
}
function propConverter(
ComposedComponent: Class<Component<*, ConvertedProps, *>>
): Class<Component<FlipMoveDefaultProps, FlipMoveProps, void>> {
return class FlipMovePropConverter extends Component {
static defaultProps = {
easing: 'ease-in-out',
duration: 350,
delay: 0,
staggerDurationBy: 0,
staggerDelayBy: 0,
typeName: 'div',
enterAnimation: defaultPreset,
leaveAnimation: defaultPreset,
disableAllAnimations: false,
getPosition: node => node.getBoundingClientRect(),
maintainContainerHeight: false,
verticalAlignment: 'top',
};
// eslint-disable-next-line class-methods-use-this
checkForStatelessFunctionalComponents(children: mixed) {
// Skip all console warnings in production.
// Bail early, to avoid unnecessary work.
if (nodeEnv === 'production') {
return;
}
// FlipMove does not support stateless functional components.
// Check to see if any supplied components won't work.
// If the child doesn't have a key, it means we aren't animating it.
// It's allowed to be an SFC, since we ignore it.
const childArray: Array<Element<*>> = Children.toArray(children);
const noStateless = childArray.every(child =>
!isElementAnSFC(child) || typeof child.key === 'undefined'
);
if (!noStateless) {
statelessFunctionalComponentSupplied();
}
}
convertProps(props: FlipMoveProps): ConvertedProps {
const workingProps: ConvertedProps = {
// explicitly bypass the props that don't need conversion
children: props.children,
easing: props.easing,
onStart: props.onStart,
onFinish: props.onFinish,
onStartAll: props.onStartAll,
onFinishAll: props.onFinishAll,
typeName: props.typeName,
disableAllAnimations: props.disableAllAnimations,
getPosition: props.getPosition,
maintainContainerHeight: props.maintainContainerHeight,
verticalAlignment: props.verticalAlignment,
// Do string-to-int conversion for all timing-related props
duration: this.convertTimingProp('duration'),
delay: this.convertTimingProp('delay'),
staggerDurationBy: this.convertTimingProp('staggerDurationBy'),
staggerDelayBy: this.convertTimingProp('staggerDelayBy'),
// Our enter/leave animations can be specified as boolean (default or
// disabled), string (preset name), or object (actual animation values).
// Let's standardize this so that they're always objects
appearAnimation: this.convertAnimationProp(
props.appearAnimation, appearPresets
),
enterAnimation: this.convertAnimationProp(
props.enterAnimation, enterPresets
),
leaveAnimation: this.convertAnimationProp(
props.leaveAnimation, leavePresets
),
delegated: {},
};
this.checkForStatelessFunctionalComponents(workingProps.children);
// Accept `disableAnimations`, but add a deprecation warning
if (typeof props.disableAnimations !== 'undefined') {
if (nodeEnv !== 'production') {
deprecatedDisableAnimations();
}
workingProps.disableAllAnimations = props.disableAnimations;
}
// Gather any additional props;
// they will be delegated to the ReactElement created.
const primaryPropKeys = Object.keys(workingProps);
const delegatedProps: DelegatedProps = omit(this.props, primaryPropKeys);
// The FlipMove container element needs to have a non-static position.
// We use `relative` by default, but it can be overridden by the user.
// Now that we're delegating props, we need to merge this in.
delegatedProps.style = {
position: 'relative',
...delegatedProps.style,
};
workingProps.delegated = delegatedProps;
return workingProps;
}
convertTimingProp(prop: string): number {
const rawValue: string | number = this.props[prop];
const value = typeof rawValue === 'number'
? rawValue
: parseInt(rawValue, 10);
if (isNaN(value)) {
const defaultValue: number = FlipMovePropConverter.defaultProps[prop];
if (nodeEnv !== 'production') {
invalidTypeForTimingProp({
prop,
value: rawValue,
defaultValue,
});
}
return defaultValue;
}
return value;
}
// eslint-disable-next-line class-methods-use-this
convertAnimationProp(animation: ?AnimationProp, presets: Presets): ?Animation {
switch (typeof animation) {
case 'boolean': {
// If it's true, we want to use the default preset.
// If it's false, we want to use the 'none' preset.
return presets[
animation ? defaultPreset : disablePreset
];
}
case 'string': {
const presetKeys = Object.keys(presets);
if (presetKeys.indexOf(animation) === -1) {
if (nodeEnv !== 'production') {
invalidEnterLeavePreset({
value: animation,
acceptableValues: presetKeys.join(', '),
defaultValue: defaultPreset,
});
}
return presets[defaultPreset];
}
return presets[animation];
}
default: {
return animation;
}
}
}
render() {
return (
<ComposedComponent {...this.convertProps(this.props)} />
);
}
};
}
export default propConverter;