Created
August 5, 2017 02:57
-
-
Save LKay/71e0b0d092b5bc1b30a52761cd70d403 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
import { | |
createElement, | |
cloneElement, | |
Component, | |
HTMLAttributes, | |
ReactElement, | |
ReactType, | |
CSSProperties, ReactNode, ReactInstance | |
} from "react"; | |
import * as PropTypes from "prop-types"; | |
import * as invariant from "invariant"; | |
import { TransitionActions, TransitionProps, EnterHandler, ExitHandler } from "react-transition-group/Transition"; | |
import * as classNames from "classnames"; | |
export interface TransitionReplaceClassNames { | |
height?: string; | |
heightActive?: string; | |
} | |
type ChildWrapper<P = any> = (child: ReactElement<any>, props?: P) => ReactElement<any>; | |
export interface TransitionReplaceBaseProps extends HTMLAttributes<any> { | |
changeWidth?: boolean; | |
childWrapper?: ChildWrapper; | |
classNames?: string | TransitionReplaceClassNames; | |
overflowHidden?: boolean; | |
timeout?: number; | |
} | |
export interface IntrinsicTransitionReplaceProps<T extends keyof JSX.IntrinsicElements = "div"> extends TransitionActions, TransitionReplaceBaseProps { | |
component?: T; | |
} | |
export interface ComponentTransitionReplaceProps<T extends ReactType> extends TransitionActions, TransitionReplaceBaseProps { | |
component: T; | |
} | |
export type TransitionReplaceProps<T extends keyof JSX.IntrinsicElements = "div", V extends ReactType = any> = | |
(IntrinsicTransitionReplaceProps<T> & JSX.IntrinsicElements[T]) | (ComponentTransitionReplaceProps<V>) & { | |
children?: ReactElement<TransitionProps>; | |
}; | |
interface TransitionReplaceState { | |
activeTransition: boolean; | |
currentChild: ReactElement<TransitionProps>; | |
height: number; | |
nextChild: ReactElement<TransitionProps>; | |
width: number; | |
} | |
function getNodeSize(node: Element): { height: number, width: number } { | |
const { height, width } = node ? node.getBoundingClientRect() : { height: 0, width: 0 }; | |
return { height, width }; | |
} | |
function validateChildren(children: ReactNode): void { | |
invariant( | |
React.Children.count(children) <= 1, | |
"A <TransitionReplace> may have only one child element" | |
); | |
} | |
function childWrapper<P = TransitionProps>(child: ReactElement<P>, props?: P): ReactElement<any> { | |
return createElement("div", props, child); | |
} | |
class TransitionReplace extends Component<TransitionReplaceProps, TransitionReplaceState> { | |
static propTypes = { | |
appear : PropTypes.bool, | |
changeWidth : PropTypes.bool, | |
children : PropTypes.node, | |
childWrapper : PropTypes.func, | |
classNames : PropTypes.oneOf([PropTypes.string, PropTypes.object]), | |
component : PropTypes.oneOf([PropTypes.string, PropTypes.object]), | |
enter : PropTypes.bool, | |
exit : PropTypes.bool, | |
overflowHidden : PropTypes.bool, | |
timeout : PropTypes.number | |
} | |
static defaultProps = { | |
childWrapper, | |
component : "div", | |
timeout : 0 | |
}; | |
static childContextTypes = { | |
transitionGroup : PropTypes.object.isRequired, | |
}; | |
private appeared: boolean = false; | |
private entering: boolean = false; | |
private exiting: boolean = false; | |
state = { | |
activeTransition : false, | |
currentChild : undefined, | |
height : null, | |
nextChild : undefined, | |
width : null | |
}; | |
getChildContext() { | |
return { | |
transitionGroup : { isMounting: !this.appeared } | |
} | |
} | |
componentWillMount() { | |
const { children } = this.props; | |
validateChildren(children); | |
if (children) { | |
let currentChild: ReactElement<TransitionProps> = React.Children.only(children); | |
currentChild = cloneElement<TransitionProps, Partial<TransitionProps>>(currentChild, { | |
in : true, | |
appear : this.getProp(currentChild, "appear"), | |
enter : this.getProp(currentChild, "enter"), | |
exit : this.getProp(currentChild, "exit") | |
}); | |
// Initial child should be entering, dependent on appear | |
this.setState({ currentChild }); | |
} | |
} | |
componentDidMount() { | |
this.appeared = true; | |
} | |
componentWillReceiveProps(nextProps: TransitionReplaceProps) { | |
validateChildren(nextProps.children); | |
let currentChild: ReactElement<TransitionProps> = this.state.currentChild; | |
let nextChild: ReactElement<TransitionProps>; | |
let height: number = this.state.height; | |
if (nextProps.children) { | |
nextChild = React.Children.only(nextProps.children); | |
} | |
// item hasn't changed transition states | |
// copy over the last transition props | |
if (nextChild && currentChild && currentChild.key === nextChild.key && !this.state.nextChild) { | |
currentChild = cloneElement<TransitionProps, Partial<TransitionProps>>(nextChild, { | |
in : currentChild.props.in, | |
enter : this.getProp(nextChild, "enter", nextProps), | |
exit : this.getProp(nextChild, "exit", nextProps) | |
}); | |
nextChild = undefined | |
} | |
// new item came so replace | |
else if (nextChild && currentChild && currentChild.key !== nextChild.key) { | |
currentChild = cloneElement<TransitionProps, Partial<TransitionProps>>(currentChild, { | |
in : false | |
}); | |
nextChild = cloneElement<TransitionProps, Partial<TransitionProps>>(nextChild, { | |
in : true, | |
enter : this.getProp(nextChild, "enter", nextProps), | |
exit : this.getProp(nextChild, "exit", nextProps) | |
}); | |
} | |
// no new item so remove current (exiting) | |
else if (!nextChild && !!currentChild && currentChild.props.in) { | |
currentChild = cloneElement<TransitionProps, Partial<TransitionProps>>(currentChild, { | |
in : false | |
}); | |
nextChild = undefined; | |
} | |
// item is new (entering) | |
else if (nextChild && !currentChild) { | |
nextChild = cloneElement<TransitionProps, Partial<TransitionProps>>(nextChild, { | |
in : true, | |
enter : this.getProp(nextChild, "enter", nextProps), | |
exit : this.getProp(nextChild, "exit", nextProps) | |
}); | |
} | |
this.setState({ | |
activeTransition : this.state.activeTransition || !!nextChild, | |
currentChild, | |
nextChild | |
}); | |
} | |
handleExitCurrent(handler: ExitHandler): ExitHandler { | |
return (node: HTMLElement) => { | |
const { height, width } = getNodeSize(node); | |
console.warn("EXIT SIZE", width, height) | |
if (!this.exiting) { | |
this.setState({ | |
activeTransition : true, | |
height, | |
width | |
}, () => { | |
if (typeof handler === "function") { | |
handler(node); | |
} | |
}); | |
} | |
this.exiting = true; | |
} | |
} | |
handleExitedCurrent(handler: ExitHandler): ExitHandler { | |
return (node: HTMLElement) => { | |
this.exiting = false; | |
if (!this.entering) { | |
return this.setState({ | |
activeTransition : false, | |
currentChild : this.state.nextChild, | |
nextChild : undefined | |
}, () => { | |
if (typeof handler === "function") { | |
handler(node); | |
} | |
}); | |
} | |
} | |
} | |
handleEnterNext(handler: EnterHandler): EnterHandler { | |
return (node: HTMLElement, isAppearing: boolean) => { | |
const { currentChild } = this.state; | |
const { height, width } = getNodeSize(node); | |
console.warn("ENTER SIZE", width, height) | |
if (!this.entering) { | |
this.setState({ | |
activeTransition : true, | |
height, | |
width | |
}, () => { | |
if (typeof handler === "function") { | |
handler(node, isAppearing); | |
} | |
}); | |
} | |
this.entering = true; | |
} | |
} | |
handleEnteredNext(handler: EnterHandler): EnterHandler { | |
return (node: HTMLElement, isAppearing: boolean) => { | |
this.entering = false; | |
if (!this.exiting) { | |
return this.setState({ | |
activeTransition : false, | |
currentChild : this.state.nextChild, | |
nextChild : undefined | |
}, () => { | |
if (typeof handler === "function") { | |
handler(node, isAppearing); | |
} | |
}); | |
} | |
} | |
} | |
// use child config unless explictly set by the TransitionReplace component | |
private getProp(child: ReactElement<TransitionProps>, | |
prop: keyof TransitionActions, | |
props: TransitionReplaceProps = this.props) { | |
return props[prop] !== null ? props[prop] : child.props[prop]; | |
} | |
render() { | |
const { | |
appear, | |
changeWidth, | |
childWrapper, | |
classNames : classes, | |
component : Component, | |
enter, | |
exit, | |
overflowHidden, | |
timeout, | |
...props | |
} = this.props; | |
const { | |
activeTransition, | |
currentChild, | |
height, | |
nextChild, | |
width | |
} = this.state; | |
const children: Array<ReactElement<TransitionProps>> = []; | |
let style: CSSProperties = props.style; | |
let className: string = props.className; | |
if (currentChild) { | |
children.push( | |
cloneElement<TransitionProps, Partial<TransitionProps>>(currentChild, {key : currentChild.key, | |
onExit : this.handleExitCurrent(currentChild.props.onExit), | |
onExited : this.handleExitedCurrent(currentChild.props.onExited), | |
}) | |
); | |
} | |
if (nextChild) { | |
const propsStyle = { | |
style : activeTransition | |
? { ...nextChild.props.style, | |
position : "absolute", | |
top : 0, | |
left : 0, | |
width : "inherit" | |
} : nextChild.props.style | |
}; | |
children.push( | |
cloneElement<TransitionProps, Partial<TransitionProps>>(nextChild, { | |
onEnter : this.handleEnterNext(nextChild.props.onEnter), | |
onEntered : this.handleEnteredNext(nextChild.props.onEntered), | |
}, activeTransition | |
? childWrapper(nextChild.props.children, propsStyle) | |
: nextChild.props.children | |
) | |
); | |
} | |
if (activeTransition) { | |
style = { | |
...style, | |
display : "block", | |
height : (!currentChild || !nextChild) ? 0 : height, | |
position : "relative", | |
width : "100%" | |
}; | |
if (!classes) { | |
style = { ...style, transition: `height ${timeout}ms linear` } | |
} else { | |
const heightClassName = (typeof classes === "object" && classes !== null) | |
? classes.height || "" | |
: `${classNames}-height`; | |
const activeHeightClassName = (typeof classes === "object" && classes !== null) | |
? classes.heightActive || "" | |
: `${heightClassName}-active`; | |
className = classNames( | |
className, | |
heightClassName, | |
nextChild && activeTransition ? activeHeightClassName : null | |
); | |
} | |
if (overflowHidden) { | |
style = { ...style, overflow: "hidden" }; | |
} | |
if (changeWidth) { | |
style = { ...style, width }; | |
} | |
} | |
console.warn("RENDER", activeTransition, height) | |
return ( | |
<Component | |
{...props} | |
style={ style } | |
> | |
{ children } | |
</Component> | |
); | |
} | |
} | |
export default TransitionReplace; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment