Skip to content

Commit

Permalink
✨ [bento][amp-sidebar] Update animations to obey APIs mid-animation (#…
Browse files Browse the repository at this point in the history
…32584)

* mostly working

* Update animations to obey apis mid animation

* Clean up typos and style

* Update animation time

* Refactor to custom hook

* Review comments

* Use useValueRef for side and add useValueRef to utils

* Update Direction constant to AnimationState

* Simplify animation states

* Move value ref location

* Removed test code

* Review comments

* Add a comment explaning side

* Tests for animations
  • Loading branch information
krdwan committed Feb 12, 2021
1 parent f169402 commit 6130ded
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 140 deletions.
179 changes: 179 additions & 0 deletions extensions/amp-sidebar/1.0/sidebar-animations-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {Side} from './sidebar-config';
import {assertDoesNotContainDisplay, setStyles} from '../../../src/style';
import {useLayoutEffect, useRef} from '../../../src/preact';
import {useValueRef} from '../../../src/preact/component';

const ANIMATION_DURATION = 350;
const ANIMATION_EASE_IN = 'cubic-bezier(0,0,.21,1)';

const ANIMATION_KEYFRAMES_FADE_IN = [{'opacity': '0'}, {'opacity': '1'}];
const ANIMATION_KEYFRAMES_SLIDE_IN_LEFT = [
{'transform': 'translateX(-100%)'},
{'transform': 'translateX(0)'},
];
const ANIMATION_KEYFRAMES_SLIDE_IN_RIGHT = [
{'transform': 'translateX(100%)'},
{'transform': 'translateX(0)'},
];

const ANIMATION_STYLES_SIDEBAR_LEFT_INIT = {'transform': 'translateX(-100%)'};
const ANIMATION_STYLES_SIDEBAR_RIGHT_INIT = {'transform': 'translateX(100%)'};
const ANIMATION_STYLES_BACKDROP_INIT = {'opacity': '0'};
const ANIMATION_STYLES_SIDEBAR_FINAL = {'transform': ''};
const ANIMATION_STYLES_BACKDROP_FINAL = {'opacity': ''};

/**
* @param {!Element} element
* @param {!Object<string, *>} styles
*/
function safelySetStyles(element, styles) {
setStyles(element, assertDoesNotContainDisplay(styles));
}

/**
* @param {boolean} opened
* @param {{current: function|undefined}} onAfterClose
* @param {string} side
* @param {{current: Element|null}} sidebarRef
* @param {{current: Element|null}} backdropRef
* @param {function} setMounted
*/
export function useSidebarAnimation(
opened,
onAfterClose,
side,
sidebarRef,
backdropRef,
setMounted
) {
const onAfterCloseRef = useValueRef(onAfterClose);
const sidebarAnimationRef = useRef(null);
const backdropAnimationRef = useRef(null);
const currentlyAnimatingRef = useRef(false);
useLayoutEffect(() => {
const sidebarElement = sidebarRef.current;
const backdropElement = backdropRef.current;
// The component might start in a state where `side` is not known
// This effect must be restarted when the `side` becomes known
if (!sidebarElement || !backdropElement || !side) {
return;
}

const postVisibleAnim = () => {
safelySetStyles(sidebarElement, ANIMATION_STYLES_SIDEBAR_FINAL);
safelySetStyles(backdropElement, ANIMATION_STYLES_BACKDROP_FINAL);
sidebarAnimationRef.current = null;
backdropAnimationRef.current = null;
currentlyAnimatingRef.current = false;
};
const postInvisibleAnim = () => {
if (onAfterCloseRef.current) {
onAfterCloseRef.current();
}
sidebarAnimationRef.current = null;
backdropAnimationRef.current = null;
currentlyAnimatingRef.current = false;
setMounted(false);
};

// reverse animation if currently animating
if (currentlyAnimatingRef.current) {
const sidebarAnimation = sidebarAnimationRef.current;
if (sidebarAnimation) {
sidebarAnimation.reverse();
sidebarAnimation.onfinish = opened
? postVisibleAnim
: postInvisibleAnim;
}
const backdropAnimation = backdropAnimationRef.current;
if (backdropAnimation) {
backdropAnimation.reverse();
}
return;
}

// begin animation if fully opened or closed
if (opened) {
// make visible animation
if (!sidebarElement.animate || !backdropElement.animate) {
postVisibleAnim();
return;
}
safelySetStyles(
sidebarElement,
side === Side.LEFT
? ANIMATION_STYLES_SIDEBAR_LEFT_INIT
: ANIMATION_STYLES_SIDEBAR_RIGHT_INIT
);
safelySetStyles(backdropElement, ANIMATION_STYLES_BACKDROP_INIT);
const sidebarAnimation = sidebarElement.animate(
side === Side.LEFT
? ANIMATION_KEYFRAMES_SLIDE_IN_LEFT
: ANIMATION_KEYFRAMES_SLIDE_IN_RIGHT,
{
duration: ANIMATION_DURATION,
fill: 'both',
easing: ANIMATION_EASE_IN,
}
);
sidebarAnimation.onfinish = postVisibleAnim;
const backdropAnimation = backdropElement.animate(
ANIMATION_KEYFRAMES_FADE_IN,
{
duration: ANIMATION_DURATION,
fill: 'both',
easing: ANIMATION_EASE_IN,
}
);
sidebarAnimationRef.current = sidebarAnimation;
backdropAnimationRef.current = backdropAnimation;
currentlyAnimatingRef.current = true;
} else {
// make invisible animation
if (!sidebarElement.animate || !backdropElement.animate) {
postInvisibleAnim();
return;
}
const sidebarAnimation = sidebarElement.animate(
side === Side.LEFT
? ANIMATION_KEYFRAMES_SLIDE_IN_LEFT
: ANIMATION_KEYFRAMES_SLIDE_IN_RIGHT,
{
duration: ANIMATION_DURATION,
direction: 'reverse',
fill: 'both',
easing: ANIMATION_EASE_IN,
}
);
sidebarAnimation.onfinish = postInvisibleAnim;
const backdropAnimation = backdropElement.animate(
ANIMATION_KEYFRAMES_FADE_IN,
{
duration: ANIMATION_DURATION,
direction: 'reverse',
fill: 'both',
easing: ANIMATION_EASE_IN,
}
);
sidebarAnimationRef.current = sidebarAnimation;
backdropAnimationRef.current = backdropAnimation;
currentlyAnimatingRef.current = true;
}
}, [opened, onAfterCloseRef, side, sidebarRef, backdropRef, setMounted]);
}
21 changes: 21 additions & 0 deletions extensions/amp-sidebar/1.0/sidebar-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/** @protected @enum {string} */
export const Side = {
LEFT: 'left',
RIGHT: 'right',
};
146 changes: 11 additions & 135 deletions extensions/amp-sidebar/1.0/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
*/

import * as Preact from '../../../src/preact';
import {ContainWrapper} from '../../../src/preact/component';
import {assertDoesNotContainDisplay, setStyles} from '../../../src/style';
import {ContainWrapper, useValueRef} from '../../../src/preact/component';
import {Side} from './sidebar-config';
import {forwardRef} from '../../../src/preact/compat';
import {isRTL} from '../../../src/dom';
import {
Expand All @@ -26,51 +26,9 @@ import {
useRef,
useState,
} from '../../../src/preact';
import {useSidebarAnimation} from './sidebar-animations-hook';
import {useStyles} from './sidebar.jss';

/** @private @enum {string} */
const Side = {
LEFT: 'left',
RIGHT: 'right',
};

const ANIMATION_DURATION = 350;
const ANIMATION_EASE_IN = 'cubic-bezier(0,0,.21,1)';

const ANIMATION_KEYFRAMES_FADE_IN = [{'opacity': '0'}, {'opacity': '1'}];
const ANIMATION_KEYFRAMES_SLIDE_IN_LEFT = [
{'transform': 'translateX(-100%)'},
{'transform': 'translateX(0)'},
];
const ANIMATION_KEYFRAMES_SLIDE_IN_RIGHT = [
{'transform': 'translateX(100%)'},
{'transform': 'translateX(0)'},
];

const ANIMATION_STYLES_SIDEBAR_LEFT_INIT = {'transform': 'translateX(-100%)'};
const ANIMATION_STYLES_SIDEBAR_RIGHT_INIT = {'transform': 'translateX(100%)'};
const ANIMATION_STYLES_BACKDROP_INIT = {'opacity': '0'};
const ANIMATION_STYLES_SIDEBAR_FINAL = {'transform': ''};
const ANIMATION_STYLES_BACKDROP_FINAL = {'opacity': ''};

/**
* @param {T} current
* @return {{current: T}}
* @template T
*/
function useValueRef(current) {
const valueRef = useRef(null);
valueRef.current = current;
return valueRef;
}
/**
* @param {!Element} element
* @param {!Object<string, *>} styles
*/
function safelySetStyles(element, styles) {
setStyles(element, assertDoesNotContainDisplay(styles));
}

/**
* @param {!SidebarDef.Props} props
* @param {{current: (!SidebarDef.SidebarApi|null)}} ref
Expand Down Expand Up @@ -105,7 +63,6 @@ function SidebarWithRef(
// This is because they are needed within `useEffect` calls below (but are not depended for triggering)
// We use `useValueRef` for props that might change (user-controlled)
const onBeforeOpenRef = useValueRef(onBeforeOpen);
const onAfterCloseRef = useValueRef(onAfterClose);

const open = useCallback(() => {
if (onBeforeOpenRef.current) {
Expand Down Expand Up @@ -143,95 +100,14 @@ function SidebarWithRef(
setSide(isRTL(sidebarElement.ownerDocument) ? Side.RIGHT : Side.LEFT);
}, [side, mounted]);

useLayoutEffect(() => {
const sidebarElement = sidebarRef.current;
const backdropElement = backdropRef.current;
if (!sidebarElement || !backdropElement) {
return;
}

if (!side) {
return;
}

let sidebarAnimation;
let backdropAnimation;
// "Make Visible" Animation
if (opened) {
const postVisibleAnim = () => {
safelySetStyles(sidebarElement, ANIMATION_STYLES_SIDEBAR_FINAL);
safelySetStyles(backdropElement, ANIMATION_STYLES_BACKDROP_FINAL);
};
if (!sidebarElement.animate || !backdropElement.animate) {
postVisibleAnim();
return;
}

safelySetStyles(
sidebarElement,
side === Side.LEFT
? ANIMATION_STYLES_SIDEBAR_LEFT_INIT
: ANIMATION_STYLES_SIDEBAR_RIGHT_INIT
);
safelySetStyles(backdropElement, ANIMATION_STYLES_BACKDROP_INIT);
sidebarAnimation = sidebarElement.animate(
side === Side.LEFT
? ANIMATION_KEYFRAMES_SLIDE_IN_LEFT
: ANIMATION_KEYFRAMES_SLIDE_IN_RIGHT,
{
duration: ANIMATION_DURATION,
fill: 'both',
easing: ANIMATION_EASE_IN,
}
);
sidebarAnimation.onfinish = postVisibleAnim;
backdropAnimation = backdropElement.animate(ANIMATION_KEYFRAMES_FADE_IN, {
duration: ANIMATION_DURATION,
fill: 'both',
easing: ANIMATION_EASE_IN,
});
} else {
// "Make Invisible" Animation
const postInvisibleAnim = () => {
if (onAfterCloseRef.current) {
onAfterCloseRef.current();
}
sidebarAnimation = null;
backdropAnimation = null;
setMounted(false);
};
if (!sidebarElement.animate || !backdropElement.animate) {
postInvisibleAnim();
return;
}
sidebarAnimation = sidebarElement.animate(
side === Side.LEFT
? ANIMATION_KEYFRAMES_SLIDE_IN_LEFT
: ANIMATION_KEYFRAMES_SLIDE_IN_RIGHT,
{
duration: ANIMATION_DURATION,
direction: 'reverse',
fill: 'both',
easing: ANIMATION_EASE_IN,
}
);
sidebarAnimation.onfinish = postInvisibleAnim;
backdropAnimation = backdropElement.animate(ANIMATION_KEYFRAMES_FADE_IN, {
duration: ANIMATION_DURATION,
direction: 'reverse',
fill: 'both',
easing: ANIMATION_EASE_IN,
});
}
return () => {
if (sidebarAnimation) {
sidebarAnimation.cancel();
}
if (backdropAnimation) {
backdropAnimation.cancel();
}
};
}, [opened, onAfterCloseRef, side]);
useSidebarAnimation(
opened,
onAfterClose,
side,
sidebarRef,
backdropRef,
setMounted
);

return (
mounted && (
Expand Down

0 comments on commit 6130ded

Please sign in to comment.