diff --git a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx index 228e6f3004..cbbdbf4945 100644 --- a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx @@ -5,6 +5,7 @@ import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error import { BlockSuiteEditor } from '@affine/core/components/blocksuite/block-suite-editor'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { PageNotFound } from '@affine/core/pages/404'; +import { DebugLogger } from '@affine/debug'; import { Bound, type EdgelessRootService } from '@blocksuite/blocks'; import { DisposableGroup } from '@blocksuite/global/utils'; import type { AffineEditorContainer } from '@blocksuite/presets'; @@ -18,31 +19,37 @@ import { PeekViewService } from '../../services/peek-view'; import { useDoc } from '../utils'; import * as styles from './doc-peek-view.css'; +const logger = new DebugLogger('doc-peek-view'); + function fitViewport( editor: AffineEditorContainer, xywh?: `[${number},${number},${number},${number}]` ) { - const rootService = - editor.host.std.spec.getService('affine:page'); - rootService.viewport.onResize(); + try { + const rootService = + editor.host.std.spec.getService('affine:page'); + rootService.viewport.onResize(); - if (xywh) { - const viewport = { - xywh: xywh, - padding: [60, 20, 20, 20] as [number, number, number, number], - }; - rootService.viewport.setViewportByBound( - Bound.deserialize(viewport.xywh), - viewport.padding, - false - ); - } else { - const data = rootService.getFitToScreenData(); - rootService.viewport.setViewport( - data.zoom, - [data.centerX, data.centerY], - false - ); + if (xywh) { + const viewport = { + xywh: xywh, + padding: [60, 20, 20, 20] as [number, number, number, number], + }; + rootService.viewport.setViewportByBound( + Bound.deserialize(viewport.xywh), + viewport.padding, + false + ); + } else { + const data = rootService.getFitToScreenData(); + rootService.viewport.setViewport( + data.zoom, + [data.centerX, data.centerY], + false + ); + } + } catch (e) { + logger.warn('failed to fitViewPort', e); } } @@ -71,21 +78,13 @@ export function DocPeekPreview({ const [resolvedMode, setResolvedMode] = useState(mode); useEffect(() => { - requestAnimationFrame(() => { - if (editor && editor.host && resolvedMode === 'edgeless') { - editor.host - .closest('[data-testid="peek-view-modal-animation-container"]') - ?.addEventListener( - 'animationend', - () => { - fitViewport(editor, xywh); - }, - { - once: true, - } - ); - } - }); + editor?.updateComplete + .then(() => { + if (resolvedMode === 'edgeless') { + fitViewport(editor, xywh); + } + }) + .catch(console.error); return; }, [editor, resolvedMode, xywh]); diff --git a/packages/frontend/core/src/modules/peek-view/view/modal-container.css.ts b/packages/frontend/core/src/modules/peek-view/view/modal-container.css.ts index 56bf7e0075..c5afe1ab9c 100644 --- a/packages/frontend/core/src/modules/peek-view/view/modal-container.css.ts +++ b/packages/frontend/core/src/modules/peek-view/view/modal-container.css.ts @@ -1,5 +1,11 @@ import { cssVar } from '@toeverything/theme'; -import { createVar, keyframes, style } from '@vanilla-extract/css'; +import { + createVar, + generateIdentifier, + globalStyle, + keyframes, + style, +} from '@vanilla-extract/css'; export const animationTimeout = createVar(); export const transformOrigin = createVar(); @@ -42,26 +48,50 @@ const fadeOut = keyframes({ }, }); -const slideRight = keyframes({ - from: { - transform: 'translateX(-200%)', - opacity: 0, - }, - to: { - transform: 'translateX(0)', - opacity: 1, - }, +// every item must have its own unique view-transition-name +const vtContentZoom = generateIdentifier('content-zoom'); +const vtContentFade = generateIdentifier('content-fade'); +const vtOverlayFade = generateIdentifier('content-fade'); + +globalStyle(`::view-transition-group(${vtOverlayFade})`, { + animationDuration: animationTimeout, }); -const slideLeft = keyframes({ - from: { - transform: 'translateX(0)', - opacity: 1, - }, - to: { - transform: 'translateX(-200%)', - opacity: 0, - }, +globalStyle(`::view-transition-new(${vtOverlayFade})`, { + animationName: fadeIn, +}); + +globalStyle(`::view-transition-old(${vtOverlayFade})`, { + animationName: fadeOut, +}); + +globalStyle( + `::view-transition-group(${vtContentZoom}), + ::view-transition-group(${vtContentFade})`, + { + animationDuration: animationTimeout, + animationFillMode: 'forwards', + animationTimingFunction: 'cubic-bezier(0.42, 0, 0.58, 1)', + } +); + +globalStyle(`::view-transition-new(${vtContentZoom})`, { + animationName: zoomIn, + // origin has to be set in ::view-transition-new/old + transformOrigin: transformOrigin, +}); + +globalStyle(`::view-transition-old(${vtContentZoom})`, { + animationName: zoomOut, + transformOrigin: transformOrigin, +}); + +globalStyle(`::view-transition-new(${vtContentFade})`, { + animationName: fadeIn, +}); + +globalStyle(`::view-transition-old(${vtContentFade})`, { + animationName: fadeOut, }); export const modalOverlay = style({ @@ -69,17 +99,7 @@ export const modalOverlay = style({ inset: 0, zIndex: cssVar('zIndexModal'), backgroundColor: cssVar('black30'), - opacity: 0, - selectors: { - '&[data-state=entered], &[data-state=entering]': { - animation: `${fadeIn} ${animationTimeout} ease-in-out`, - animationFillMode: 'forwards', - }, - '&[data-state=exited], &[data-state=exiting]': { - animation: `${fadeOut} ${animationTimeout} ease-in-out`, - animationFillMode: 'backwards', - }, - }, + viewTransitionName: vtOverlayFade, }); export const modalContentWrapper = style({ @@ -96,42 +116,14 @@ export const modalContentContainer = style({ alignItems: 'flex-start', width: '100%', height: '100%', - willChange: 'transform, opacity', - transformOrigin: transformOrigin, - selectors: { - '&[data-state=entered], &[data-state=entering]': { - animationFillMode: 'forwards', - animationDuration: animationTimeout, - animationTimingFunction: 'cubic-bezier(0.42, 0, 0.58, 1)', - }, - '&[data-state=exited], &[data-state=exiting]': { - animationFillMode: 'forwards', - animationDuration: animationTimeout, - animationTimingFunction: 'cubic-bezier(0.42, 0, 0.58, 1)', - }, - }, }); export const modalContentContainerWithZoom = style({ - selectors: { - '&[data-state=entered], &[data-state=entering]': { - animationName: zoomIn, - }, - '&[data-state=exited], &[data-state=exiting]': { - animationName: zoomOut, - }, - }, + viewTransitionName: vtContentZoom, }); export const modalContentContainerWithFade = style({ - selectors: { - '&[data-state=entered], &[data-state=entering]': { - animationName: fadeIn, - }, - '&[data-state=exited], &[data-state=exiting]': { - animationName: fadeOut, - }, - }, + viewTransitionName: vtContentFade, }); export const containerPadding = style({ @@ -162,20 +154,5 @@ export const modalControls = style({ zIndex: -1, minWidth: '48px', padding: '8px 0 0 16px', - opacity: 0, pointerEvents: 'auto', - selectors: { - '&[data-state=entered], &[data-state=entering]': { - animationName: slideRight, - animationDuration: animationTimeout, - animationFillMode: 'forwards', - animationTimingFunction: 'ease-in-out', - }, - '&[data-state=exited], &[data-state=exiting]': { - animationName: slideLeft, - animationDuration: animationTimeout, - animationFillMode: 'forwards', - animationTimingFunction: 'ease-in-out', - }, - }, }); diff --git a/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx b/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx index 0dbce1c46f..72b865bb70 100644 --- a/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/modal-container.tsx @@ -1,5 +1,4 @@ import * as Dialog from '@radix-ui/react-dialog'; -import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; import { createContext, @@ -9,7 +8,7 @@ import { useEffect, useState, } from 'react'; -import useTransition from 'react-transition-state'; +import { flushSync } from 'react-dom'; import * as styles from './modal-container.css'; @@ -42,6 +41,15 @@ export const useInsidePeekView = () => { return !!context; }; +/** + * Convert var(--xxx) to --xxx + * @param fullName + * @returns + */ +function toCssVarName(fullName: string) { + return fullName.slice(4, -1); +} + function getElementScreenPositionCenter(target: HTMLElement) { const rect = target.getBoundingClientRect(); @@ -63,7 +71,6 @@ export type PeekViewModalContainerProps = PropsWithChildren<{ open: boolean; target?: HTMLElement; controls?: React.ReactNode; - hideOnEntering?: boolean; onAnimationStart?: () => void; onAnimateEnd?: () => void; padding?: boolean; @@ -81,7 +88,6 @@ export const PeekViewModalContainer = forwardRef< target, controls, children, - hideOnEntering, onAnimationStart, onAnimateEnd, animation = 'zoom', @@ -90,33 +96,40 @@ export const PeekViewModalContainer = forwardRef< }, ref ) { - const [{ status }, toggle] = useTransition({ - timeout: animationTimeout, - }); - const [transformOrigin, setTransformOrigin] = useState(null); + const [vtOpen, setVtOpen] = useState(open); + useEffect(() => { + document.startViewTransition(() => { + flushSync(() => { + setVtOpen(open); + }); + }); + }, [open]); + useEffect(() => { - toggle(open); const bondingBox = target ? getElementScreenPositionCenter(target) : null; const offsetLeft = (window.innerWidth - Math.min(window.innerWidth * 0.9, 1200)) / 2; const modalHeight = window.innerHeight * 0.05; - setTransformOrigin( - bondingBox - ? `${bondingBox.x - offsetLeft}px ${bondingBox.y - modalHeight}px` - : null + const transformOrigin = bondingBox + ? `${bondingBox.x - offsetLeft}px ${bondingBox.y - modalHeight}px` + : null; + + document.documentElement.style.setProperty( + toCssVarName(styles.transformOrigin), + transformOrigin + ); + + document.documentElement.style.setProperty( + toCssVarName(styles.animationTimeout), + animationTimeout + 'ms' ); }, [open, target]); return ( - + @@ -125,10 +138,6 @@ export const PeekViewModalContainer = forwardRef< data-testid={testId} data-peek-view-wrapper className={styles.modalContentWrapper} - style={assignInlineVars({ - [styles.transformOrigin]: transformOrigin, - [styles.animationTimeout]: `${animationTimeout}ms`, - })} >
- {hideOnEntering && status === 'entering' ? null : children} + {children} {controls ? ( -
- {controls} -
+
{controls}
) : null}