Repository: facebook/react
Pull request: [playground] ViewTransition on internals toggle & tab expansion

<!--
  Thanks for submitting a pull request!
  We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your pull request. The three fields below are mandatory.

  Before submitting a pull request, please make sure the following is done:

  1. Fork [the repository](https://github.com/facebook/react) and create your branch from `main`.
  2. Run `yarn` in the repository root.
  3. If you've fixed a bug or added code that should be tested, add tests!
  4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch TestName` is helpful in development.
  5. Run `yarn test --prod` to test in the production environment. It supports the same options as `yarn test`.
  6. If you need a debugger, run `yarn test --debug --watch TestName`, open `chrome://inspect`, and press "Inspect".
  7. Format your code with [prettier](https://github.com/prettier/prettier) (`yarn prettier`).
  8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

  Learn more about contributing: https://reactjs.org/docs/how-to-contribute.html
-->

## Summary

<!--
 Explain the **motivation** for making this change. What existing problem does the pull request solve?
-->

Added `<ViewTransition>` for when the "Show Internals" button is toggled for a basic fade transition. Additionally added a transition for when tabs are expanded in the advanced view of the Compiler Playground to display a smoother show/hide animation.  

## How did you test this change?

<!--
  Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes the user interface.
  How exactly did you verify that your PR solves the issue you wanted to solve?
  If you leave this empty, your PR will very likely be closed.
-->

https://github.com/user-attachments/assets/c706b337-289e-488d-8cd7-45ff1d27788d






Changed files:
filename: compiler/apps/playground/components/AccordionWindow.tsx
patch: @@ -6,7 +6,14 @@
  */
 
 import {Resizable} from 're-resizable';
-import React, {useCallback} from 'react';
+import React, {
+  useCallback,
+  useId,
+  unstable_ViewTransition as ViewTransition,
+  unstable_addTransitionType as addTransitionType,
+  startTransition,
+} from 'react';
+import {EXPAND_ACCORDION_TRANSITION} from '../lib/transitionTypes';
 
 type TabsRecord = Map<string, React.ReactNode>;
 
@@ -50,49 +57,69 @@ function AccordionWindowItem({
   setTabsOpen: (newTab: Set<string>) => void;
   hasChanged: boolean;
 }): React.ReactElement {
+  const id = useId();
   const isShow = tabsOpen.has(name);
 
-  const toggleTabs = useCallback(() => {
-    const nextState = new Set(tabsOpen);
-    if (nextState.has(name)) {
-      nextState.delete(name);
-    } else {
-      nextState.add(name);
-    }
-    setTabsOpen(nextState);
-  }, [tabsOpen, name, setTabsOpen]);
+  const transitionName = `accordion-window-item-${id}`;
+
+  const toggleTabs = () => {
+    startTransition(() => {
+      addTransitionType(EXPAND_ACCORDION_TRANSITION);
+      const nextState = new Set(tabsOpen);
+      if (nextState.has(name)) {
+        nextState.delete(name);
+      } else {
+        nextState.add(name);
+      }
+      setTabsOpen(nextState);
+    });
+  };
 
   // Replace spaces with non-breaking spaces
   const displayName = name.replace(/ /g, '\u00A0');
 
   return (
     <div key={name} className="flex flex-row">
       {isShow ? (
-        <Resizable className="border-r" minWidth={550} enable={{right: true}}>
-          <h2
-            title="Minimize tab"
-            aria-label="Minimize tab"
-            onClick={toggleTabs}
-            className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
-              hasChanged ? 'font-bold' : 'font-light'
-            } text-secondary hover:text-link`}>
-            - {displayName}
-          </h2>
-          {tabs.get(name) ?? <div>No output for {name}</div>}
-        </Resizable>
+        <ViewTransition
+          name={transitionName}
+          update={{
+            [EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
+            default: 'none',
+          }}>
+          <Resizable className="border-r" minWidth={550} enable={{right: true}}>
+            <h2
+              title="Minimize tab"
+              aria-label="Minimize tab"
+              onClick={toggleTabs}
+              className={`p-4 duration-150 ease-in border-b cursor-pointer border-grey-200 ${
+                hasChanged ? 'font-bold' : 'font-light'
+              } text-secondary hover:text-link`}>
+              - {displayName}
+            </h2>
+            {tabs.get(name) ?? <div>No output for {name}</div>}
+          </Resizable>
+        </ViewTransition>
       ) : (
-        <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
-          <button
-            title={`Expand compiler tab: ${name}`}
-            aria-label={`Expand compiler tab: ${name}`}
-            style={{transform: 'rotate(90deg) translate(-50%)'}}
-            onClick={toggleTabs}
-            className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
-              hasChanged ? 'font-bold' : 'font-light'
-            } text-secondary hover:text-link`}>
-            {displayName}
-          </button>
-        </div>
+        <ViewTransition
+          name={transitionName}
+          update={{
+            [EXPAND_ACCORDION_TRANSITION]: 'expand-accordion',
+            default: 'none',
+          }}>
+          <div className="relative items-center h-full px-1 py-6 align-middle border-r border-grey-200">
+            <button
+              title={`Expand compiler tab: ${name}`}
+              aria-label={`Expand compiler tab: ${name}`}
+              style={{transform: 'rotate(90deg) translate(-50%)'}}
+              onClick={toggleTabs}
+              className={`flex-grow-0 w-5 transition-colors duration-150 ease-in ${
+                hasChanged ? 'font-bold' : 'font-light'
+              } text-secondary hover:text-link`}>
+              {displayName}
+            </button>
+          </div>
+        </ViewTransition>
       )}
     </div>
   );

filename: compiler/apps/playground/components/Editor/Output.tsx
patch: @@ -32,7 +32,10 @@ import AccordionWindow from '../AccordionWindow';
 import TabbedWindow from '../TabbedWindow';
 import {monacoOptions} from './monacoOptions';
 import {BabelFileResult} from '@babel/core';
-import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
+import {
+  CONFIG_PANEL_TRANSITION,
+  TOGGLE_INTERNALS_TRANSITION,
+} from '../../lib/transitionTypes';
 import {LRUCache} from 'lru-cache';
 
 const MemoizedOutput = memo(Output);
@@ -291,6 +294,7 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
       <ViewTransition
         update={{
           [CONFIG_PANEL_TRANSITION]: 'container',
+          [TOGGLE_INTERNALS_TRANSITION]: '',
           default: 'none',
         }}>
         <TabbedWindow
@@ -306,6 +310,7 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
     <ViewTransition
       update={{
         [CONFIG_PANEL_TRANSITION]: 'accordion-container',
+        [TOGGLE_INTERNALS_TRANSITION]: '',
         default: 'none',
       }}>
       <AccordionWindow

filename: compiler/apps/playground/components/Header.tsx
patch: @@ -10,11 +10,16 @@ import {CheckIcon} from '@heroicons/react/solid';
 import clsx from 'clsx';
 import Link from 'next/link';
 import {useSnackbar} from 'notistack';
-import {useState} from 'react';
+import {
+  useState,
+  startTransition,
+  unstable_addTransitionType as addTransitionType,
+} from 'react';
 import {defaultStore} from '../lib/defaultStore';
 import {IconGitHub} from './Icons/IconGitHub';
 import Logo from './Logo';
 import {useStore, useStoreDispatch} from './StoreContext';
+import {TOGGLE_INTERNALS_TRANSITION} from '../lib/transitionTypes';
 
 export default function Header(): JSX.Element {
   const [showCheck, setShowCheck] = useState(false);
@@ -62,7 +67,12 @@ export default function Header(): JSX.Element {
             <input
               type="checkbox"
               checked={store.showInternals}
-              onChange={() => dispatchStore({type: 'toggleInternals'})}
+              onChange={() =>
+                startTransition(() => {
+                  addTransitionType(TOGGLE_INTERNALS_TRANSITION);
+                  dispatchStore({type: 'toggleInternals'});
+                })
+              }
               className="absolute opacity-0 cursor-pointer h-full w-full m-0"
             />
             <span

filename: compiler/apps/playground/lib/transitionTypes.ts
patch: @@ -7,3 +7,5 @@
 
 export const CONFIG_PANEL_TRANSITION = 'config-panel';
 export const TOGGLE_TAB_TRANSITION = 'toggle-tab';
+export const TOGGLE_INTERNALS_TRANSITION = 'toggle-internals';
+export const EXPAND_ACCORDION_TRANSITION = 'open-accordion';

filename: compiler/apps/playground/styles/globals.css
patch: @@ -116,3 +116,11 @@
 ::view-transition-group(.tab-text) {
 	z-index: 1;
 }
+
+::view-transition-old(.expand-accordion),
+::view-transition-new(.expand-accordion) {
+  width: auto;
+}
+::view-transition-group(.expand-accordion) {
+  overflow: clip;
+}