diff --git a/packages/react-devtools-shared/src/__tests__/profilerContext-test.js b/packages/react-devtools-shared/src/__tests__/profilerContext-test.js index 9a9bb14c0ba0c..2cd2340fb7acd 100644 --- a/packages/react-devtools-shared/src/__tests__/profilerContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilerContext-test.js @@ -584,4 +584,75 @@ describe('ProfilerContext', () => { await utils.actAsync(() => context.selectFiber(childID, 'Child')); expect(inspectedElementID).toBe(parentID); }); + + it('should toggle profiling when the keyboard shortcut is pressed', async () => { + // Context providers + const Profiler = + require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default; + const { + TimelineContextController, + } = require('react-devtools-timeline/src/TimelineContext'); + const { + SettingsContextController, + } = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext'); + const { + ModalDialogContextController, + } = require('react-devtools-shared/src/devtools/views/ModalDialog'); + + // Dom component for profiling to be enabled + const Component = () => null; + utils.act(() => render()); + + const profilerContainer = document.createElement('div'); + document.body.appendChild(profilerContainer); + + // Create a root for the profiler + const profilerRoot = ReactDOMClient.createRoot(profilerContainer); + + // Render the profiler + utils.act(() => { + profilerRoot.render( + + + + + + + + + , + ); + }); + + // Verify that the profiler is not profiling. + expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false); + + // Trigger the keyboard shortcut. + const ownerWindow = profilerContainer.ownerDocument.defaultView; + const isMac = + typeof navigator !== 'undefined' && + navigator.platform.toUpperCase().indexOf('MAC') >= 0; + + const keyEvent = new KeyboardEvent('keydown', { + key: 'e', + metaKey: isMac, + ctrlKey: !isMac, + bubbles: true, + }); + + // Dispatch keyboard event to toggle profiling on + // Try utils.actAsync with recursivelyFlush=false + await utils.actAsync(() => { + ownerWindow.dispatchEvent(keyEvent); + }, false); + expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(true); + + // Dispatch keyboard event to toggle profiling off + await utils.actAsync(() => { + ownerWindow.dispatchEvent(keyEvent); + }, false); + expect(store.profilerStore.isProfilingBasedOnUserInput).toBe(false); + + document.body.removeChild(profilerContainer); + }); }); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 4ac55b46f0e8c..5e330637fd910 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -8,7 +8,7 @@ */ import * as React from 'react'; -import {Fragment, useContext} from 'react'; +import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react'; import {ModalDialog} from '../ModalDialog'; import {ProfilerContext} from './ProfilerContext'; import TabBar from '../TabBar'; @@ -38,6 +38,11 @@ import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext'; import styles from './Profiler.css'; function Profiler(_: {}) { + const profilerRef = useRef(null); + const isMac = + typeof navigator !== 'undefined' && + navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const { didRecordCommits, isProcessingData, @@ -47,6 +52,8 @@ function Profiler(_: {}) { selectedTabID, selectTab, supportsProfiling, + startProfiling, + stopProfiling, } = useContext(ProfilerContext); const {file: timelineTraceEventData, searchInputContainerRef} = @@ -56,6 +63,32 @@ function Profiler(_: {}) { const isLegacyProfilerSelected = selectedTabID !== 'timeline'; + // Cmd+E to start/stop profiler recording + const handleKeyDown = useEffectEvent((event: KeyboardEvent) => { + const correctModifier = isMac ? event.metaKey : event.ctrlKey; + if (correctModifier && event.key === 'e') { + if (isProfiling) { + stopProfiling(); + } else { + startProfiling(); + } + event.preventDefault(); + event.stopPropagation(); + } + }); + + useEffect(() => { + const div = profilerRef.current; + if (!div) { + return; + } + const ownerWindow = div.ownerDocument.defaultView; + ownerWindow.addEventListener('keydown', handleKeyDown); + return () => { + ownerWindow.removeEventListener('keydown', handleKeyDown); + }; + }, []); + let view = null; if (didRecordCommits || selectedTabID === 'timeline') { switch (selectedTabID) { @@ -112,7 +145,7 @@ function Profiler(_: {}) { return ( -
+
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js b/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js index 7e79df9e08146..6aa48a3cefa34 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js @@ -30,13 +30,19 @@ export default function RecordToggle({disabled}: Props): React.Node { className = styles.ActiveRecordToggle; } + const isMac = + typeof navigator !== 'undefined' && + navigator.platform.toUpperCase().indexOf('MAC') >= 0; + const shortcut = isMac ? '⌘E' : 'Ctrl+E'; + const title = `${isProfiling ? 'Stop' : 'Start'} profiling - ${shortcut}`; + return ( );