mirror of
https://github.com/facebook/react.git
synced 2026-02-24 04:33:04 +00:00
Stacked on #33194 and #33200. When Suspense boundaries reveal during streaming, the Fizz runtime will be responsible for animating the reveal if necessary (not in this PR). However, for the future runtime to know what to do it needs to know about the `<ViewTransition>` configuration to apply. Ofc, these are virtual nodes that disappear from the HTML. We could model them as comments like we do with other virtual nodes like Suspense and Activity. However, that doesn't let us target them with querySelector and CSS (for no-JS transitions). We also don't have to model every ViewTransition since not every combination can happen using only the server runtime. So instead this collapses `<ViewTransition>` and applies the configuration to the inner DOM nodes. ```js <ViewTransition name="hi"> <div /> <div /> </ViewTransition> ``` Becomes: ```html <div vt-name="hi" vt-update="auto"></div> <div vt-name="hi_1" vt-update="auto"></div> ``` I use `vt-` prefix as opposed to `data-` to keep these virtual attributes away from user specific ones but we're effectively claiming this namespace. There are four triggers `vt-update`, `vt-enter`, `vt-exit` and `vt-share`. The server resolves which ones might apply to this DOM node. The value represents the class name (after resolving view-transition-type mappings) or `"auto"` if no specific class name is needed but this is still a trigger. The value can also be `"none"`. This is different from missing because for example an `vt-update="none"` will block mutations inside it from triggering the boundary where as a missing `vt-update` would bubble up to be handled by a parent. `vt-name` is technically only necessary when `vt-share` is specified to find a pair. However, since an explicit name can also be used to target specific CSS selectors, we include it even for other cases. We want to exclude as many of these annotations as possible. `vt-enter` can only affect the first DOM node inside a Suspense boundary's content since the reveal would cause it to enter but nothing deeper inside. Similarly `vt-exit` can only affect the first DOM node inside a fallback. So for every other case we can exclude them. (For future MPA ViewTransitions of the whole document it might also be something we annotate to children inside the `<body>` as well.) Ideally we'd only include `vt-enter` for Suspense boundaries that actually flushed a fallback but since we prepare all that content earlier it's hard to know. `vt-share` can be anywhere inside an fallback or content. Technically we don't have to include it outside the root most Suspense boundary or for boundaries that are inlined into the root shell. However, this is tricky to detect. It would also not be correct for future MPA ViewTransitions because in that case the shared scenario can affect anything in the two documents so it needs to be in every node everywhere which is effectively what we do. If a `share` class is specified but it has no explicit name, we can exclude it since it can't match anything. `vt-update` is only necessary if something below or a sibling might update like a Suspense boundary. However, since we don't know when rendering a segment if it'll later asynchronously add a Suspense boundary later we have to assume that anywhere might have a child. So these are always included. We collapse to use the inner most one when directly nested though since that's the one that ends up winning. There are some weird edge cases that can't be fully modeled by the lack of virtual nodes.
73 lines
2.1 KiB
JavaScript
73 lines
2.1 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {ViewTransitionProps, ViewTransitionClass} from 'shared/ReactTypes';
|
|
import type {TreeContext} from './ReactFizzTreeContext';
|
|
import type {ResumableState} from './ReactFizzConfig';
|
|
|
|
import {getTreeId} from './ReactFizzTreeContext';
|
|
import {makeId} from './ReactFizzConfig';
|
|
|
|
export function getViewTransitionName(
|
|
props: ViewTransitionProps,
|
|
treeContext: TreeContext,
|
|
resumableState: ResumableState,
|
|
): string {
|
|
if (props.name != null && props.name !== 'auto') {
|
|
return props.name;
|
|
}
|
|
const treeId = getTreeId(treeContext);
|
|
return makeId(resumableState, treeId, 0);
|
|
}
|
|
|
|
function getClassNameByType(classByType: ?ViewTransitionClass): ?string {
|
|
if (classByType == null || typeof classByType === 'string') {
|
|
return classByType;
|
|
}
|
|
let className: ?string = null;
|
|
const activeTypes = null; // TODO: Support passing active types.
|
|
if (activeTypes !== null) {
|
|
for (let i = 0; i < activeTypes.length; i++) {
|
|
const match = classByType[activeTypes[i]];
|
|
if (match != null) {
|
|
if (match === 'none') {
|
|
// If anything matches "none" that takes precedence over any other
|
|
// type that also matches.
|
|
return 'none';
|
|
}
|
|
if (className == null) {
|
|
className = match;
|
|
} else {
|
|
className += ' ' + match;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (className == null) {
|
|
// We had no other matches. Match the default for this configuration.
|
|
return classByType.default;
|
|
}
|
|
return className;
|
|
}
|
|
|
|
export function getViewTransitionClassName(
|
|
defaultClass: ?ViewTransitionClass,
|
|
eventClass: ?ViewTransitionClass,
|
|
): ?string {
|
|
const className: ?string = getClassNameByType(defaultClass);
|
|
const eventClassName: ?string = getClassNameByType(eventClass);
|
|
if (eventClassName == null) {
|
|
return className === 'auto' ? null : className;
|
|
}
|
|
if (eventClassName === 'auto') {
|
|
return null;
|
|
}
|
|
return eventClassName;
|
|
}
|