feat(core): adjust center peek animation (#7393)
This commit is contained in:
@@ -133,9 +133,17 @@ function resolvePeekInfoFromPeekTarget(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PeekViewAnimation = 'fade' | 'zoom' | 'none';
|
||||||
|
|
||||||
export class PeekViewEntity extends Entity {
|
export class PeekViewEntity extends Entity {
|
||||||
private readonly _active$ = new LiveData<ActivePeekView | null>(null);
|
private readonly _active$ = new LiveData<ActivePeekView | null>(null);
|
||||||
private readonly _show$ = new LiveData<boolean>(false);
|
private readonly _show$ = new LiveData<{
|
||||||
|
animation: PeekViewAnimation;
|
||||||
|
value: boolean;
|
||||||
|
}>({
|
||||||
|
animation: 'zoom',
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
|
||||||
constructor(private readonly workbenchService: WorkbenchService) {
|
constructor(private readonly workbenchService: WorkbenchService) {
|
||||||
super();
|
super();
|
||||||
@@ -143,7 +151,7 @@ export class PeekViewEntity extends Entity {
|
|||||||
|
|
||||||
active$ = this._active$.distinctUntilChanged();
|
active$ = this._active$.distinctUntilChanged();
|
||||||
show$ = this._show$
|
show$ = this._show$
|
||||||
.map(show => show && this._active$.value !== null)
|
.map(show => (this._active$.value !== null ? show : null))
|
||||||
.distinctUntilChanged();
|
.distinctUntilChanged();
|
||||||
|
|
||||||
// return true if the peek view will be handled
|
// return true if the peek view will be handled
|
||||||
@@ -159,17 +167,23 @@ export class PeekViewEntity extends Entity {
|
|||||||
const active = this._active$.value;
|
const active = this._active$.value;
|
||||||
|
|
||||||
// if there is an active peek view and it is a doc peek view, we will navigate it first
|
// if there is an active peek view and it is a doc peek view, we will navigate it first
|
||||||
if (active?.info.type === 'doc' && this.show$.value) {
|
if (active?.info.type === 'doc' && this.show$.value?.value) {
|
||||||
// TODO(@pengx17): scroll to the viewing position?
|
// TODO(@pengx17): scroll to the viewing position?
|
||||||
this.workbenchService.workbench.openDoc(active.info.docId);
|
this.workbenchService.workbench.openDoc(active.info.docId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._active$.next({ target, info: resolvedInfo });
|
this._active$.next({ target, info: resolvedInfo });
|
||||||
this._show$.next(true);
|
this._show$.next({
|
||||||
|
value: true,
|
||||||
|
animation: resolvedInfo.type === 'doc' ? 'zoom' : 'fade',
|
||||||
|
});
|
||||||
return firstValueFrom(race(this._active$, this.show$).pipe(map(() => {})));
|
return firstValueFrom(race(this._active$, this.show$).pipe(map(() => {})));
|
||||||
};
|
};
|
||||||
|
|
||||||
close = () => {
|
close = (animation?: PeekViewAnimation) => {
|
||||||
this._show$.next(false);
|
this._show$.next({
|
||||||
|
value: false,
|
||||||
|
animation: animation ?? this._show$.value.animation,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { OnEvent, Service } from '@toeverything/infra';
|
|||||||
import { WorkbenchLocationChanged } from '../../workbench/services/workbench';
|
import { WorkbenchLocationChanged } from '../../workbench/services/workbench';
|
||||||
import { PeekViewEntity } from '../entities/peek-view';
|
import { PeekViewEntity } from '../entities/peek-view';
|
||||||
|
|
||||||
@OnEvent(WorkbenchLocationChanged, e => e.peekView.close)
|
@OnEvent(WorkbenchLocationChanged, e => () => e.peekView.close())
|
||||||
export class PeekViewService extends Service {
|
export class PeekViewService extends Service {
|
||||||
public readonly peekView = this.framework.createEntity(PeekViewEntity);
|
public readonly peekView = this.framework.createEntity(PeekViewEntity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ export const ImagePreviewPeekView = (
|
|||||||
): ReactElement | null => {
|
): ReactElement | null => {
|
||||||
const [blockId, setBlockId] = useState<string | null>(props.blockId);
|
const [blockId, setBlockId] = useState<string | null>(props.blockId);
|
||||||
const peekView = useService(PeekViewService).peekView;
|
const peekView = useService(PeekViewService).peekView;
|
||||||
const onClose = peekView.close;
|
const onClose = useCallback(() => peekView.close(), [peekView]);
|
||||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,106 +1,17 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import {
|
import { style } from '@vanilla-extract/css';
|
||||||
createVar,
|
|
||||||
generateIdentifier,
|
|
||||||
globalStyle,
|
|
||||||
keyframes,
|
|
||||||
style,
|
|
||||||
} from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
export const animationTimeout = createVar();
|
|
||||||
export const transformOrigin = createVar();
|
|
||||||
export const animationType = createVar();
|
|
||||||
|
|
||||||
const zoomIn = keyframes({
|
|
||||||
from: {
|
|
||||||
transform: 'scale(0.10)',
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
transform: 'scale(1)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const zoomOut = keyframes({
|
|
||||||
to: {
|
|
||||||
opacity: 0,
|
|
||||||
transform: 'scale(0.10)',
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
opacity: 1,
|
|
||||||
transform: 'scale(1)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fadeIn = keyframes({
|
|
||||||
from: {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fadeOut = keyframes({
|
|
||||||
from: {
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
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({
|
export const modalOverlay = style({
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
inset: 0,
|
inset: 0,
|
||||||
zIndex: cssVar('zIndexModal'),
|
zIndex: cssVar('zIndexModal'),
|
||||||
backgroundColor: cssVar('black30'),
|
backgroundColor: cssVar('black30'),
|
||||||
viewTransitionName: vtOverlayFade,
|
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
|
selectors: {
|
||||||
|
'&[data-anime-state="animating"]': {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modalContentWrapper = style({
|
export const modalContentWrapper = style({
|
||||||
@@ -113,32 +24,36 @@ export const modalContentWrapper = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const modalContentContainer = style({
|
export const modalContentContainer = style({
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 12,
|
||||||
|
selectors: {
|
||||||
|
'[data-padding="true"] &': {
|
||||||
|
width: '90%',
|
||||||
|
height: '90%',
|
||||||
|
maxWidth: 1248,
|
||||||
|
},
|
||||||
|
'&[data-anime-state="animating"]': {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modalContentContainerWithZoom = style({
|
export const dialog = style({
|
||||||
viewTransitionName: vtContentZoom,
|
backgroundColor: cssVar('backgroundOverlayPanelColor'),
|
||||||
});
|
boxShadow: cssVar('shadow3'),
|
||||||
|
|
||||||
export const modalContentContainerWithFade = style({
|
|
||||||
viewTransitionName: vtContentFade,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const containerPadding = style({
|
|
||||||
width: '90%',
|
|
||||||
height: '90%',
|
|
||||||
maxWidth: 1248,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const modalContent = style({
|
export const modalContent = style({
|
||||||
flex: 1,
|
borderRadius: 'inherit',
|
||||||
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
backgroundColor: cssVar('backgroundOverlayPanelColor'),
|
flexShrink: 0,
|
||||||
backdropFilter: 'drop-shadow(0px 0px 2px rgba(0, 0, 0, 0.08))',
|
overflow: 'hidden',
|
||||||
borderRadius: '8px',
|
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
// :focus-visible will set outline
|
// :focus-visible will set outline
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
@@ -151,7 +66,9 @@ export const modalContent = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const modalControls = style({
|
export const modalControls = style({
|
||||||
flexShrink: 0,
|
position: 'absolute',
|
||||||
|
left: '100%',
|
||||||
|
top: 0,
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
minWidth: '48px',
|
minWidth: '48px',
|
||||||
padding: '8px 0 0 16px',
|
padding: '8px 0 0 16px',
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import anime, { type AnimeInstance, type AnimeParams } from 'animejs';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
type PropsWithChildren,
|
type PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
|
||||||
|
|
||||||
|
import type { PeekViewAnimation } from '../entities/peek-view';
|
||||||
import * as styles from './modal-container.css';
|
import * as styles from './modal-container.css';
|
||||||
|
|
||||||
const animationTimeout = 200;
|
|
||||||
|
|
||||||
const contentOptions: Dialog.DialogContentProps = {
|
const contentOptions: Dialog.DialogContentProps = {
|
||||||
['data-testid' as string]: 'peek-view-modal',
|
['data-testid' as string]: 'peek-view-modal',
|
||||||
onPointerDownOutside: e => {
|
onPointerDownOutside: e => {
|
||||||
@@ -31,11 +32,6 @@ const contentOptions: Dialog.DialogContentProps = {
|
|||||||
// this is because radix-ui register the escape key event on the document using capture, which is not possible to prevent in blocksuite
|
// this is because radix-ui register the escape key event on the document using capture, which is not possible to prevent in blocksuite
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
},
|
},
|
||||||
style: {
|
|
||||||
padding: 0,
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// a dummy context to let elements know they are inside a peek view
|
// a dummy context to let elements know they are inside a peek view
|
||||||
@@ -50,31 +46,6 @@ export const useInsidePeekView = () => {
|
|||||||
return !!context;
|
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();
|
|
||||||
|
|
||||||
if (rect.top === 0 || rect.left === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
||||||
const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: rect.x + scrollLeft + rect.width / 2,
|
|
||||||
y: rect.y + scrollTop + rect.height / 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PeekViewModalContainerProps = PropsWithChildren<{
|
export type PeekViewModalContainerProps = PropsWithChildren<{
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -83,8 +54,10 @@ export type PeekViewModalContainerProps = PropsWithChildren<{
|
|||||||
onAnimationStart?: () => void;
|
onAnimationStart?: () => void;
|
||||||
onAnimateEnd?: () => void;
|
onAnimateEnd?: () => void;
|
||||||
padding?: boolean;
|
padding?: boolean;
|
||||||
animation?: 'fade' | 'zoom';
|
animation?: PeekViewAnimation;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
|
/** Whether to apply shadow & bg */
|
||||||
|
dialogFrame?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const PeekViewModalOverlay = 'div';
|
const PeekViewModalOverlay = 'div';
|
||||||
@@ -103,22 +76,193 @@ export const PeekViewModalContainer = forwardRef<
|
|||||||
onAnimateEnd,
|
onAnimateEnd,
|
||||||
animation = 'zoom',
|
animation = 'zoom',
|
||||||
padding = true,
|
padding = true,
|
||||||
testId,
|
dialogFrame = true,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const [vtOpen, setVtOpen] = useState(open);
|
const [vtOpen, setVtOpen] = useState(open);
|
||||||
useEffect(() => {
|
const [animeState, setAnimeState] = useState<'idle' | 'ready' | 'animating'>(
|
||||||
if (document.startViewTransition) {
|
'idle'
|
||||||
document.startViewTransition(() => {
|
);
|
||||||
flushSync(() => {
|
const contentClipRef = useRef<HTMLDivElement>(null);
|
||||||
setVtOpen(open);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
const controlsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const prevAnimeMap = useRef<Record<string, AnimeInstance | undefined>>({});
|
||||||
|
|
||||||
|
const animateControls = useCallback((animateIn = false) => {
|
||||||
|
const controls = controlsRef.current;
|
||||||
|
if (!controls) return;
|
||||||
|
anime({
|
||||||
|
targets: controls,
|
||||||
|
opacity: animateIn ? [0, 1] : [1, 0],
|
||||||
|
translateX: animateIn ? [-32, 0] : [0, -32],
|
||||||
|
easing: 'easeOutQuad',
|
||||||
|
duration: 230,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
const zoomAnimate = useCallback(
|
||||||
|
async (
|
||||||
|
zoomIn?: boolean,
|
||||||
|
paramsMap?: {
|
||||||
|
overlay?: AnimeParams;
|
||||||
|
content?: AnimeParams;
|
||||||
|
contentWrapper?: AnimeParams;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
const contentClip = contentClipRef.current;
|
||||||
|
const content = contentRef.current;
|
||||||
|
const overlay = overlayRef.current;
|
||||||
|
|
||||||
|
if (!contentClip || !content || !target || !overlay) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targets = contentClip;
|
||||||
|
const lockSizeEl = content;
|
||||||
|
|
||||||
|
const from = zoomIn ? target : contentClip;
|
||||||
|
const to = zoomIn ? contentClip : target;
|
||||||
|
|
||||||
|
const fromRect = from.getBoundingClientRect();
|
||||||
|
const toRect = to.getBoundingClientRect();
|
||||||
|
|
||||||
|
targets.style.position = 'fixed';
|
||||||
|
targets.style.overflow = 'hidden';
|
||||||
|
targets.style.opacity = '1';
|
||||||
|
lockSizeEl.style.width = zoomIn
|
||||||
|
? `${toRect.width}px`
|
||||||
|
: `${fromRect.width}px`;
|
||||||
|
lockSizeEl.style.height = zoomIn
|
||||||
|
? `${toRect.height}px`
|
||||||
|
: `${fromRect.height}px`;
|
||||||
|
lockSizeEl.style.overflow = 'hidden';
|
||||||
|
overlay.style.pointerEvents = 'none';
|
||||||
|
|
||||||
|
prevAnimeMap.current.overlay?.pause();
|
||||||
|
prevAnimeMap.current.content?.pause();
|
||||||
|
prevAnimeMap.current.contentWrapper?.pause();
|
||||||
|
|
||||||
|
const overlayAnime = anime({
|
||||||
|
targets: overlay,
|
||||||
|
opacity: zoomIn ? [0, 1] : [1, 0],
|
||||||
|
easing: 'easeOutQuad',
|
||||||
|
duration: 230,
|
||||||
|
...paramsMap?.overlay,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentAnime =
|
||||||
|
paramsMap?.content &&
|
||||||
|
anime({
|
||||||
|
targets: content,
|
||||||
|
...paramsMap.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentWrapperAnime = anime({
|
||||||
|
targets,
|
||||||
|
left: [fromRect.left, toRect.left],
|
||||||
|
top: [fromRect.top, toRect.top],
|
||||||
|
width: [fromRect.width, toRect.width],
|
||||||
|
height: [fromRect.height, toRect.height],
|
||||||
|
easing: 'easeOutQuad',
|
||||||
|
duration: 230,
|
||||||
|
...paramsMap?.contentWrapper,
|
||||||
|
complete: (ins: AnimeInstance) => {
|
||||||
|
paramsMap?.contentWrapper?.complete?.(ins);
|
||||||
|
setAnimeState('idle');
|
||||||
|
overlay.style.pointerEvents = '';
|
||||||
|
if (zoomIn) {
|
||||||
|
Object.assign(targets.style, {
|
||||||
|
position: '',
|
||||||
|
left: '',
|
||||||
|
top: '',
|
||||||
|
width: '',
|
||||||
|
height: '',
|
||||||
|
overflow: '',
|
||||||
|
});
|
||||||
|
Object.assign(lockSizeEl.style, {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
overflow: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
prevAnimeMap.current = {
|
||||||
|
overlay: overlayAnime,
|
||||||
|
content: contentAnime,
|
||||||
|
contentWrapper: contentWrapperAnime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[target]
|
||||||
|
);
|
||||||
|
const animateZoomIn = useCallback(() => {
|
||||||
|
setAnimeState('animating');
|
||||||
|
setVtOpen(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
zoomAnimate(true, {
|
||||||
|
contentWrapper: {
|
||||||
|
opacity: [0.5, 1],
|
||||||
|
easing: 'cubicBezier(.46,.36,0,1)',
|
||||||
|
duration: 400,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
opacity: [0, 1],
|
||||||
|
duration: 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => animateControls(true))
|
||||||
|
.catch(console.error);
|
||||||
|
}, 0);
|
||||||
|
}, [animateControls, zoomAnimate]);
|
||||||
|
const animateZoomOut = useCallback(() => {
|
||||||
|
setAnimeState('animating');
|
||||||
|
animateControls(false);
|
||||||
|
zoomAnimate(false, {
|
||||||
|
contentWrapper: {
|
||||||
|
easing: 'cubicBezier(.57,.25,.76,.44)',
|
||||||
|
opacity: [1, 0.5],
|
||||||
|
duration: 180,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
opacity: [1, 0],
|
||||||
|
duration: 180,
|
||||||
|
easing: 'easeOutQuad',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => setVtOpen(false))
|
||||||
|
.catch(console.error);
|
||||||
|
}, [animateControls, zoomAnimate]);
|
||||||
|
|
||||||
|
const animateFade = useCallback((animateIn: boolean) => {
|
||||||
|
setAnimeState('animating');
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
if (animateIn) setVtOpen(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
const overlay = overlayRef.current;
|
||||||
|
const contentClip = contentClipRef.current;
|
||||||
|
if (!overlay || !contentClip) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
anime({
|
||||||
|
targets: [overlay, contentClip],
|
||||||
|
opacity: animateIn ? [0, 1] : [1, 0],
|
||||||
|
easing: 'easeOutQuad',
|
||||||
|
duration: 230,
|
||||||
|
complete: () => {
|
||||||
|
if (!animateIn) setVtOpen(false);
|
||||||
|
setAnimeState('idle');
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
});
|
||||||
setVtOpen(open);
|
}, []);
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -133,57 +277,53 @@ export const PeekViewModalContainer = forwardRef<
|
|||||||
}, [onOpenChange]);
|
}, [onOpenChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bondingBox = target ? getElementScreenPositionCenter(target) : null;
|
if (animation === 'zoom') {
|
||||||
const offsetLeft =
|
open ? animateZoomIn() : animateZoomOut();
|
||||||
(window.innerWidth - Math.min(window.innerWidth * 0.9, 1200)) / 2;
|
} else if (animation === 'fade') {
|
||||||
const modalHeight = window.innerHeight * 0.05;
|
animateFade(open).catch(console.error);
|
||||||
const transformOrigin = bondingBox
|
} else if (animation === 'none') {
|
||||||
? `${bondingBox.x - offsetLeft}px ${bondingBox.y - modalHeight}px`
|
setVtOpen(open);
|
||||||
: null;
|
}
|
||||||
|
}, [animateZoomOut, animation, open, target, animateZoomIn, animateFade]);
|
||||||
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
toCssVarName(styles.transformOrigin),
|
|
||||||
transformOrigin
|
|
||||||
);
|
|
||||||
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
toCssVarName(styles.animationTimeout),
|
|
||||||
animationTimeout + 'ms'
|
|
||||||
);
|
|
||||||
}, [open, target]);
|
|
||||||
return (
|
return (
|
||||||
<PeekViewContext.Provider value={emptyContext}>
|
<PeekViewContext.Provider value={emptyContext}>
|
||||||
<Dialog.Root modal open={vtOpen} onOpenChange={onOpenChange}>
|
<Dialog.Root modal open={vtOpen} onOpenChange={onOpenChange}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<PeekViewModalOverlay
|
<PeekViewModalOverlay
|
||||||
|
ref={overlayRef}
|
||||||
className={styles.modalOverlay}
|
className={styles.modalOverlay}
|
||||||
onAnimationStart={onAnimationStart}
|
onAnimationStart={onAnimationStart}
|
||||||
onAnimationEnd={onAnimateEnd}
|
onAnimationEnd={onAnimateEnd}
|
||||||
|
data-anime-state={animeState}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-testid={testId}
|
data-padding={padding}
|
||||||
data-peek-view-wrapper
|
data-peek-view-wrapper
|
||||||
className={styles.modalContentWrapper}
|
className={clsx(styles.modalContentWrapper)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
data-anime-state={animeState}
|
||||||
styles.modalContentContainer,
|
ref={contentClipRef}
|
||||||
padding && styles.containerPadding,
|
className={styles.modalContentContainer}
|
||||||
animation === 'fade'
|
|
||||||
? styles.modalContentContainerWithFade
|
|
||||||
: styles.modalContentContainerWithZoom
|
|
||||||
)}
|
|
||||||
data-testid="peek-view-modal-animation-container"
|
|
||||||
>
|
>
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
{...contentOptions}
|
{...contentOptions}
|
||||||
className={styles.modalContent}
|
className={clsx({
|
||||||
|
[styles.modalContent]: true,
|
||||||
|
[styles.dialog]: dialogFrame,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
<Dialog.Title style={{ display: 'none' }} />
|
||||||
|
<div style={{ height: '100%' }} ref={contentRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
{controls ? (
|
{controls ? (
|
||||||
<div className={styles.modalControls}>{controls}</div>
|
<div ref={controlsRef} className={styles.modalControls}>
|
||||||
|
{controls}
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export const DefaultPeekViewControls = ({
|
|||||||
icon: <CloseIcon />,
|
icon: <CloseIcon />,
|
||||||
nameKey: 'close',
|
nameKey: 'close',
|
||||||
name: t['com.affine.peek-view-controls.close'](),
|
name: t['com.affine.peek-view-controls.close'](),
|
||||||
onClick: peekView.close,
|
onClick: () => peekView.close(),
|
||||||
},
|
},
|
||||||
].filter((opt): opt is ControlButtonProps => Boolean(opt));
|
].filter((opt): opt is ControlButtonProps => Boolean(opt));
|
||||||
}, [peekView, t]);
|
}, [peekView, t]);
|
||||||
@@ -109,7 +109,7 @@ export const DocPeekViewControls = ({
|
|||||||
icon: <CloseIcon />,
|
icon: <CloseIcon />,
|
||||||
nameKey: 'close',
|
nameKey: 'close',
|
||||||
name: t['com.affine.peek-view-controls.close'](),
|
name: t['com.affine.peek-view-controls.close'](),
|
||||||
onClick: peekView.close,
|
onClick: () => peekView.close(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ExpandFullIcon />,
|
icon: <ExpandFullIcon />,
|
||||||
@@ -123,7 +123,7 @@ export const DocPeekViewControls = ({
|
|||||||
if (mode) {
|
if (mode) {
|
||||||
doc?.setMode(mode);
|
doc?.setMode(mode);
|
||||||
}
|
}
|
||||||
peekView.close();
|
peekView.close('none');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
environment.isDesktop && {
|
environment.isDesktop && {
|
||||||
@@ -132,7 +132,7 @@ export const DocPeekViewControls = ({
|
|||||||
name: t['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
name: t['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
workbench.openDoc(docId, { at: 'beside' });
|
workbench.openDoc(docId, { at: 'beside' });
|
||||||
peekView.close();
|
peekView.close('none');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
!environment.isDesktop && {
|
!environment.isDesktop && {
|
||||||
@@ -144,7 +144,7 @@ export const DocPeekViewControls = ({
|
|||||||
`/workspace/${workspace.id}/${docId}#${blockId ?? ''}`,
|
`/workspace/${workspace.id}/${docId}#${blockId ?? ''}`,
|
||||||
'_blank'
|
'_blank'
|
||||||
);
|
);
|
||||||
peekView.close();
|
peekView.close('none');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
].filter((opt): opt is ControlButtonProps => Boolean(opt));
|
].filter((opt): opt is ControlButtonProps => Boolean(opt));
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const getRendererProps = (
|
|||||||
? activePeekView.target
|
? activePeekView.target
|
||||||
: undefined,
|
: undefined,
|
||||||
padding: activePeekView.info.type === 'doc',
|
padding: activePeekView.info.type === 'doc',
|
||||||
animation: activePeekView.info.type === 'image' ? 'fade' : 'zoom',
|
dialogFrame: activePeekView.info.type !== 'image',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +104,8 @@ export const PeekViewManagerModal = () => {
|
|||||||
return (
|
return (
|
||||||
<PeekViewModalContainer
|
<PeekViewModalContainer
|
||||||
{...renderProps}
|
{...renderProps}
|
||||||
open={show && !!renderProps}
|
open={!!show?.value && !!renderProps}
|
||||||
|
animation={show?.animation || 'none'}
|
||||||
onOpenChange={open => {
|
onOpenChange={open => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
peekViewEntity.close();
|
peekViewEntity.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user