diff --git a/docs/animations-demo-inner/app.css b/docs/animations-demo-inner/app.css new file mode 100644 index 000000000..911c08db8 --- /dev/null +++ b/docs/animations-demo-inner/app.css @@ -0,0 +1,175 @@ +/* Using CSS-vars for transitions so you can experiment easily */ +:root { + --infernoAnimationEnter: all 1.2s ease-out; + --infernoAnimationLeave: all .6s ease-out; +} + +/*******************************************/ +/* Animate height and opacity of card
  • */ +/*******************************************/ +.inner-leave { + /* Leave animation start state */ + opacity: 1; + transform: translateX(0); +} + +.inner-leave-active { + /* Leave animation transitions */ + overflow: visible; + transition: var(--infernoAnimationLeave); + pointer-events: none; /* prevent hover to fire transition events */ +} + +.inner-leave-end { + /* Leave animation end state */ + opacity: 0; + transform: translateX(-100%); +} + +.inner-enter { + /* Enter animation start state */ + opacity: 0.5 ; + transform: translateX(50%); +} + +.inner-enter-active { + /* Enter animation transitions */ + transition: var(--infernoAnimationEnter); + pointer-events: none; /* prevent hover to fire transition events */ +} + +.inner-enter-end { + /* Enter animation end state */ + opacity: 1; + transform: translateX(0); +} + + +/* Some CSS for the list and cards */ +.page { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.page img { + margin: 20vh auto 0; + display: block; + background-color: white; +} + +.page h3 { + background-color: white; + margin: 1rem; + text-align: center; +} + +.page p { + background-color: white; + margin: 1rem auto; + max-width: 20rem; + text-align: left; +} + +.inner { + max-width: 30rem; + margin: 10vh auto 2rem; + padding: 4rem; + background-color: #eee; +} + +.inner h2 { + font-size: 3em; + margin: 3rem auto; + text-align: center; +} + +/* Some general CSS for the example */ + +button { + padding: 0.5rem 1rem; + margin: 0.5rem auto 0; + display: block; + background: #35a748; + color: white; + border-style: none; + border-radius: 4px; + width: 10rem; +} button:hover { + background: #3775bb; +} + +body { + font-family: helvetica; + box-sizing: border-box; + margin: 0; + padding: 1rem; + display: flex; + gap: 2rem; + justify-content: center; + align-items: flex-end; + height: 100vh; + overflow-y: scroll; + overflow-x: hidden; +} + +.App { + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding: 1rem; + width: calc(100vw - 2rem); + height: 100vh; +} + +h1 { + position: absolute; + bottom: 0; + left: 0; + transform: rotate(-90deg); + font-size: 2rem; + font-weight: 800; + color: #ddd; + text-align: center; + margin: 0.5rem; + white-space: nowrap; + width: 2rem; + transform-origin: 50% 50%; +} + +h1 small { + font-size: 0.5em; +} +h1 small a { + color: #4A90E2; + text-decoration: none; +} +h1 small a:hover { + color: #3775bb; +} + +h2 { + font-size: 1rem; + font-weight: 100; + color: #4A90E2; + text-align: center; +} + +h3 { + font-size: 0.85rem; + font-weight: 100; +} + +p { + font-size: 0.85rem; + font-weight: 100; + color: #888; + text-align: center; + margin-top: -0.5em; + width: 40rem; + max-width: 100%; +} diff --git a/docs/animations-demo-inner/app.js b/docs/animations-demo-inner/app.js new file mode 100644 index 000000000..cd4b14952 --- /dev/null +++ b/docs/animations-demo-inner/app.js @@ -0,0 +1,90 @@ +(function() { + "use strict"; + var Component = Inferno.Component; + var createElement = Inferno.createElement; + var createRef = Inferno.createRef; + var InfernoAnimation = Inferno.Animation; + + var { + AnimatedComponent, + componentDidAppear, + componentWillDisappear + } = InfernoAnimation; + + var { + addClassName, + removeClassName, + forceReflow, + registerTransitionListener, + } = InfernoAnimation.utils; + + class Page extends Component { + componentDidAppear(dom) { + // We need to store a reference to the animating child that + // isn't removed on unmount. Currently this requires passing + // a ref as property and referencing the .current property + // of that object. + this._innerEl = this.props.innerRef.current; + componentDidAppear(this._innerEl, { animation: "inner" }); + } + + componentWillDisappear(dom, callback) { + componentWillDisappear(this._innerEl, { animation: "inner" }, callback); + } + + render() { + return ( + createElement('div', { + className: "page", + }, createElement('div', { className: "random-wrapper" }, [ + createElement('h3', null, "Page " + this.props.step), + createElement('img', { width: "120px", height: "120px", src: "avatar.png" }), + createElement('p', null, "The entire page is swapped, but we are only animating div.inner. This gives the apperance of only swapping the box below."), + createElement('p', null, "In order not to hide the incoming content we can't set background on div.page. The background needs to be provided by a backdrop in the wizard component."), + createElement('div', { ref: this.props.innerRef, className: "inner"}, [ + createElement('h2', null, "Step " + this.props.step), + createElement('button', {onClick: (e) => { e.preventDefault(); this.props.onNext() }}, "Next") + ]) + ]) + ) + ); + } + } + + const nrofSteps = 3; + + class Wizard extends Component { + constructor() { + super(); + + // Ref objects used to reference the animating children of each page + this._innerAnimRefs = []; + for (let i = 0; i < nrofSteps; i++) { + this._innerAnimRefs.push(createRef()); + } + + this.state = { + showStepIndex: 0 + }; + } + + doGoNext = () => { + this.setState({ + showStepIndex: (this.state.showStepIndex + 1) % nrofSteps + }) + } + + render() { + const { showStepIndex } = this.state; + + return createElement(Page, { key: "page_" + showStepIndex, step: showStepIndex + 1, innerRef: this._innerAnimRefs[showStepIndex], onNext: this.doGoNext }); + } + } + + + document.addEventListener('DOMContentLoaded', function () { + var container_1 = document.querySelector('#App1'); + + Inferno.render(createElement(Wizard), container_1); + }); +})(); diff --git a/docs/animations-demo-inner/avatar.png b/docs/animations-demo-inner/avatar.png new file mode 100644 index 000000000..c007023c5 Binary files /dev/null and b/docs/animations-demo-inner/avatar.png differ diff --git a/docs/animations-demo-inner/dist/bundle.js b/docs/animations-demo-inner/dist/bundle.js new file mode 100644 index 000000000..604d1bf18 --- /dev/null +++ b/docs/animations-demo-inner/dist/bundle.js @@ -0,0 +1 @@ +!function(){"use strict";function e(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,n(e,t)}function n(e,t){return n=Object.setPrototypeOf||function(e,n){return e.__proto__=n,e},n(e,t)}!function(){var n=Inferno.Component,t=Inferno.createElement,r=Inferno.createRef,o=Inferno.Animation;o.AnimatedComponent;var i=o.componentDidAppear,p=o.componentWillDisappear,s=o.utils;s.addClassName,s.removeClassName,s.forceReflow,s.registerTransitionListener;var a=function(n){function r(){return n.apply(this,arguments)||this}e(r,n);var o=r.prototype;return o.componentDidAppear=function(e){this._innerEl=this.props.innerRef.current,i(this._innerEl,{prefix:"inner"})},o.componentWillDisappear=function(e,n){p(this._innerEl,{prefix:"inner"},n)},o.render=function(){var e=this;return t("div",{className:"page"},t("div",{className:"random-wrapper"},[t("img",{width:"120px",height:"120px",src:"avatar.png"}),t("div",{ref:this.props.innerRef,className:"inner"},[t("h2",null,"Step "+this.props.step),t("button",{onClick:function(n){n.preventDefault(),e.props.onNext()}},"Next")])]))},r}(n),c=function(n){function o(){var e;(e=n.call(this)||this).doGoNext=function(){e.setState({showStepIndex:(e.state.showStepIndex+1)%3})},e._innerAnimRefs=[];for(var t=0;t<3;t++)e._innerAnimRefs.push(r());return e.state={showStepIndex:0},e}return e(o,n),o.prototype.render=function(){var e=this.state.showStepIndex;return t(a,{key:"page_"+e,step:e+1,innerRef:this._innerAnimRefs[e],onNext:this.doGoNext})},o}(n);document.addEventListener("DOMContentLoaded",(function(){var e=document.querySelector("#App1");Inferno.render(t(c),e)}))}()}(); diff --git a/docs/animations-demo-inner/index.html b/docs/animations-demo-inner/index.html new file mode 100644 index 000000000..adb212e17 --- /dev/null +++ b/docs/animations-demo-inner/index.html @@ -0,0 +1,16 @@ + + + + + + inferno-animation + + + + + + +

    inferno-animation demo animation of child

    +
    + + diff --git a/docs/index.html b/docs/index.html index 8d4a236ee..7e932963c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -50,9 +50,14 @@

    Examples

    This demo uses inferno-animation to animate multiple elements of a card when it is added and removed from the DOM.
    Example | View source... | View CSS...

    +

    + Animating child when parent is mounted/unmounted
    + Here the animation is only performed on a child component of page when the page is mounted and unnmounted.
    + Example | View source... | View CSS... +

    Animations on mount and unmount
    - This is an implementation of inferno-animation where components are animated when added and removed from the DOM.
    + This is an implementation of inferno-animation where components are animated when added, removed or moved in the DOM.
    Example | View source... | View CSS...

    diff --git a/packages/inferno-animation/readme.md b/packages/inferno-animation/readme.md index 2e31aedc0..d25360103 100644 --- a/packages/inferno-animation/readme.md +++ b/packages/inferno-animation/readme.md @@ -19,6 +19,8 @@ There are three base components you can extend from to get animations in a strai - AnimatedMoveComponent -- animates on move (within the same parent) - AnimatedAllComponent -- animates on add/remove and move (within the same parent) +You can also animate functional components. There are a couple of examples of animations in the main repos in the `docs/animations` and `docs/animations-demo` folder. + If you don't want to extend from one of the pre-wired components, look att src/AnimatedAllComponent.ts to see how to wire up the three animation hooks: @@ -26,8 +28,6 @@ how to wire up the three animation hooks: - componentWillDisappear - componentWillMove -There are a couple of examples of animations in the main repos in the `docs/animations` and `docs/animations-demo` folder. - Using AnimatedAllComponent is just like working with ordinary components. Don't forget to add the CSS or you can get strange results: @@ -93,4 +93,53 @@ import { componentDidAppear, componentWillDisappear, componentWillMove } from 'i IMPORTANT! Always use the provided helper methods instead of implementing the hooks yourself. There might be optimisations and/or changes to how the animation hooks are implemented in future versions -of Inferno that you want to benefit from. \ No newline at end of file +of Inferno that you want to benefit from. + +### Bootstrap style modal animation +This is an example of how you could implement a Bootstrap style Modal animation using inferno-animation. These two animations are used both for the backdrop and the modal and the purpose is to support the CSS-rules without modification. + +- always use the inferno-animation utility functions +- implementation is straight forward +- `callback` in animateModalOnWillDisappear triggers the dom-removal in Inferno and is crucial! + +Custom animations won't be coordinated with the standard animations to reduce reflow, but performance is not an issue with just a few animations running simultaneously. Use the standard animations for grid or list items. + +Call these helper methods from `componentDidAppear` and `componentWillDisapper` of your backdrop and content component when you build a Bootstrap style modal. + +```js +import { utils } from 'inferno-animation' +const { + addClassName, + removeClassName, + registerTransitionListener, + forceReflow, + setDisplay +} = utils + +export function animateModalOnWillDisappear (dom, callback, onClosed) { + registerTransitionListener([dom], () => { + // Always call the dom removal callback first! + callback && callback() + onClosed && onClosed() + }) + + setTimeout(() => { + removeClassName(dom, 'show') + }, 5) +} + +export function animateModalOnDidAppear (dom, onOpened) { + setDisplay(dom, 'none') + addClassName(dom, 'fade') + forceReflow(dom) + setDisplay(dom, undefined) + + registerTransitionListener([dom, dom.children[0]], function () { + // *** Cleanup *** + setDisplay(dom, undefined) + onOpened && onOpened(dom) + }) + + addClassName(dom, 'show') +} +``` \ No newline at end of file diff --git a/packages/inferno-animation/src/animations.ts b/packages/inferno-animation/src/animations.ts index fdd3fdc59..b998264c0 100644 --- a/packages/inferno-animation/src/animations.ts +++ b/packages/inferno-animation/src/animations.ts @@ -11,7 +11,8 @@ import { setDisplay, resetDisplay, setTransform, - clearTransform + clearTransform, + forceReflow } from './utils'; import { queueAnimation, AnimationPhase } from './animationCoordinator'; import { isNullOrUndef, isNull } from 'inferno-shared'; @@ -72,7 +73,13 @@ function _didAppear(phase: AnimationPhase, dom: HTMLElement, cls: AnimationClass resetDisplay(dom, display); return; case AnimationPhase.MEASURE: - getDimensions(dom); + // In case of img element that hasn't been loaded, just trigger reflow + if (dom.tagName !== 'IMG' || (dom as any).complete) { + dimensions = getDimensions(dom); + } else { + // + forceReflow(); + } return; case AnimationPhase.SET_START_STATE: // 1. Set start of animation @@ -109,7 +116,7 @@ export function componentWillDisappear(dom: HTMLElement, props, callback: Functi function _willDisappear(phase: AnimationPhase, dom: HTMLElement, callback: Function, cls: AnimationClass, dimensions) { switch (phase) { case AnimationPhase.MEASURE: - // 1. Get dimensions and set animation start state + // 1. Set animation start state and dimensions setDimensions(dom, dimensions.width, dimensions.height); addClassName(dom, cls.start); return; diff --git a/packages/inferno/src/DOM/unmounting.ts b/packages/inferno/src/DOM/unmounting.ts index c9c471341..adca9cc13 100644 --- a/packages/inferno/src/DOM/unmounting.ts +++ b/packages/inferno/src/DOM/unmounting.ts @@ -46,7 +46,7 @@ export function unmount(vNode, animations: AnimationQueues) { children.componentWillUnmount(); } - // If we have a componentWillDisappear on this component, block children + // If we have a componentWillDisappear on this component, block children from animating let childAnimations = animations; if (isFunction(children.componentWillDisappear)) { childAnimations = new AnimationQueues(); @@ -57,7 +57,7 @@ export function unmount(vNode, animations: AnimationQueues) { children.$UN = true; unmount(children.$LI, childAnimations); } else if (flags & VNodeFlags.ComponentFunction) { - // If we have a onComponentWillDisappear on this component, block children + // If we have a onComponentWillDisappear on this component, block children from animating let childAnimations = animations; ref = vNode.ref; if (!isNullOrUndef(ref)) {