UNPKG

preact-css-transition-group

Version:

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

210 lines (187 loc) 6.12 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'; 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> ); } }