Repository: facebook/react
Pull request: Rename <ViewTransition className="..."> to <ViewTransition default="...">

It was always confusing that this is not a CSS class but a view-transition-class.

The `className` sticks out a bit among its siblings `enter`, `exit`, `update` and `share`. The idea is that the most specific definition override is the class name that gets applied and this prop is really just the fallback, catch-all or "any" that is applied if you didn't specify a more specific one.

It has also since evolved not just to take a string but also a map of Transition Type to strings.

The "class" is really the type of the value. We could add a suffix to all of them like `defaultClass`, `enterClass`, `exitClass`, `updateClass` and `shareClass`. However, this doesn't necessarily make sense with the mapping of Transition Type to string. It also makes it a bit too DOM centric. In React Native this might still be called a "class" but it might be represented by an object definition. We might even allow some kind of inline style form for the DOM too. Really this is about picking which "animation" that runs which can be a string or instance. "Animation" is too broad because there's also a concept of a CSS Animation and these are really sets of CSS animations (group, image-pair, old, new). It could maybe be `defaultTransition`, `enterTransition`, etc but that seems unnecessarily repetitive and still doesn't say anything about it being a class.

We also already have the name "default" in the map of Transition Types. In fact you can now specify a default for default:

```
<ViewTransition default={{"navigation-back": "slide-out", "default": "fade-in"}}>
```

One thing I don't like about the name `"default"` is that it might be common to just apply a named class that does it matching to enter/exit/update in the CSS selectors (such as the `:only-child` rule) instead of doing that mapping to each one using React. In that can you end up specifying only `default={...}` a lot and then what is it the "default" for? It's more like "all". I think it's likely that you end up with either "default" or the specific forms instead of both at once.

Changed files:
filename: fixtures/view-transition/src/components/Page.js
patch: @@ -33,7 +33,7 @@ const b = (
 function Component() {
   return (
     <ViewTransition
-      className={
+      default={
         transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
       }>
       <p className="roboto-font">Slide In from Left, Slide Out to Right</p>
@@ -97,17 +97,17 @@ export default function Page({url, navigate}) {
           }}>
           {url === '/?b' ? 'Goto A' : 'Goto B'}
         </button>
-        <ViewTransition className="none">
+        <ViewTransition default="none">
           <div>
             <ViewTransition>
               <div>
-                <ViewTransition className={transitions['slide-on-nav']}>
+                <ViewTransition default={transitions['slide-on-nav']}>
                   <h1>{!show ? 'A' : 'B' + counter}</h1>
                 </ViewTransition>
               </div>
             </ViewTransition>
             <ViewTransition
-              className={{
+              default={{
                 'navigation-back': transitions['slide-right'],
                 'navigation-forward': transitions['slide-left'],
               }}>

filename: packages/react-reconciler/src/ReactFiberApplyGesture.js
patch: @@ -151,7 +151,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void {
             // and can stop searching (size reaches zero).
             pairs.delete(name);
             const className: ?string = getViewTransitionClassName(
-              props.className,
+              props.default,
               props.share,
             );
             if (className !== 'none') {
@@ -196,7 +196,7 @@ function trackEnterViewTransitions(deletion: Fiber): void {
         ? appearingViewTransitions.get(name)
         : undefined;
     const className: ?string = getViewTransitionClassName(
-      props.className,
+      props.default,
       pair !== undefined ? props.share : props.enter,
     );
     if (className !== 'none') {
@@ -259,7 +259,7 @@ function applyAppearingPairViewTransition(child: Fiber): void {
       // Note that this class name that doesn't actually really matter because the
       // "new" side will be the one that wins in practice.
       const className: ?string = getViewTransitionClassName(
-        props.className,
+        props.default,
         props.share,
       );
       if (className !== 'none') {
@@ -282,7 +282,7 @@ function applyExitViewTransition(placement: Fiber): void {
   const props: ViewTransitionProps = placement.memoizedProps;
   const name = getViewTransitionName(props, state);
   const className: ?string = getViewTransitionClassName(
-    props.className,
+    props.default,
     // Note that just because we don't have a pair yet doesn't mean we won't find one
     // later. However, that doesn't matter because if we do the class name that wins
     // is the one applied by the "new" side anyway.
@@ -307,7 +307,7 @@ function applyNestedViewTransition(child: Fiber): void {
   const props: ViewTransitionProps = child.memoizedProps;
   const name = getViewTransitionName(props, state);
   const className: ?string = getViewTransitionClassName(
-    props.className,
+    props.default,
     props.update,
   );
   if (className !== 'none') {
@@ -336,7 +336,7 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
   // want the props from "current" since that's the class that would've won if
   // it was the normal direction. To preserve the same effect in either direction.
   const className: ?string = getViewTransitionClassName(
-    newProps.className,
+    newProps.default,
     newProps.update,
   );
   if (className === 'none') {

filename: packages/react-reconciler/src/ReactFiberBeginWork.js
patch: @@ -322,6 +322,7 @@ export let didWarnAboutReassigningProps: boolean;
 let didWarnAboutRevealOrder;
 let didWarnAboutTailOptions;
 let didWarnAboutDefaultPropsOnFunctionComponent;
+let didWarnAboutClassNameOnViewTransition;
 
 if (__DEV__) {
   didWarnAboutBadClass = ({}: {[string]: boolean});
@@ -332,6 +333,7 @@ if (__DEV__) {
   didWarnAboutRevealOrder = ({}: {[empty]: boolean});
   didWarnAboutTailOptions = ({}: {[string]: boolean});
   didWarnAboutDefaultPropsOnFunctionComponent = ({}: {[string]: boolean});
+  didWarnAboutClassNameOnViewTransition = ({}: {[string]: boolean});
 }
 
 export function reconcileChildren(
@@ -3295,6 +3297,25 @@ function updateViewTransition(
       pushMaterializedTreeId(workInProgress);
     }
   }
+  if (__DEV__) {
+    // $FlowFixMe[prop-missing]
+    if (pendingProps.className !== undefined) {
+      const example =
+        typeof pendingProps.className === 'string'
+          ? JSON.stringify(pendingProps.className)
+          : '{...}';
+      if (!didWarnAboutClassNameOnViewTransition[example]) {
+        didWarnAboutClassNameOnViewTransition[example] = true;
+        console.error(
+          '<ViewTransition> doesn\'t accept a "className" prop. It has been renamed to "default".\n' +
+            '-   <ViewTransition className=%s>\n' +
+            '+   <ViewTransition default=%s>',
+          example,
+          example,
+        );
+      }
+    }
+  }
   if (current !== null && current.memoizedProps.name !== pendingProps.name) {
     // If the name changes, we schedule a ref effect to create a new ref instance.
     workInProgress.flags |= Ref | RefStatic;

filename: packages/react-reconciler/src/ReactFiberCommitViewTransitions.js
patch: @@ -228,7 +228,7 @@ function commitAppearingPairViewTransitions(placement: Fiber): void {
           }
           const name = props.name;
           const className: ?string = getViewTransitionClassName(
-            props.className,
+            props.default,
             props.share,
           );
           if (className !== 'none') {
@@ -267,7 +267,7 @@ export function commitEnterViewTransitions(
     const props: ViewTransitionProps = placement.memoizedProps;
     const name = getViewTransitionName(props, state);
     const className: ?string = getViewTransitionClassName(
-      props.className,
+      props.default,
       state.paired ? props.share : props.enter,
     );
     if (className !== 'none') {
@@ -337,7 +337,7 @@ function commitDeletedPairViewTransitions(deletion: Fiber): void {
           const pair = pairs.get(name);
           if (pair !== undefined) {
             const className: ?string = getViewTransitionClassName(
-              props.className,
+              props.default,
               props.share,
             );
             if (className !== 'none') {
@@ -389,7 +389,7 @@ export function commitExitViewTransitions(deletion: Fiber): void {
         ? appearingViewTransitions.get(name)
         : undefined;
     const className: ?string = getViewTransitionClassName(
-      props.className,
+      props.default,
       pair !== undefined ? props.share : props.exit,
     );
     if (className !== 'none') {
@@ -470,7 +470,7 @@ export function commitBeforeUpdateViewTransition(
   // a layout only change, then the "foo" class will be applied even though
   // it was not actually an update. Which is a bug.
   const className: ?string = getViewTransitionClassName(
-    newProps.className,
+    newProps.default,
     newProps.update,
   );
   if (className === 'none') {
@@ -495,7 +495,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
       const props: ViewTransitionProps = child.memoizedProps;
       const name = getViewTransitionName(props, child.stateNode);
       const className: ?string = getViewTransitionClassName(
-        props.className,
+        props.default,
         props.update,
       );
       if (className !== 'none') {
@@ -735,7 +735,7 @@ export function measureUpdateViewTransition(
   const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
   // Whether it ends up having been updated or relayout we apply the update class name.
   const className: ?string = getViewTransitionClassName(
-    props.className,
+    props.default,
     props.update,
   );
   if (className === 'none') {
@@ -787,7 +787,7 @@ export function measureNestedViewTransitions(
       const state: ViewTransitionState = child.stateNode;
       const name = getViewTransitionName(props, state);
       const className: ?string = getViewTransitionClassName(
-        props.className,
+        props.default,
         props.update,
       );
       let previousMeasurements: null | Array<InstanceMeasurement>;

filename: packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
patch: @@ -29,7 +29,7 @@ export type ViewTransitionClass = 'none' | string | ViewTransitionClassPerType;
 export type ViewTransitionProps = {
   name?: string,
   children?: ReactNodeList,
-  className?: ViewTransitionClass,
+  default?: ViewTransitionClass,
   enter?: ViewTransitionClass,
   exit?: ViewTransitionClass,
   share?: ViewTransitionClass,
@@ -129,11 +129,5 @@ export function getViewTransitionClassName(
   if (eventClassName == null) {
     return className;
   }
-  if (eventClassName === 'none') {
-    return eventClassName;
-  }
-  if (className != null && className !== 'none') {
-    return className + ' ' + eventClassName;
-  }
   return eventClassName;
 }