Skip to content

Instantly share code, notes, and snippets.

@LKay
Created August 5, 2017 02:57
Show Gist options
  • Save LKay/71e0b0d092b5bc1b30a52761cd70d403 to your computer and use it in GitHub Desktop.
Save LKay/71e0b0d092b5bc1b30a52761cd70d403 to your computer and use it in GitHub Desktop.
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