Skip to content

Commit 03c2051

Browse files
committed
feat(component): startScopedViewTranstion func to support scoped view transition (#8093)
AF-1293
1 parent 73dd1d3 commit 03c2051

File tree

7 files changed

+72
-26
lines changed

7 files changed

+72
-26
lines changed

packages/frontend/component/src/ui/modal/modal.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import clsx from 'clsx';
1212
import type { CSSProperties, MouseEvent } from 'react';
1313
import { forwardRef, useCallback, useEffect, useState } from 'react';
1414

15+
import { startScopedViewTransition } from '../../utils';
1516
import type { IconButtonProps } from '../button';
1617
import { IconButton } from '../button';
1718
import * as styles from './styles.css';
@@ -86,21 +87,19 @@ class ModalTransitionContainer extends HTMLElement {
8687
}
8788

8889
this.animationFrame = requestAnimationFrame(() => {
89-
if (typeof document.startViewTransition === 'function') {
90-
const nodes = this.pendingTransitionNodes;
90+
const nodes = this.pendingTransitionNodes;
91+
nodes.forEach(child => {
92+
if (child instanceof HTMLElement) {
93+
child.classList.add('vt-active');
94+
}
95+
});
96+
startScopedViewTransition(styles.modalVTScope, () => {
9197
nodes.forEach(child => {
92-
if (child instanceof HTMLElement) {
93-
child.classList.add('vt-active');
94-
}
95-
});
96-
document.startViewTransition(() => {
97-
nodes.forEach(child => {
98-
// eslint-disable-next-line unicorn/prefer-dom-node-remove
99-
super.removeChild(child);
100-
});
98+
// eslint-disable-next-line unicorn/prefer-dom-node-remove
99+
super.removeChild(child);
101100
});
102-
this.pendingTransitionNodes = [];
103-
}
101+
});
102+
this.pendingTransitionNodes = [];
104103
});
105104
}
106105
}

packages/frontend/component/src/ui/modal/styles.css.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ import {
77
keyframes,
88
style,
99
} from '@vanilla-extract/css';
10+
11+
import { vtScopeSelector } from '../../utils/view-transition';
1012
export const widthVar = createVar('widthVar');
1113
export const heightVar = createVar('heightVar');
1214
export const minHeightVar = createVar('minHeightVar');
1315

16+
export const modalVTScope = generateIdentifier('modal');
17+
1418
const overlayShow = keyframes({
1519
from: {
1620
opacity: 0,
@@ -94,14 +98,14 @@ export const modalContentWrapper = style({
9498
animation: `${contentShowFadeScaleTop} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
9599
animationFillMode: 'forwards',
96100
},
97-
'&.anim-fadeScaleTop.vt-active': {
101+
[`${vtScopeSelector(modalVTScope)} &.anim-fadeScaleTop.vt-active`]: {
98102
viewTransitionName: modalContentViewTransitionNameFadeScaleTop,
99103
},
100104
'&.anim-slideBottom': {
101105
animation: `${contentShowSlideBottom} 0.23s ease`,
102106
animationFillMode: 'forwards',
103107
},
104-
'&.anim-slideBottom.vt-active': {
108+
[`${vtScopeSelector(modalVTScope)} &.anim-slideBottom.vt-active`]: {
105109
viewTransitionName: modalContentViewTransitionNameSlideBottom,
106110
},
107111
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './observe-resize';
2+
export { startScopedViewTransition } from './view-transition';

packages/frontend/component/src/utils/observe-resize.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ let _resizeObserver: ResizeObserver | null = null;
99
const elementsMap = new WeakMap<Element, Array<ObserveResize>>();
1010

1111
// for debugging
12-
(window as any)._resizeObserverElementsMap = elementsMap;
13-
12+
if (typeof window !== 'undefined') {
13+
(window as any)._resizeObserverElementsMap = elementsMap;
14+
}
1415
/**
1516
* @internal get or initialize the ResizeObserver instance
1617
*/
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const setScope = (scope: string) =>
2+
document.body.setAttribute(`data-${scope}`, '');
3+
const rmScope = (scope: string) =>
4+
document.body.removeAttribute(`data-${scope}`);
5+
6+
/**
7+
* A wrapper around `document.startViewTransition` that adds a scope attribute to the body element.
8+
*/
9+
export function startScopedViewTransition(
10+
scope: string | string[],
11+
cb: () => Promise<void> | void,
12+
options?: { timeout?: number }
13+
) {
14+
if (typeof document === 'undefined') return;
15+
16+
if (typeof document.startViewTransition === 'function') {
17+
const scopes = Array.isArray(scope) ? scope : [scope];
18+
const timeout = options?.timeout ?? 2000;
19+
20+
scopes.forEach(setScope);
21+
22+
const vt = document.startViewTransition(cb);
23+
const timeoutPromise = new Promise<void>((_, reject) => {
24+
setTimeout(() => reject(new Error('View transition timeout')), timeout);
25+
});
26+
27+
Promise.race([vt.finished, timeoutPromise])
28+
.catch(err => console.error(`View transition[${scope}] failed: ${err}`))
29+
.finally(() => scopes.forEach(rmScope));
30+
} else {
31+
cb()?.catch(console.error);
32+
}
33+
}
34+
35+
export function vtScopeSelector(scope: string) {
36+
return `[data-${scope}]`;
37+
}

packages/frontend/mobile/src/components/search-input/style.css.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { cssVarV2 } from '@toeverything/theme/v2';
2-
import { style } from '@vanilla-extract/css';
2+
import { generateIdentifier, style } from '@vanilla-extract/css';
3+
4+
export const searchVTName = generateIdentifier('mobile-search-input');
5+
export const searchVTScope = generateIdentifier('mobile-search');
36

47
export const wrapper = style({
58
position: 'relative',
69
backgroundColor: cssVarV2('layer/background/primary'),
7-
viewTransitionName: 'mobile-search-input',
10+
11+
selectors: {
12+
[`[data-${searchVTScope}] &`]: {
13+
viewTransitionName: searchVTName,
14+
},
15+
},
816
});
917

1018
export const prefixIcon = style({

packages/frontend/mobile/src/views/home-header/index.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IconButton } from '@affine/component';
1+
import { IconButton, startScopedViewTransition } from '@affine/component';
22
import { openSettingModalAtom } from '@affine/core/atoms';
33
import { WorkbenchService } from '@affine/core/modules/workbench';
44
import { useI18n } from '@affine/i18n';
@@ -9,6 +9,7 @@ import { useSetAtom } from 'jotai';
99
import { useCallback, useState } from 'react';
1010

1111
import { SearchInput, WorkspaceSelector } from '../../components';
12+
import { searchVTScope } from '../../components/search-input/style.css';
1213
import { useGlobalEvent } from '../../hooks/use-global-events';
1314
import * as styles from './styles.css';
1415

@@ -33,13 +34,8 @@ export const HomeHeader = () => {
3334
);
3435

3536
const navSearch = useCallback(() => {
36-
if (!document.startViewTransition) {
37-
return workbench.open('/search');
38-
}
39-
40-
document.startViewTransition(() => {
37+
startScopedViewTransition(searchVTScope, () => {
4138
workbench.open('/search');
42-
return new Promise(resolve => setTimeout(resolve, 150));
4339
});
4440
}, [workbench]);
4541

0 commit comments

Comments
 (0)