preact-css-transition-group-copy
Version:
Apply CSS transitions when adding or removing Preact components/elements.
244 lines (222 loc) • 6.26 kB
JSX
/**
* Copyright 2013-2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* Additional credit to the Author of rc-css-transition-group: https://github.com/yiminghe
* File originally extracted from the React source, converted to ES6 by https://github.com/developit
*/
import { h, cloneElement, Component } from "preact";
import { getKey, filterNullChildren } from "./util";
import {
mergeChildMappings,
isShownInChildren,
isShownInChildrenByKey,
inChildren,
inChildrenByKey,
} from "./TransitionChildMapping";
import { CSSTransitionGroupChild } from "./CSSTransitionGroupChild.jsx";
export class CSSTransitionGroup extends Component {
static defaultProps = {
component: "span",
transitionEnter: true,
transitionLeave: true,
};
constructor(props) {
super();
this.refs = {};
this.state = {
children: (props.children || []).slice(),
};
}
shouldComponentUpdate(_, { children }) {
return children !== this.state.children;
}
componentWillMount() {
this.currentlyTransitioningKeys = {};
this.keysToEnter = [];
this.keysToLeave = [];
}
componentWillReceiveProps({ children, exclusive, showProp }) {
let nextChildMapping = filterNullChildren(children || []).slice();
// last props children if exclusive
let prevChildMapping = filterNullChildren(
exclusive ? this.props.children : this.state.children
);
let newChildren = mergeChildMappings(prevChildMapping, nextChildMapping);
if (showProp) {
newChildren = newChildren.map((c) => {
if (
!c.props[showProp] &&
isShownInChildren(prevChildMapping, c, showProp)
) {
c = cloneElement(c, { [showProp]: true });
}
return c;
});
}
if (exclusive) {
// make middle state children invalid
// restore to last props children
newChildren.forEach((c) => this.stop(getKey(c)));
}
this.setState({ children: newChildren });
this.forceUpdate();
nextChildMapping.forEach((c) => {
let { key } = c,
hasPrev = prevChildMapping && inChildren(prevChildMapping, c);
if (showProp) {
if (hasPrev) {
let showInPrev = isShownInChildren(prevChildMapping, c, showProp),
showInNow = c.props[showProp];
if (
!showInPrev &&
showInNow &&
!this.currentlyTransitioningKeys[key]
) {
this.keysToEnter.push(key);
}
}
} else if (!hasPrev && !this.currentlyTransitioningKeys[key]) {
this.keysToEnter.push(key);
}
});
prevChildMapping.forEach((c) => {
let { key } = c,
hasNext = nextChildMapping && inChildren(nextChildMapping, c);
if (showProp) {
if (hasNext) {
let showInNext = isShownInChildren(nextChildMapping, c, showProp);
let showInNow = c.props[showProp];
if (
!showInNext &&
showInNow &&
!this.currentlyTransitioningKeys[key]
) {
this.keysToLeave.push(key);
}
}
} else if (!hasNext && !this.currentlyTransitioningKeys[key]) {
this.keysToLeave.push(key);
}
});
}
performEnter(key) {
this.currentlyTransitioningKeys[key] = true;
let component = this.refs[key];
if (component.componentWillEnter) {
component.componentWillEnter(() => this._handleDoneEntering(key));
} else {
this._handleDoneEntering(key);
}
}
_handleDoneEntering(key) {
delete this.currentlyTransitioningKeys[key];
let currentChildMapping = filterNullChildren(this.props.children),
showProp = this.props.showProp;
if (
!currentChildMapping ||
(!showProp && !inChildrenByKey(currentChildMapping, key)) ||
(showProp && !isShownInChildrenByKey(currentChildMapping, key, showProp))
) {
// This was removed before it had fully entered. Remove it.
this.performLeave(key);
} else {
this.setState({ children: currentChildMapping });
// this.forceUpdate();
}
}
stop(key) {
delete this.currentlyTransitioningKeys[key];
let component = this.refs[key];
if (component) component.stop();
}
performLeave(key) {
this.currentlyTransitioningKeys[key] = true;
let component = this.refs[key];
if (component && component.componentWillLeave) {
component.componentWillLeave(() => this._handleDoneLeaving(key));
} else {
// Note that this is somewhat dangerous b/c it calls setState()
// again, effectively mutating the component before all the work
// is done.
this._handleDoneLeaving(key);
}
}
_handleDoneLeaving(key) {
delete this.currentlyTransitioningKeys[key];
let showProp = this.props.showProp,
currentChildMapping = filterNullChildren(this.props.children);
if (
showProp &&
currentChildMapping &&
isShownInChildrenByKey(currentChildMapping, key, showProp)
) {
this.performEnter(key);
} else if (
!showProp &&
currentChildMapping &&
inChildrenByKey(currentChildMapping, key)
) {
// This entered again before it fully left. Add it again.
this.performEnter(key);
} else {
this.setState({ children: currentChildMapping });
// this.forceUpdate();
}
}
componentDidUpdate() {
let { keysToEnter, keysToLeave } = this;
this.keysToEnter = [];
keysToEnter.forEach((k) => this.performEnter(k));
this.keysToLeave = [];
keysToLeave.forEach((k) => this.performLeave(k));
}
renderChild = (child) => {
let {
transitionName,
transitionEnter,
transitionLeave,
transitionEnterTimeout,
transitionLeaveTimeout,
} = this.props,
key = getKey(child);
return (
<CSSTransitionGroupChild
key={key}
ref={(c) => {
if (!(this.refs[key] = c)) child = null;
}}
name={transitionName}
enter={transitionEnter}
leave={transitionLeave}
enterTimeout={transitionEnterTimeout}
leaveTimeout={transitionLeaveTimeout}
>
{child}
</CSSTransitionGroupChild>
);
};
render(
{
component: Component,
transitionName,
transitionEnter,
transitionLeave,
transitionEnterTimeout,
transitionLeaveTimeout,
children: c,
...props
},
{ children }
) {
return (
<Component {...props}>
{filterNullChildren(children).map(this.renderChild)}
</Component>
);
}
}