Repository: facebook/react
Pull request: [playground] Persist open tabs on compiler error

This change allows it so that tabs that were open before a compiler error are automatically opened again when the error is resolved. Quality of life change for those especially working with the advanced view of the playground. 

https://github.com/user-attachments/assets/cd2dc117-e6fc-4f57-a08f-259757c4f5e8



Changed files:
filename: compiler/apps/playground/components/AccordionWindow.tsx
patch: @@ -23,6 +23,7 @@ export default function AccordionWindow(props: {
   tabsOpen: Set<string>;
   setTabsOpen: (newTab: Set<string>) => void;
   changedPasses: Set<string>;
+  isFailure: boolean;
 }): React.ReactElement {
   return (
     <div className="flex-1 min-w-[550px] sm:min-w-0">
@@ -36,6 +37,7 @@ export default function AccordionWindow(props: {
               tabsOpen={props.tabsOpen}
               setTabsOpen={props.setTabsOpen}
               hasChanged={props.changedPasses.has(name)}
+              isFailure={props.isFailure}
             />
           );
         })}
@@ -50,15 +52,17 @@ function AccordionWindowItem({
   tabsOpen,
   setTabsOpen,
   hasChanged,
+  isFailure,
 }: {
   name: string;
   tabs: TabsRecord;
   tabsOpen: Set<string>;
   setTabsOpen: (newTab: Set<string>) => void;
   hasChanged: boolean;
+  isFailure: boolean;
 }): React.ReactElement {
   const id = useId();
-  const isShow = tabsOpen.has(name);
+  const isShow = isFailure ? name === 'Output' : tabsOpen.has(name);
 
   const transitionName = `accordion-window-item-${id}`;
 

filename: compiler/apps/playground/components/Editor/Output.tsx
patch: @@ -265,14 +265,8 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
    * Update the active tab back to the output or errors tab when the compilation state
    * changes between success/failure.
    */
-  const [previousOutputKind, setPreviousOutputKind] = useState(
-    compilerOutput.kind,
-  );
-  if (compilerOutput.kind !== previousOutputKind) {
-    setPreviousOutputKind(compilerOutput.kind);
-    setTabsOpen(new Set(['Output']));
-    setActiveTab('Output');
-  }
+
+  const isFailure = compilerOutput.kind !== 'ok';
   const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
   let lastResult: string = '';
   for (const [passName, results] of compilerOutput.results) {
@@ -301,6 +295,8 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
           tabs={tabs}
           activeTab={activeTab}
           onTabChange={setActiveTab}
+          // Display the Output tab on compilation failure
+          activeTabOverride={isFailure ? 'Output' : undefined}
         />
       </ViewTransition>
     );
@@ -319,6 +315,7 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element {
         tabsOpen={tabsOpen}
         tabs={tabs}
         changedPasses={changedPasses}
+        isFailure={isFailure}
       />
     </ViewTransition>
   );

filename: compiler/apps/playground/components/TabbedWindow.tsx
patch: @@ -17,11 +17,15 @@ export default function TabbedWindow({
   tabs,
   activeTab,
   onTabChange,
+  activeTabOverride,
 }: {
   tabs: Map<string, React.ReactNode>;
   activeTab: string;
   onTabChange: (tab: string) => void;
+  activeTabOverride?: string;
 }): React.ReactElement {
+  const currentActiveTab = activeTabOverride ? activeTabOverride : activeTab;
+
   const id = useId();
   const transitionName = `tab-highlight-${id}`;
 
@@ -37,7 +41,7 @@ export default function TabbedWindow({
       <div className="flex flex-col h-full max-w-full">
         <div className="flex p-2 flex-shrink-0">
           {Array.from(tabs.keys()).map(tab => {
-            const isActive = activeTab === tab;
+            const isActive = currentActiveTab === tab;
             return (
               <button
                 key={tab}
@@ -49,6 +53,8 @@ export default function TabbedWindow({
                 {isActive && (
                   <ViewTransition
                     name={transitionName}
+                    enter={{default: 'none'}}
+                    exit={{default: 'none'}}
                     share={{
                       [TOGGLE_TAB_TRANSITION]: 'tab-highlight',
                       default: 'none',
@@ -58,6 +64,8 @@ export default function TabbedWindow({
                   </ViewTransition>
                 )}
                 <ViewTransition
+                  enter={{default: 'none'}}
+                  exit={{default: 'none'}}
                   update={{
                     [TOGGLE_TAB_TRANSITION]: 'tab-text',
                     default: 'none',
@@ -69,7 +77,7 @@ export default function TabbedWindow({
           })}
         </div>
         <div className="flex-1 overflow-hidden w-full h-full">
-          {tabs.get(activeTab)}
+          {tabs.get(currentActiveTab)}
         </div>
       </div>
     </div>