From 1a6bfdecab34564cfe34a158f66b055c8d1c823f Mon Sep 17 00:00:00 2001 From: Ivn Nv Date: Sun, 5 Apr 2026 19:15:42 +0300 Subject: [PATCH 1/3] [DevTools] Add settings to hide Profiler and Suspense tabs --- .../src/main/index.js | 10 +++ .../react-devtools-shared/src/constants.js | 4 ++ .../src/devtools/views/DevTools.js | 68 ++++++++++++++++--- .../views/Settings/GeneralSettings.js | 22 ++++++ .../views/Settings/ProfilerSettings.js | 21 ++++++ .../views/Settings/SettingsContext.js | 20 ++++++ .../devtools/views/Settings/SettingsModal.js | 4 +- .../views/Settings/SettingsShared.css | 6 ++ 8 files changed, 142 insertions(+), 13 deletions(-) diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 0072b6934585..8d9501cbc26b 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -22,6 +22,8 @@ import { } from 'react-devtools-shared/src/storage'; import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; import { + LOCAL_STORAGE_HIDE_PROFILER_TAB_KEY, + LOCAL_STORAGE_HIDE_SUSPENSE_TAB_KEY, LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, } from 'react-devtools-shared/src/constants'; @@ -351,6 +353,10 @@ function createProfilerPanel() { return; } + if (localStorageGetItem(LOCAL_STORAGE_HIDE_PROFILER_TAB_KEY) === 'true') { + return; + } + chrome.devtools.panels.create( __IS_CHROME__ || __IS_EDGE__ ? 'Profiler ⚛' : 'Profiler', __IS_EDGE__ ? 'icons/production.svg' : '', @@ -427,6 +433,10 @@ function createSuspensePanel() { return; } + if (localStorageGetItem(LOCAL_STORAGE_HIDE_SUSPENSE_TAB_KEY) === 'true') { + return; + } + chrome.devtools.panels.create( __IS_CHROME__ || __IS_EDGE__ ? 'Suspense ⚛' : 'Suspense', __IS_EDGE__ ? 'icons/production.svg' : '', diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 53488cc2f454..6ceac409f424 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -66,6 +66,10 @@ export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY = 'React::DevTools::traceUpdatesEnabled'; export const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY = 'React::DevTools::supportsProfiling'; +export const LOCAL_STORAGE_HIDE_PROFILER_TAB_KEY = + 'React::DevTools::hideProfilerTab'; +export const LOCAL_STORAGE_HIDE_SUSPENSE_TAB_KEY = + 'React::DevTools::hideSuspenseTab'; export const PROFILER_EXPORT_VERSION = 5; diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index bf541f667287..3e488a519836 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -13,7 +13,14 @@ import '@reach/menu-button/styles.css'; import '@reach/tooltip/styles.css'; import * as React from 'react'; -import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'; +import { + useCallback, + useContext, + useEffect, + useLayoutEffect, + useMemo, + useRef, +} from 'react'; import Store from '../store'; import { BridgeContext, @@ -27,7 +34,10 @@ import SuspenseTab from './SuspenseTab/SuspenseTab'; import TabBar from './TabBar'; import EditorPane from './Editor/EditorPane'; import InspectedElementPane from './InspectedElement/InspectedElementPane'; -import {SettingsContextController} from './Settings/SettingsContext'; +import { + SettingsContext, + SettingsContextController, +} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext'; import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; @@ -135,7 +145,46 @@ const suspenseTab = { title: 'React Suspense', }; -const tabs = [componentsTab, profilerTab, suspenseTab]; +const allTabs = [componentsTab, profilerTab, suspenseTab]; + +function DevToolsNavigationTabBar({ + currentTab, + selectTab, +}: { + currentTab: TabID, + selectTab: (tabId: TabID) => void, +}): React.Node { + const {hideProfilerTab, hideSuspenseTab} = useContext(SettingsContext); + const visibleTabs = useMemo( + () => + allTabs.filter( + t => + !(hideProfilerTab && t.id === 'profiler') && + !(hideSuspenseTab && t.id === 'suspense'), + ), + [hideProfilerTab, hideSuspenseTab], + ); + + // If the active tab is hidden, switch to components + useEffect(() => { + if ( + (hideProfilerTab && currentTab === 'profiler') || + (hideSuspenseTab && currentTab === 'suspense') + ) { + selectTab('components'); + } + }, [hideProfilerTab, hideSuspenseTab, currentTab, selectTab]); + + return ( + + ); +} export default function DevTools({ bridge, @@ -248,18 +297,18 @@ export default function DevTools({ if (event.ctrlKey || event.metaKey) { switch (event.key) { case '1': - selectTab(tabs[0].id); + selectTab(allTabs[0].id); event.preventDefault(); event.stopPropagation(); break; case '2': - selectTab(tabs[1].id); + selectTab(allTabs[1].id); event.preventDefault(); event.stopPropagation(); break; case '3': - if (tabs.length > 2) { - selectTab(tabs[2].id); + if (allTabs.length > 2) { + selectTab(allTabs[2].id); event.preventDefault(); event.stopPropagation(); } @@ -321,12 +370,9 @@ export default function DevTools({ {process.env.DEVTOOLS_VERSION}
-
)} diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js index d8f50a9cdc7d..a5ff3c80e92e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js @@ -37,7 +37,9 @@ function getChangeLogUrl(version: ?string): string | null { export default function GeneralSettings(_: {}): React.Node { const { displayDensity, + hideSuspenseTab, setDisplayDensity, + setHideSuspenseTab, setTheme, setTraceUpdatesEnabled, theme, @@ -120,6 +122,26 @@ export default function GeneralSettings(_: {}): React.Node { )} +
+ + {hideSuspenseTab && ( +
+ The Suspense tab will be hidden when DevTools is opened in a new + tab, or when DevTools is reopened in the current tab. +
+ )} +
+
{showBackendVersion && (
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js index 75bb97d902e2..90b257accddb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js @@ -11,6 +11,7 @@ import * as React from 'react'; import {useCallback, useContext, useMemo, useRef} from 'react'; import {useSubscription} from '../hooks'; import {StoreContext} from '../context'; +import {SettingsContext} from './SettingsContext'; import {ProfilerContext} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext'; import styles from './SettingsShared.css'; @@ -23,6 +24,7 @@ export default function ProfilerSettings(_: {}): React.Node { setIsCommitFilterEnabled, setMinCommitDuration, } = useContext(ProfilerContext); + const {hideProfilerTab, setHideProfilerTab} = useContext(SettingsContext); const store = useContext(StoreContext); const recordChangeDescriptionsSubscription = useMemo( @@ -102,6 +104,25 @@ export default function ProfilerSettings(_: {}): React.Node {  (ms)
+
+ + {hideProfilerTab && ( +
+ The Profiler tab will be hidden when DevTools is opened in a new + tab, or when DevTools is reopened in the current tab. +
+ )} +
); } diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index c20249e89942..51fc1110543b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -19,6 +19,8 @@ import { } from 'react'; import { LOCAL_STORAGE_BROWSER_THEME, + LOCAL_STORAGE_HIDE_PROFILER_TAB_KEY, + LOCAL_STORAGE_HIDE_SUSPENSE_TAB_KEY, LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY, LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, } from 'react-devtools-shared/src/constants'; @@ -53,6 +55,12 @@ type Context = { traceUpdatesEnabled: boolean, setTraceUpdatesEnabled: (value: boolean) => void, + + hideProfilerTab: boolean, + setHideProfilerTab: (value: boolean) => void, + + hideSuspenseTab: boolean, + setHideSuspenseTab: (value: boolean) => void, }; const SettingsContext: ReactContext = createContext( @@ -113,6 +121,10 @@ function SettingsContextController({ LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, false, ); + const [hideProfilerTab, setHideProfilerTab] = + useLocalStorageWithLog(LOCAL_STORAGE_HIDE_PROFILER_TAB_KEY, false); + const [hideSuspenseTab, setHideSuspenseTab] = + useLocalStorageWithLog(LOCAL_STORAGE_HIDE_SUSPENSE_TAB_KEY, false); const documentElements = useMemo(() => { const array: Array = [ @@ -183,8 +195,12 @@ function SettingsContextController({ displayDensity === 'compact' ? COMPACT_LINE_HEIGHT : COMFORTABLE_LINE_HEIGHT, + hideProfilerTab, + hideSuspenseTab, parseHookNames, setDisplayDensity, + setHideProfilerTab, + setHideSuspenseTab, setParseHookNames, setTheme, setTraceUpdatesEnabled, @@ -194,8 +210,12 @@ function SettingsContextController({ }), [ displayDensity, + hideProfilerTab, + hideSuspenseTab, parseHookNames, setDisplayDensity, + setHideProfilerTab, + setHideSuspenseTab, setParseHookNames, setTheme, setTraceUpdatesEnabled, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js index 023b342a0cb9..dc516293a7de 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js @@ -109,7 +109,7 @@ function SettingsModalImpl({store}: ImplProps) { currentTab={selectedTabID} id="Settings" selectTab={selectTab} - tabs={tabs} + tabs={allTabs} type="settings" />
@@ -123,7 +123,7 @@ function SettingsModalImpl({store}: ImplProps) { ); } -const tabs = [ +const allTabs = [ { id: 'general', icon: 'settings', diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css index 71c533278884..8d4ac0bbbaec 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css @@ -73,6 +73,12 @@ margin-right: 0.25rem; } +.SettingHint { + font-size: var(--font-size-sans-small); + color: var(--color-dim); + margin-top: 0.25rem; +} + .NoFiltersCell { padding: 0.25rem 0; color: var(--color-dim); From a7b2113d3df12323d41c33ec524d7ce4e9311de4 Mon Sep 17 00:00:00 2001 From: Ivn Nv Date: Mon, 6 Apr 2026 10:19:54 +0300 Subject: [PATCH 2/3] Fix keyboard shortcuts navigating to hidden tabs --- .../src/devtools/views/DevTools.js | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 3e488a519836..afe2e65add8c 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -295,24 +295,11 @@ export default function DevTools({ const ownerWindow = div.ownerDocument.defaultView; const handleKeyDown = (event: KeyboardEvent) => { if (event.ctrlKey || event.metaKey) { - switch (event.key) { - case '1': - selectTab(allTabs[0].id); - event.preventDefault(); - event.stopPropagation(); - break; - case '2': - selectTab(allTabs[1].id); - event.preventDefault(); - event.stopPropagation(); - break; - case '3': - if (allTabs.length > 2) { - selectTab(allTabs[2].id); - event.preventDefault(); - event.stopPropagation(); - } - break; + const tabIndex = parseInt(event.key, 10) - 1; + if (tabIndex >= 0 && tabIndex < visibleTabs.length) { + selectTab(visibleTabs[tabIndex].id); + event.preventDefault(); + event.stopPropagation(); } } }; @@ -320,7 +307,7 @@ export default function DevTools({ return () => { ownerWindow.removeEventListener('keydown', handleKeyDown); }; - }, [showTabBar]); + }, [showTabBar, visibleTabs]); useLayoutEffect(() => { return () => { From 99ce39604eac6bc6a0e6c468a88e90c6f2de266e Mon Sep 17 00:00:00 2001 From: Ivn Nv Date: Tue, 7 Apr 2026 13:49:50 +0300 Subject: [PATCH 3/3] Fix keyboard shortcuts navigating to hidden tabs --- .../src/devtools/views/DevTools.js | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index afe2e65add8c..d677a4ffb46f 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -175,6 +175,24 @@ function DevToolsNavigationTabBar({ } }, [hideProfilerTab, hideSuspenseTab, currentTab, selectTab]); + // Override keyboard shortcuts to use visible tabs only + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey || event.metaKey) { + const tabIndex = parseInt(event.key, 10) - 1; + if (tabIndex >= 0 && tabIndex < visibleTabs.length) { + selectTab(visibleTabs[tabIndex].id); + event.preventDefault(); + event.stopPropagation(); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [visibleTabs, selectTab]); + return ( (null); - useEffect(() => { - if (!showTabBar) { - return; - } - - const div = devToolsRef.current; - if (div === null) { - return; - } - - const ownerWindow = div.ownerDocument.defaultView; - const handleKeyDown = (event: KeyboardEvent) => { - if (event.ctrlKey || event.metaKey) { - const tabIndex = parseInt(event.key, 10) - 1; - if (tabIndex >= 0 && tabIndex < visibleTabs.length) { - selectTab(visibleTabs[tabIndex].id); - event.preventDefault(); - event.stopPropagation(); - } - } - }; - ownerWindow.addEventListener('keydown', handleKeyDown); - return () => { - ownerWindow.removeEventListener('keydown', handleKeyDown); - }; - }, [showTabBar, visibleTabs]); + // Keyboard shortcuts (Ctrl/Cmd+1/2/3) are handled in + // DevToolsNavigationTabBar to operate on visible tabs only. useLayoutEffect(() => { return () => {