From 24fcb89996d0bfe47441df2390b89592090a80d8 Mon Sep 17 00:00:00 2001 From: eddie <66155195+just-toby@users.noreply.github.com> Date: Tue, 28 Feb 2023 13:56:05 -0800 Subject: [PATCH] fix: modal animations, new fade animation (#505) * feat: new fade animation for dialogs * feat: remove y translate from animation * fix: test update * fix: circular dependency --- src/components/BottomSheetModal.tsx | 4 +- src/components/Dialog.tsx | 70 ++++++++----------- .../__snapshots__/Header.test.tsx.snap | 24 +++---- src/theme/animations.ts | 41 ++++++++++- src/utils/animations.ts | 25 ++++--- 5 files changed, 99 insertions(+), 65 deletions(-) diff --git a/src/components/BottomSheetModal.tsx b/src/components/BottomSheetModal.tsx index d48ba432f..31c36c2de 100644 --- a/src/components/BottomSheetModal.tsx +++ b/src/components/BottomSheetModal.tsx @@ -3,9 +3,9 @@ import { StyledXButton } from 'icons' import { forwardRef, PropsWithChildren, useState } from 'react' import { createPortal } from 'react-dom' import styled, { keyframes } from 'styled-components/macro' -import { AnimationSpeed } from 'theme' +import { AnimationSpeed, SlideAnimationType } from 'theme' -import Dialog, { Header, Modal, Provider as DialogProvider, SlideAnimationType } from './Dialog' +import Dialog, { Header, Modal, Provider as DialogProvider } from './Dialog' const slideInBottom = keyframes` from { diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index d2f5cd2a4..162ebfebf 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -8,7 +8,16 @@ import ms from 'ms.macro' import { createContext, ReactElement, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import styled, { css, keyframes } from 'styled-components/macro' -import { AnimationSpeed, Color, Layer, Provider as ThemeProvider, ThemedText, TransitionDuration } from 'theme' +import { + AnimationSpeed, + Color, + fadeAnimationCss, + Layer, + Provider as ThemeProvider, + SlideAnimationType, + ThemedText, + TransitionDuration, +} from 'theme' import { useUnmountingAnimation } from 'utils/animations' import { PopoverBoundaryProvider } from './Popover' @@ -37,18 +46,6 @@ export enum DialogAnimationType { NONE = 'none', } -export enum SlideAnimationType { - /** Used when the Dialog is closing. */ - CLOSING = 'closing', - /** - * Used when the Dialog is paging to another Dialog screen. - * Paging occurs when multiple screens are sequenced in the Dialog, so that an action that closes - * one will simultaneously open the next. Special-casing paging animations can make the user feel - * like they are not leaving the Dialog, despite the initial screen closing. - */ - PAGING = 'paging', -} - const Context = createContext({ element: null as HTMLElement | null, options: {} as DialogOptions | undefined, @@ -177,18 +174,6 @@ const slideOutRight = keyframes` } ` -const fadeIn = keyframes` - from { - transform: translateY(40px) scale(0.9); - } -` - -const fadeOut = keyframes` - to { - transform: translateY(40px) scale(0.9); - } -` - const HiddenWrapper = styled.div<{ hideOverflow?: boolean; constrain?: boolean }>` border-radius: ${({ theme }) => theme.borderRadius.large}em; height: ${({ constrain }) => (constrain ? 'fit-content' : '100%')}; @@ -215,13 +200,6 @@ const slideAnimationCss = css` } ` -const fadeAnimationCss = css` - animation: ${fadeIn} ${AnimationSpeed.Fast} ease-in-out; - &.${SlideAnimationType.CLOSING} { - animation: ${fadeOut} ${AnimationSpeed.Fast} ease-in-out; - } -` - const EMPTY_CSS = css`` const getAnimation = (animationType?: DialogAnimationType) => { @@ -236,17 +214,12 @@ const getAnimation = (animationType?: DialogAnimationType) => { } } -const AnimationWrapper = styled.div<{ animationType?: DialogAnimationType }>` - ${Modal} { - ${({ animationType }) => getAnimation(animationType)} - } -` - -const FullScreenWrapper = styled.div<{ enabled?: boolean }>` - ${({ enabled }) => +const FullScreenWrapper = styled.div<{ enabled?: boolean; fadeAnimation?: boolean }>` + ${({ enabled, fadeAnimation }) => enabled && css` align-items: center; + ${fadeAnimation ? fadeAnimationCss : ''} background-color: ${({ theme }) => theme.scrim}; display: flex; height: 100%; @@ -255,6 +228,7 @@ const FullScreenWrapper = styled.div<{ enabled?: boolean }>` position: fixed; top: 0; width: 100%; + z-index: ${Layer.DIALOG}; ${HiddenWrapper} { @@ -264,6 +238,12 @@ const FullScreenWrapper = styled.div<{ enabled?: boolean }>` `} ` +const AnimationWrapper = styled.div<{ animationType?: DialogAnimationType }>` + ${Modal} { + ${({ animationType }) => getAnimation(animationType)} + } +` + // Accounts for any animation lag const PopoverAnimationUpdateDelay = ms`100` @@ -301,6 +281,7 @@ export default function Dialog({ color, children, onClose, forceContain }: Dialo const skipUnmountAnimation = context.options?.animationType === DialogAnimationType.NONE const modal = useRef(null) + const fullScreenWrapperRef = useRef(null) useUnmountingAnimation( popoverRef, () => { @@ -320,7 +301,7 @@ export default function Dialog({ color, children, onClose, forceContain }: Dialo return (mountPoint?.childElementCount ?? 0) > 1 ? SlideAnimationType.PAGING : SlideAnimationType.CLOSING } }, - modal, + [fullScreenWrapperRef, modal], skipUnmountAnimation ) @@ -332,7 +313,12 @@ export default function Dialog({ color, children, onClose, forceContain }: Dialo
- + diff --git a/src/components/__snapshots__/Header.test.tsx.snap b/src/components/__snapshots__/Header.test.tsx.snap index 46e22b23c..37393d442 100644 --- a/src/components/__snapshots__/Header.test.tsx.snap +++ b/src/components/__snapshots__/Header.test.tsx.snap @@ -32,7 +32,7 @@ Object { class="Popover__Reference-sc-1liex6z-1 dFwFDV" >
, getAnimatingClass: () => string, - animatedElement?: RefObject, + animatedElements?: RefObject[], skip = false ) { useEffect(() => { const current = node.current - const animated = animatedElement?.current ?? current + const animated = animatedElements?.map((element) => element.current) ?? [current] const parent = current?.parentElement const removeChild = parent?.removeChild if (!(parent && removeChild) || skip) return parent.removeChild = function (child: T) { if ((child as Node) === current && animated) { - animated.classList.add(getAnimatingClass()) - if (isAnimating(animated)) { - animated.addEventListener('animationend', () => { - removeChild.call(parent, child) + animated.forEach((element) => element?.classList.add(getAnimatingClass())) + const animating = animated.find((element) => isAnimating(element ?? undefined)) + if (animating) { + animating?.addEventListener('animationend', (x) => { + // This check is needed because the animationend event will fire for all animations on the + // element or its children. + if (x.target === animating) { + removeChild.call(parent, child) + } }) } else { removeChild.call(parent, child) @@ -47,5 +56,5 @@ export function useUnmountingAnimation( return () => { parent.removeChild = removeChild } - }, [animatedElement, getAnimatingClass, node, skip]) + }, [animatedElements, getAnimatingClass, node, skip]) }