Skip to content

Commit

Permalink
fix: modal animations, new fade animation (#505)
Browse files Browse the repository at this point in the history
* feat: new fade animation for dialogs

* feat: remove y translate from animation

* fix: test update

* fix: circular dependency
  • Loading branch information
just-toby authored Feb 28, 2023
1 parent 46a3572 commit 24fcb89
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 65 deletions.
4 changes: 2 additions & 2 deletions src/components/BottomSheetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
70 changes: 28 additions & 42 deletions src/components/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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%')};
Expand All @@ -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) => {
Expand All @@ -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%;
Expand All @@ -255,6 +228,7 @@ const FullScreenWrapper = styled.div<{ enabled?: boolean }>`
position: fixed;
top: 0;
width: 100%;
z-index: ${Layer.DIALOG};
${HiddenWrapper} {
Expand All @@ -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`

Expand Down Expand Up @@ -301,6 +281,7 @@ export default function Dialog({ color, children, onClose, forceContain }: Dialo

const skipUnmountAnimation = context.options?.animationType === DialogAnimationType.NONE
const modal = useRef<HTMLDivElement>(null)
const fullScreenWrapperRef = useRef<HTMLDivElement>(null)
useUnmountingAnimation(
popoverRef,
() => {
Expand All @@ -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
)

Expand All @@ -332,7 +313,12 @@ export default function Dialog({ color, children, onClose, forceContain }: Dialo
<ThemeProvider>
<PopoverBoundaryProvider value={popoverRef.current} updateTrigger={updatePopover}>
<div ref={popoverRef}>
<FullScreenWrapper enabled={pageCentered} onClick={closeOnBackgroundClick}>
<FullScreenWrapper
enabled={pageCentered}
fadeAnimation={context.options?.animationType === DialogAnimationType.FADE}
onClick={closeOnBackgroundClick}
ref={fullScreenWrapperRef}
>
<HiddenWrapper constrain={pageCentered} hideOverflow={!pageCentered}>
<AnimationWrapper animationType={context.options?.animationType}>
<OnCloseContext.Provider value={onClose}>
Expand Down
24 changes: 12 additions & 12 deletions src/components/__snapshots__/Header.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Object {
class="Popover__Reference-sc-1liex6z-1 dFwFDV"
>
<button
class="Button__BaseButton-sc-1soikk5-0 Button__transparentButton-sc-1soikk5-2 Button__StyledIconButton-sc-1soikk5-3 gleKKe jBWboA jwaziW Settings__SettingsButton-sc-1hvqnx5-1 evHSmT"
class="Button__BaseButton-sc-1soikk5-0 Button__transparentButton-sc-1soikk5-2 Button__StyledIconButton-sc-1soikk5-3 gleKKe jBWboA jwaziW Settings__SettingsButton-sc-1hvqnx5-1 ewOqAq"
data-testid="settings-button"
>
<svg
Expand All @@ -59,7 +59,7 @@ Object {
</button>
</div>
<div
class="Popover__PopoverContainer-sc-1liex6z-0 bSKTJc"
class="Popover__PopoverContainer-sc-1liex6z-0 koqvVY"
style="position: absolute; left: 0px; top: 0px;"
>
<div
Expand Down Expand Up @@ -139,7 +139,7 @@ Object {
</div>
</div>
<div
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm jYVfac"
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm eJCkeI"
>
<div
class="Column-sc-1ul9eki-0 Expando__InnerColumn-sc-yzkwmi-5 Expando___StyledInnerColumn2-sc-yzkwmi-6 iSonrV NVvmR gyFZzL"
Expand Down Expand Up @@ -287,7 +287,7 @@ Object {
</div>
</div>
<div
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm jYVfac"
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm eJCkeI"
>
<div
class="Column-sc-1ul9eki-0 Expando__InnerColumn-sc-yzkwmi-5 Expando___StyledInnerColumn2-sc-yzkwmi-6 iSonrV NNzpo gyFZzL"
Expand Down Expand Up @@ -323,7 +323,7 @@ Object {
</div>
</div>
<div
class="Popover__PopoverContainer-sc-1liex6z-0 bSKTJc"
class="Popover__PopoverContainer-sc-1liex6z-0 koqvVY"
style="position: absolute; left: 0px; top: 0px;"
>
<div
Expand All @@ -337,7 +337,7 @@ Object {
/>
</div>
<div
class="Popover__PopoverContainer-sc-1liex6z-0 bSKTJc"
class="Popover__PopoverContainer-sc-1liex6z-0 koqvVY"
style="position: absolute; left: 0px; top: 0px;"
>
<div
Expand Down Expand Up @@ -386,7 +386,7 @@ Object {
class="Popover__Reference-sc-1liex6z-1 dFwFDV"
>
<button
class="Button__BaseButton-sc-1soikk5-0 Button__transparentButton-sc-1soikk5-2 Button__StyledIconButton-sc-1soikk5-3 gleKKe jBWboA jwaziW Settings__SettingsButton-sc-1hvqnx5-1 evHSmT"
class="Button__BaseButton-sc-1soikk5-0 Button__transparentButton-sc-1soikk5-2 Button__StyledIconButton-sc-1soikk5-3 gleKKe jBWboA jwaziW Settings__SettingsButton-sc-1hvqnx5-1 ewOqAq"
data-testid="settings-button"
>
<svg
Expand All @@ -413,7 +413,7 @@ Object {
</button>
</div>
<div
class="Popover__PopoverContainer-sc-1liex6z-0 bSKTJc"
class="Popover__PopoverContainer-sc-1liex6z-0 koqvVY"
style="position: absolute; left: 0px; top: 0px;"
>
<div
Expand Down Expand Up @@ -493,7 +493,7 @@ Object {
</div>
</div>
<div
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm jYVfac"
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm eJCkeI"
>
<div
class="Column-sc-1ul9eki-0 Expando__InnerColumn-sc-yzkwmi-5 Expando___StyledInnerColumn2-sc-yzkwmi-6 iSonrV NVvmR gyFZzL"
Expand Down Expand Up @@ -641,7 +641,7 @@ Object {
</div>
</div>
<div
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm jYVfac"
class="Column-sc-1ul9eki-0 Expando__ExpandoColumn-sc-yzkwmi-4 jgGrBm eJCkeI"
>
<div
class="Column-sc-1ul9eki-0 Expando__InnerColumn-sc-yzkwmi-5 Expando___StyledInnerColumn2-sc-yzkwmi-6 iSonrV NNzpo gyFZzL"
Expand Down Expand Up @@ -677,7 +677,7 @@ Object {
</div>
</div>
<div
class="Popover__PopoverContainer-sc-1liex6z-0 bSKTJc"
class="Popover__PopoverContainer-sc-1liex6z-0 koqvVY"
style="position: absolute; left: 0px; top: 0px;"
>
<div
Expand All @@ -691,7 +691,7 @@ Object {
/>
</div>
<div
class="Popover__PopoverContainer-sc-1liex6z-0 bSKTJc"
class="Popover__PopoverContainer-sc-1liex6z-0 koqvVY"
style="position: absolute; left: 0px; top: 0px;"
>
<div
Expand Down
41 changes: 40 additions & 1 deletion src/theme/animations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { css, keyframes } from 'styled-components/macro'

export enum TransitionDuration {
Fast = 125,
Medium = 250,
Medium = 200,
Slow = 250,
}

Expand All @@ -9,3 +11,40 @@ export const AnimationSpeed = {
Medium: `${TransitionDuration.Medium}ms`,
Slow: `${TransitionDuration.Slow}ms`,
}

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',
}

export const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`

export const fadeOut = keyframes`
to {
opacity: 0;
}
from {
opacity: 1;
}
`

export const fadeAnimationCss = css`
animation: ${fadeIn} ${AnimationSpeed.Medium} ease-in-out;
&.${SlideAnimationType.CLOSING} {
animation: ${fadeOut} ${AnimationSpeed.Medium} ease-in-out;
}
`
25 changes: 17 additions & 8 deletions src/utils/animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,36 @@ export function isAnimating(node?: Animatable | Document) {
* Note that getAnimatingClass will be called when the node would normally begin unmounting.
*
* If the animation should be applied to an element that is not the root node of the removed subtree,
* pass that element as the animatedElement parameter.
* pass that element in the animatedElements array.
*
* Using the animatedElements array, you can apply exit animations to multiple elements at once.
* Currently this only supports using the same className for all the elements, and uses
* the animation of the first element in the array to determine when to unmount the node.
*/
export function useUnmountingAnimation(
node: RefObject<HTMLElement>,
getAnimatingClass: () => string,
animatedElement?: RefObject<HTMLElement>,
animatedElements?: RefObject<HTMLElement>[],
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 <T extends Node>(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)
Expand All @@ -47,5 +56,5 @@ export function useUnmountingAnimation(
return () => {
parent.removeChild = removeChild
}
}, [animatedElement, getAnimatingClass, node, skip])
}, [animatedElements, getAnimatingClass, node, skip])
}

1 comment on commit 24fcb89

@vercel
Copy link

@vercel vercel bot commented on 24fcb89 Feb 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

widgets – ./

widgets-uniswap.vercel.app
widgets-git-main-uniswap.vercel.app
widgets-seven-tau.vercel.app

Please sign in to comment.