mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
feat(component): add animations to modal (#7474)
Add opening and closing animations to modal.
The usage of conditional rendering as shown below is not recommended:
```
open ? (
<Modal
open={open}
...
/>
) : null,
```
When the modal is closed, it gets removed from the DOM instantly without running any exit animations that might be defined in the Modal component.
This commit is contained in:
@@ -10,7 +10,8 @@ import * as VisuallyHidden from '@radix-ui/react-visually-hidden';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { forwardRef, useCallback, useEffect } from 'react';
|
||||
import { type TransitionState, useTransition } from 'react-transition-state';
|
||||
|
||||
import type { IconButtonProps } from '../button';
|
||||
import { IconButton } from '../button';
|
||||
@@ -28,7 +29,8 @@ export interface ModalProps extends DialogProps {
|
||||
* @default false
|
||||
*/
|
||||
persistent?: boolean;
|
||||
|
||||
// animation for modal open/close
|
||||
animationTimeout?: number;
|
||||
portalOptions?: DialogPortalProps;
|
||||
contentOptions?: DialogContentProps;
|
||||
overlayOptions?: DialogOverlayProps;
|
||||
@@ -57,8 +59,10 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||
withoutCloseButton = false,
|
||||
modal,
|
||||
persistent,
|
||||
|
||||
animationTimeout = 120,
|
||||
portalOptions,
|
||||
open: customOpen,
|
||||
onOpenChange: customOnOpenChange,
|
||||
contentOptions: {
|
||||
style: contentStyle,
|
||||
className: contentClassName,
|
||||
@@ -68,6 +72,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||
} = {},
|
||||
overlayOptions: {
|
||||
className: overlayClassName,
|
||||
style: overlayStyle,
|
||||
...otherOverlayOptions
|
||||
} = {},
|
||||
closeButtonOptions = {},
|
||||
@@ -76,11 +81,38 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [{ status }, toggle] = useTransition({
|
||||
timeout: animationTimeout,
|
||||
onStateChange: useCallback(
|
||||
({ current }: { current: TransitionState }) => {
|
||||
// add more status if needed
|
||||
if (current.status === 'exited') customOnOpenChange?.(false);
|
||||
if (current.status === 'entered') customOnOpenChange?.(true);
|
||||
},
|
||||
[customOnOpenChange]
|
||||
),
|
||||
});
|
||||
useEffect(() => {
|
||||
toggle(customOpen);
|
||||
}, [customOpen]);
|
||||
|
||||
return (
|
||||
<Dialog.Root modal={modal} {...props}>
|
||||
<Dialog.Root
|
||||
modal={modal}
|
||||
open={status !== 'exited'}
|
||||
onOpenChange={toggle}
|
||||
{...props}
|
||||
>
|
||||
<Dialog.Portal {...portalOptions}>
|
||||
<Dialog.Overlay
|
||||
className={clsx(styles.modalOverlay, overlayClassName)}
|
||||
data-state={status}
|
||||
style={{
|
||||
...assignInlineVars({
|
||||
[styles.animationTimeout]: `${animationTimeout}ms`,
|
||||
}),
|
||||
...overlayStyle,
|
||||
}}
|
||||
{...otherOverlayOptions}
|
||||
/>
|
||||
<div data-modal={modal} className={clsx(styles.modalContentWrapper)}>
|
||||
@@ -100,14 +132,17 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||
[onEscapeKeyDown, persistent]
|
||||
)}
|
||||
className={clsx(styles.modalContent, contentClassName)}
|
||||
data-state={status}
|
||||
style={{
|
||||
...assignInlineVars({
|
||||
[styles.widthVar]: getVar(width, '50vw'),
|
||||
[styles.heightVar]: getVar(height, 'unset'),
|
||||
[styles.minHeightVar]: getVar(minHeight, '26px'),
|
||||
[styles.animationTimeout]: `${animationTimeout}ms`,
|
||||
}),
|
||||
...contentStyle,
|
||||
}}
|
||||
{...(description ? {} : { 'aria-describedby': undefined })}
|
||||
{...otherContentOptions}
|
||||
ref={ref}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,61 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, globalStyle, style } from '@vanilla-extract/css';
|
||||
import { createVar, globalStyle, keyframes, style } from '@vanilla-extract/css';
|
||||
export const widthVar = createVar('widthVar');
|
||||
export const heightVar = createVar('heightVar');
|
||||
export const minHeightVar = createVar('minHeightVar');
|
||||
export const animationTimeout = createVar();
|
||||
|
||||
const overlayShow = keyframes({
|
||||
from: {
|
||||
opacity: 0,
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
const overlayHide = keyframes({
|
||||
to: {
|
||||
opacity: 0,
|
||||
},
|
||||
from: {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const contentShow = keyframes({
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(-2%) scale(0.96)',
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
});
|
||||
const contentHide = keyframes({
|
||||
to: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(-2%) scale(0.96)',
|
||||
},
|
||||
from: {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0) scale(1)',
|
||||
},
|
||||
});
|
||||
|
||||
export const modalOverlay = style({
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: cssVar('backgroundModalColor'),
|
||||
zIndex: cssVar('zIndexModal'),
|
||||
selectors: {
|
||||
'&[data-state=entered], &[data-state=entering]': {
|
||||
animation: `${overlayShow} ${animationTimeout} forwards`,
|
||||
},
|
||||
'&[data-state=exited], &[data-state=exiting]': {
|
||||
animation: `${overlayHide} ${animationTimeout} forwards`,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const modalContentWrapper = style({
|
||||
position: 'fixed',
|
||||
@@ -39,6 +87,16 @@ export const modalContent = style({
|
||||
maxHeight: 'calc(100vh - 32px)',
|
||||
// :focus-visible will set outline
|
||||
outline: 'none',
|
||||
selectors: {
|
||||
'&[data-state=entered], &[data-state=entering]': {
|
||||
animation: `${contentShow} ${animationTimeout} cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
'&[data-state=exited], &[data-state=exiting]': {
|
||||
animation: `${contentHide} ${animationTimeout} cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||
animationFillMode: 'forwards',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const closeButton = style({
|
||||
position: 'absolute',
|
||||
|
||||
Reference in New Issue
Block a user