diff --git a/packages/frontend/component/src/ui/modal/modal.tsx b/packages/frontend/component/src/ui/modal/modal.tsx index 5c2710b0339b6..68c439f9bc260 100644 --- a/packages/frontend/component/src/ui/modal/modal.tsx +++ b/packages/frontend/component/src/ui/modal/modal.tsx @@ -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( withoutCloseButton = false, modal, persistent, - + animationTimeout = 120, portalOptions, + open: customOpen, + onOpenChange: customOnOpenChange, contentOptions: { style: contentStyle, className: contentClassName, @@ -68,6 +72,7 @@ export const Modal = forwardRef( } = {}, overlayOptions: { className: overlayClassName, + style: overlayStyle, ...otherOverlayOptions } = {}, closeButtonOptions = {}, @@ -76,11 +81,38 @@ export const Modal = forwardRef( }, 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 ( - +
@@ -100,14 +132,17 @@ export const Modal = forwardRef( [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} > diff --git a/packages/frontend/component/src/ui/modal/styles.css.ts b/packages/frontend/component/src/ui/modal/styles.css.ts index 99808bcd074f6..20ec2df75f4a9 100644 --- a/packages/frontend/component/src/ui/modal/styles.css.ts +++ b/packages/frontend/component/src/ui/modal/styles.css.ts @@ -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', diff --git a/packages/frontend/core/src/components/affine/auth/sign-in.tsx b/packages/frontend/core/src/components/affine/auth/sign-in.tsx index ee448d764031c..0bb7a6eb60b0a 100644 --- a/packages/frontend/core/src/components/affine/auth/sign-in.tsx +++ b/packages/frontend/core/src/components/affine/auth/sign-in.tsx @@ -1,10 +1,12 @@ import { notify } from '@affine/component'; import { AuthInput, ModalHeader } from '@affine/component/auth-components'; import { Button } from '@affine/component/ui/button'; +import { authAtom } from '@affine/core/atoms'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { Trans, useI18n } from '@affine/i18n'; import { ArrowDownBigIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; +import { useAtomValue } from 'jotai'; import type { FC } from 'react'; import { useCallback, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -34,6 +36,7 @@ export const SignIn: FC = ({ const [verifyToken, challenge] = useCaptcha(); const [isValidEmail, setIsValidEmail] = useState(true); + const { openModal } = useAtomValue(authAtom); useEffect(() => { const timeout = setInterval(() => { @@ -45,7 +48,7 @@ export const SignIn: FC = ({ }; }, [authService]); const loginStatus = useLiveData(authService.session.status$); - if (loginStatus === 'authenticated') { + if (loginStatus === 'authenticated' && openModal) { onSignedIn?.(); } diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts index 3b04237fde59c..ee56663922266 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.css.ts @@ -1,47 +1,14 @@ import { cssVar } from '@toeverything/theme'; -import { createVar, keyframes, style } from '@vanilla-extract/css'; +import { style } from '@vanilla-extract/css'; -export const animationTimeout = createVar(); - -const contentShow = keyframes({ - from: { - opacity: 0, - }, - to: { - opacity: 1, - }, -}); -const contentHide = keyframes({ - to: { - opacity: 0, - }, - from: { - opacity: 1, - }, +export const container = style({ + maxWidth: 480, + minWidth: 360, + padding: '20px 0', + alignSelf: 'start', + marginTop: '120px', }); -export const overlay = style({ - selectors: { - '&.entered, &.entering': { - animation: `${contentShow} ${animationTimeout} forwards`, - }, - '&.exited, &.exiting': { - animation: `${contentHide} ${animationTimeout} forwards`, - }, - }, -}); - -export const container = style([ - overlay, - { - maxWidth: 480, - minWidth: 360, - padding: '20px 0', - alignSelf: 'start', - marginTop: '120px', - }, -]); - export const titleContainer = style({ display: 'flex', width: '100%', diff --git a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx index c44be15bec1f3..8827112449842 100644 --- a/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/info-modal/info-modal.tsx @@ -12,17 +12,7 @@ import { useService, type Workspace, } from '@toeverything/infra'; -import { assignInlineVars } from '@vanilla-extract/dynamic'; -import clsx from 'clsx'; -import { - Suspense, - useCallback, - useContext, - useEffect, - useMemo, - useRef, -} from 'react'; -import { useTransition } from 'react-transition-state'; +import { Suspense, useCallback, useContext, useMemo, useRef } from 'react'; import { BlocksuiteHeaderTitle } from '../../../blocksuite/block-suite-header/title'; import { managerContext } from '../common'; @@ -37,8 +27,6 @@ import * as styles from './info-modal.css'; import { TagsRow } from './tags-row'; import { TimeRow } from './time-row'; -const animationTimeout = 120; - export const InfoModal = ({ open, onOpenChange, @@ -52,14 +40,6 @@ export const InfoModal = ({ }) => { const titleInputHandleRef = useRef(null); - const [{ status }, toggle] = useTransition({ - timeout: animationTimeout, - }); - - useEffect(() => { - toggle(open); - }, [open, toggle]); - const manager = usePagePropertiesManager(page); const handleClose = useCallback(() => { @@ -80,20 +60,10 @@ export const InfoModal = ({ return ( diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx index ee88ba4146b8b..042f80f28b692 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/edit-collection.tsx @@ -23,9 +23,6 @@ export interface EditCollectionModalProps { } const contentOptions: DialogContentProps = { - onPointerDownOutside: e => { - e.preventDefault(); - }, style: { padding: 0, maxWidth: 944, @@ -60,6 +57,7 @@ export const EditCollectionModal = ({ width="calc(100% - 64px)" height="80%" contentOptions={contentOptions} + persistent > {open && init ? ( void; }>(); - const close = useCallback(() => { - onChange(undefined); + const close = useCallback((open: boolean) => { + if (!open) { + onChange(undefined); + } }, []); + const handleCancel = useCallback(() => { + close(false); + }, [close]); return { node: ( ) : null} @@ -48,7 +53,7 @@ export const useSelectPage = ({ onChange({ init, onConfirm: list => { - close(); + close(false); res(list); }, }); diff --git a/packages/frontend/core/src/components/page-list/view/use-edit-collection.tsx b/packages/frontend/core/src/components/page-list/view/use-edit-collection.tsx index 11a0ffb642504..5afb09d16c589 100644 --- a/packages/frontend/core/src/components/page-list/view/use-edit-collection.tsx +++ b/packages/frontend/core/src/components/page-list/view/use-edit-collection.tsx @@ -11,18 +11,22 @@ export const useEditCollection = () => { mode?: 'page' | 'rule'; onConfirm: (collection: Collection) => void; }>(); - const close = useCallback(() => setData(undefined), []); + const close = useCallback((open: boolean) => { + if (!open) { + setData(undefined); + } + }, []); return { - node: data ? ( + node: ( {})} /> - ) : null, + ), open: ( collection: Collection, mode?: EditCollectionMode @@ -50,19 +54,23 @@ export const useEditCollectionName = ({ name: string; onConfirm: (name: string) => void; }>(); - const close = useCallback(() => setData(undefined), []); + const close = useCallback((open: boolean) => { + if (!open) { + setData(undefined); + } + }, []); return { - node: data ? ( + node: ( {})} /> - ) : null, + ), open: (name: string): Promise => new Promise(res => { setData({