UNPKG

preact-css-transition-group-copy

Version:

Apply CSS transitions when adding or removing Preact components/elements.

244 lines (222 loc) 6.26 kB
/** * 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> ); } }