@samitouri / QOS-React / commits / a7d8dddaf3

[DevTools] Add Settings button on Suspense Tab (#34624)

The settings dialog appears on all tabs and should be reachable from Suspense tab too. It's a bit weird because it's not contextual to the tab and it shows you whatever your last settings tab was opened. Maybe it should default to opening to the current tab's settings? There aren't any Suspense specific settings yet but there definitely will be. We could move the "Show all" into settings but it might be frequently that you want to check why something isn't suspending a Suspense boundary or test SSR streaming. However, the general settings still apply to the Suspense tab. E.g. switching dark/light mode. <img width="857" height="233" alt="Screenshot 2025-09-27 at 12 35 05 PM" src="https://github.com/user-attachments/assets/4a38e94f-2074-4dce-906b-9a1c40bccb9b" />

Sebastian Markbåge committed Sep 28, 2025 at 19:09 UTC a7d8dddaf321c581a4128897123127486163eb8c
2 files changed +85 -66
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css
+10 -2
@@ -14,6 +14,14 @@
14 -webkit-font-smoothing: var(--font-smoothing);
15 }
16
17 +.VRule {
18 + height: 20px;
19 + width: 1px;
20 + flex: 0 0 1px;
21 + margin: 0 0.5rem;
22 + background-color: var(--color-border);
23 +}
24 +
25 .TreeWrapper {
26 border-top: 1px solid var(--color-border);
27 flex: 1 1 65%;
@@ -107,13 +115,13 @@
115 .SuspenseTreeViewHeader {
116 flex: 0 0 42px;
117 padding: 0.5rem;
110 - display: grid;
111 - grid-template-columns: auto 1fr auto auto auto;
118 + display: flex;
119 align-items: flex-start;
120 border-bottom: 1px solid var(--color-border);
121 }
122
123 .SuspenseBreadcrumbs {
124 + flex: 1;
125 /**
126 * TODO: Switch to single item view on overflow like OwnerStack does.
127 * OwnerStack has more constraints that make it easier so it won't be a 1:1 port.
packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js
+75 -64
@@ -33,7 +33,7 @@ import {
33 SuspenseTreeDispatcherContext,
34 SuspenseTreeStateContext,
35 } from './SuspenseTreeContext';
36 -import {StoreContext} from '../context';
36 +import {StoreContext, OptionsContext} from '../context';
37 import {TreeDispatcherContext} from '../Components/TreeContext';
38 import Button from '../Button';
39 import Tooltip from '../Components/reach-ui/tooltip';
@@ -41,6 +41,9 @@ import typeof {
41 SyntheticEvent,
42 SyntheticPointerEvent,
43 } from 'react-dom-bindings/src/events/SyntheticEvent';
44 +import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
45 +import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
46 +import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
47
48 type Orientation = 'horizontal' | 'vertical';
49
@@ -212,6 +215,7 @@ function ToggleInspectedElement({
215 }
216
217 function SuspenseTab(_: {}) {
218 + const {hideSettings} = useContext(OptionsContext);
219 const [state, dispatch] = useReducer<LayoutState, null, LayoutAction>(
220 layoutReducer,
221 null,
@@ -394,75 +398,82 @@ function SuspenseTab(_: {}) {
398 };
399
400 return (
397 - <div className={styles.SuspenseTab} ref={wrapperTreeRef}>
398 - <div className={styles.TreeWrapper} ref={resizeTreeRef}>
399 - {treeListDisabled ? null : (
400 - <div
401 - className={styles.TreeList}
402 - hidden={treeListHidden}
403 - ref={resizeTreeListRef}>
404 - <SuspenseTreeList />
405 - </div>
406 - )}
407 - {treeListDisabled ? null : (
408 - <div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
401 + <SettingsModalContextController>
402 + <div className={styles.SuspenseTab} ref={wrapperTreeRef}>
403 + <div className={styles.TreeWrapper} ref={resizeTreeRef}>
404 + {treeListDisabled ? null : (
405 <div
410 - onPointerDown={onResizeStart}
411 - onPointerMove={onResizeTreeList}
412 - onPointerUp={onResizeEnd}
413 - className={styles.ResizeBar}
414 - />
415 - </div>
416 - )}
417 - <div className={styles.TreeView}>
418 - <header className={styles.SuspenseTreeViewHeader}>
419 - {treeListDisabled ? (
420 - <div />
421 - ) : (
422 - <ToggleTreeList dispatch={dispatch} state={state} />
423 - )}
424 - <div className={styles.SuspenseBreadcrumbs}>
425 - <SuspenseBreadcrumbs />
406 + className={styles.TreeList}
407 + hidden={treeListHidden}
408 + ref={resizeTreeListRef}>
409 + <SuspenseTreeList />
410 </div>
427 - <ToggleUniqueSuspenders />
428 - <SelectRoot />
429 - <ToggleInspectedElement
430 - dispatch={dispatch}
431 - state={state}
432 - orientation="horizontal"
433 - />
434 - </header>
435 - <div className={styles.Rects}>
436 - <SuspenseRects />
437 - </div>
438 - <footer className={styles.SuspenseTreeViewFooter}>
439 - <div className={styles.SuspenseTimeline}>
440 - <SuspenseTimeline />
411 + )}
412 + {treeListDisabled ? null : (
413 + <div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
414 + <div
415 + onPointerDown={onResizeStart}
416 + onPointerMove={onResizeTreeList}
417 + onPointerUp={onResizeEnd}
418 + className={styles.ResizeBar}
419 + />
420 + </div>
421 + )}
422 + <div className={styles.TreeView}>
423 + <header className={styles.SuspenseTreeViewHeader}>
424 + {treeListDisabled ? (
425 + <div />
426 + ) : (
427 + <ToggleTreeList dispatch={dispatch} state={state} />
428 + )}
429 + <div className={styles.SuspenseBreadcrumbs}>
430 + <SuspenseBreadcrumbs />
431 + </div>
432 + <ToggleUniqueSuspenders />
433 + <SelectRoot />
434 + {!hideSettings && <div className={styles.VRule} />}
435 + {!hideSettings && <SettingsModalContextToggle />}
436 + <ToggleInspectedElement
437 + dispatch={dispatch}
438 + state={state}
439 + orientation="horizontal"
440 + />
441 + </header>
442 + <div className={styles.Rects}>
443 + <SuspenseRects />
444 </div>
442 - <ToggleInspectedElement
443 - dispatch={dispatch}
444 - state={state}
445 - orientation="vertical"
446 - />
447 - </footer>
445 + <footer className={styles.SuspenseTreeViewFooter}>
446 + <div className={styles.SuspenseTimeline}>
447 + <SuspenseTimeline />
448 + </div>
449 + <ToggleInspectedElement
450 + dispatch={dispatch}
451 + state={state}
452 + orientation="vertical"
453 + />
454 + </footer>
455 + </div>
456 </div>
449 - </div>
450 - <div className={styles.ResizeBarWrapper} hidden={inspectedElementHidden}>
457 <div
452 - onPointerDown={onResizeStart}
453 - onPointerMove={onResizeTree}
454 - onPointerUp={onResizeEnd}
455 - className={styles.ResizeBar}
456 - />
457 - </div>
458 - <div
459 - className={styles.InspectedElementWrapper}
460 - hidden={inspectedElementHidden}>
461 - <InspectedElementErrorBoundary>
462 - <InspectedElement />
463 - </InspectedElementErrorBoundary>
458 + className={styles.ResizeBarWrapper}
459 + hidden={inspectedElementHidden}>
460 + <div
461 + onPointerDown={onResizeStart}
462 + onPointerMove={onResizeTree}
463 + onPointerUp={onResizeEnd}
464 + className={styles.ResizeBar}
465 + />
466 + </div>
467 + <div
468 + className={styles.InspectedElementWrapper}
469 + hidden={inspectedElementHidden}>
470 + <InspectedElementErrorBoundary>
471 + <InspectedElement />
472 + </InspectedElementErrorBoundary>
473 + </div>
474 + <SettingsModal />
475 </div>
465 - </div>
476 + </SettingsModalContextController>
477 );
478 }
479