chore: clean dialog mounting logic (#3559)

* fix: apply scrollbar css on first render

* fix: useUnmount portability

* chore: clean up dialog ordering

* fix: dialog border-radius

* chore: cleanup dialog unmount animation
This commit is contained in:
Zach Pomerantz 2022-03-21 12:55:46 -07:00 committed by GitHub
parent ce6c783174
commit ee96973212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 63 deletions

@ -1,8 +1,8 @@
import 'wicg-inert'
import useUnmount from 'lib/hooks/useUnmount'
import { X } from 'lib/icons'
import styled, { Color, Layer, ThemeProvider } from 'lib/theme'
import { delayUnmountForAnimation } from 'lib/utils/animations'
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
@ -97,8 +97,10 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.setActive(true)
return () => context.setActive(false)
}, [context])
const dialog = useRef<HTMLDivElement>(null)
useUnmount(dialog)
const modal = useRef<HTMLDivElement>(null)
useEffect(() => delayUnmountForAnimation(modal), [])
useEffect(() => {
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
document.addEventListener('keydown', close, true)
@ -108,9 +110,11 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.element &&
createPortal(
<ThemeProvider>
<Modal color={color} ref={dialog}>
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
</Modal>
<OnCloseContext.Provider value={onClose}>
<Modal color={color} ref={modal}>
{children}
</Modal>
</OnCloseContext.Provider>
</ThemeProvider>,
context.element
)

@ -6,10 +6,10 @@ import { TransactionsUpdater } from 'lib/hooks/transactions'
import { Web3Provider } from 'lib/hooks/useActiveWeb3React'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import useEip1193Provider from 'lib/hooks/useEip1193Provider'
import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { Provider as I18nProvider } from 'lib/i18n'
import { MulticallUpdater, store as multicallStore } from 'lib/state/multicall'
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
import { UNMOUNTING } from 'lib/utils/animations'
import { PropsWithChildren, StrictMode, useState } from 'react'
import { Provider as ReduxProvider } from 'react-redux'
@ -48,18 +48,19 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
}
`
const slideDown = keyframes`
to {
const slideIn = keyframes`
from {
transform: translateY(calc(100% - 0.25em));
}
`
const slideUp = keyframes`
from {
const slideOut = keyframes`
to {
transform: translateY(calc(100% - 0.25em));
}
`
const DialogWrapper = styled.div`
border-radius: ${({ theme }) => theme.borderRadius}em;
height: calc(100% - 0.5em);
left: 0;
margin: 0.25em;
@ -73,11 +74,11 @@ const DialogWrapper = styled.div`
}
${Modal} {
animation: ${slideUp} 0.25s ease-in-out;
}
animation: ${slideIn} 0.25s ease-in;
${Modal}.${UNMOUNTING} {
animation: ${slideDown} 0.25s ease-in-out;
&.${UNMOUNTING} {
animation: ${slideOut} 0.25s ease-out;
}
}
`

@ -1,7 +1,5 @@
import { css } from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useNativeEvent from './useNativeEvent'
import { useMemo } from 'react'
const overflowCss = css`
overflow-y: scroll;
@ -48,21 +46,15 @@ interface ScrollbarOptions {
}
export default function useScrollbar(element: HTMLElement | null, { padded = false }: ScrollbarOptions = {}) {
const [overflow, setOverflow] = useState(true)
useEffect(() => {
setOverflow(hasOverflow(element))
}, [element])
useNativeEvent(
element,
'transitionend',
useCallback(() => setOverflow(hasOverflow(element)), [element])
return useMemo(
// NB: The css must be applied on an element's first render. WebKit will not re-apply overflow
// properties until any transitions have ended, so waiting a frame for state would cause jank.
() => (hasOverflow(element) ? scrollbarCss(padded) : overflowCss),
[element, padded]
)
return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded])
function hasOverflow(element: HTMLElement | null) {
if (!element) {
return true
}
if (!element) return true
return element.scrollHeight > element.clientHeight
}
}

@ -1,33 +0,0 @@
import { RefObject, useEffect } from 'react'
export const UNMOUNTING = 'unmounting'
/**
* Delays a node's unmounting so that an animation may be applied.
* An animation *must* be applied, or the node will not unmount.
*/
export default function useUnmount(node: RefObject<HTMLElement>) {
useEffect(() => {
const current = node.current
const parent = current?.parentElement
const removeChild = parent?.removeChild
if (parent && removeChild) {
parent.removeChild = function <T extends Node>(child: T) {
if ((child as Node) === current) {
current.classList.add(UNMOUNTING)
current.onanimationend = () => {
removeChild.call(parent, child)
}
return child
} else {
return removeChild.call(parent, child) as T
}
}
}
return () => {
if (parent && removeChild) {
parent.removeChild = removeChild
}
}
}, [node])
}

@ -0,0 +1,36 @@
import { RefObject } from 'react'
export function isAnimating(node: HTMLElement) {
return (node.getAnimations().length ?? 0) > 0
}
export const UNMOUNTING = 'unmounting'
/**
* Delays a node's unmounting until any animations on that node are finished, so that an unmounting
* animation may be applied. If there is no animation, this is a no-op.
*
* CSS should target the UNMOUNTING class to determine when to apply an unmounting animation.
*/
export function delayUnmountForAnimation(node: RefObject<HTMLElement>) {
const current = node.current
const parent = current?.parentElement
const removeChild = parent?.removeChild
if (parent && removeChild) {
parent.removeChild = function <T extends Node>(child: T) {
if ((child as Node) === current) {
current.classList.add(UNMOUNTING)
if (isAnimating(current)) {
current.addEventListener('animationend', () => {
removeChild.call(parent, child)
})
} else {
removeChild.call(parent, child)
}
return child
} else {
return removeChild.call(parent, child) as T
}
}
}
}