8
*/
9
10
import type {Interaction, Point} from './view-base';
11
-import type {
12
- ReactEventInfo,
13
- TimelineData,
14
- ReactMeasure,
15
- ViewState,
16
-} from './types';
11
+import type {ReactEventInfo, TimelineData, ViewState} from './types';
12
13
import * as React from 'react';
14
import {
21
useCallback,
22
} from 'react';
23
import AutoSizer from 'react-virtualized-auto-sizer';
29
-import {copy} from 'clipboard-js';
30
-import prettyMilliseconds from 'pretty-ms';
24
25
import {
26
HorizontalPanAndZoomView,
49
import {COLORS} from './content-views/constants';
50
import {clampState, moveStateToRange} from './view-base/utils/scrollState';
51
import EventTooltip from './EventTooltip';
59
-import {RegistryContext} from 'react-devtools-shared/src/devtools/ContextMenu/Contexts';
60
-import ContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenu';
61
-import ContextMenuItem from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem';
62
-import useContextMenu from 'react-devtools-shared/src/devtools/ContextMenu/useContextMenu';
63
-import {getBatchRange} from './utils/getBatchRange';
52
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
53
import {TimelineSearchContext} from './TimelineSearchContext';
54
import {TimelineContext} from './TimelineContext';
55
+import CanvasPageContextMenu from './CanvasPageContextMenu';
56
68
-import styles from './CanvasPage.css';
57
+import type {ContextMenuRef} from 'react-devtools-shared/src/devtools/ContextMenu/types';
58
70
-const CONTEXT_MENU_ID = 'canvas';
59
+import styles from './CanvasPage.css';
60
61
type Props = {
62
profilerData: TimelineData,
82
);
83
}
84
96
-const copySummary = (data: TimelineData, measure: ReactMeasure) => {
97
- const {batchUID, duration, timestamp, type} = measure;
98
-
99
- const [startTime, stopTime] = getBatchRange(batchUID, data);
100
-
101
- copy(
102
- JSON.stringify({
103
- type,
104
- timestamp: prettyMilliseconds(timestamp),
105
- duration: prettyMilliseconds(duration),
106
- batchDuration: prettyMilliseconds(stopTime - startTime),
107
- }),
108
- );
109
-};
110
-
111
-const zoomToBatch = (
112
- data: TimelineData,
113
- measure: ReactMeasure,
114
- viewState: ViewState,
115
- width: number,
116
-) => {
117
- const {batchUID} = measure;
118
- const [rangeStart, rangeEnd] = getBatchRange(batchUID, data);
119
-
120
- // Convert from time range to ScrollState
121
- const scrollState = moveStateToRange({
122
- state: viewState.horizontalScrollState,
123
- rangeStart,
124
- rangeEnd,
125
- contentLength: data.duration,
126
-
127
- minContentLength: data.duration * MIN_ZOOM_LEVEL,
128
- maxContentLength: data.duration * MAX_ZOOM_LEVEL,
129
- containerLength: width,
130
- });
131
-
132
- viewState.updateHorizontalScrollState(scrollState);
133
-};
134
-
85
const EMPTY_CONTEXT_INFO: ReactEventInfo = {
86
componentMeasure: null,
87
flamechartStackFrame: null,
110
}: AutoSizedCanvasProps) {
111
const canvasRef = useRef<HTMLCanvasElement | null>(null);
112
163
- const [isContextMenuShown, setIsContextMenuShown] = useState<boolean>(false);
113
const [mouseLocation, setMouseLocation] = useState<Point>(zeroPoint); // DOM coordinates
114
const [hoveredEvent, setHoveredEvent] = useState<ReactEventInfo | null>(null);
115
+ const [lastHoveredEvent, setLastHoveredEvent] =
116
+ useState<ReactEventInfo | null>(null);
117
+
118
+ const contextMenuRef: ContextMenuRef = useRef(null);
119
120
const resetHoveredEvent = useCallback(
121
() => setHoveredEvent(EMPTY_CONTEXT_INFO),
122
[],
123
);
124
+ const updateHoveredEvent = useCallback(
125
+ (event: ReactEventInfo) => {
126
+ setHoveredEvent(event);
127
+
128
+ // If menu is already open, don't update the hovered event data
129
+ // So the same set of menu items is preserved until the current context menu is closed
130
+ if (contextMenuRef.current?.isShown()) {
131
+ return;
132
+ }
133
+
134
+ const {
135
+ componentMeasure,
136
+ flamechartStackFrame,
137
+ measure,
138
+ networkMeasure,
139
+ schedulingEvent,
140
+ suspenseEvent,
141
+ } = event;
142
+
143
+ // We have to keep track of last non-empty hovered event, since this will be the input for context menu items
144
+ // We can't just pass hoveredEvent to ContextMenuContainer,
145
+ // since it will be reset each time user moves mouse away from event object on the canvas
146
+ if (
147
+ componentMeasure != null ||
148
+ flamechartStackFrame != null ||
149
+ measure != null ||
150
+ networkMeasure != null ||
151
+ schedulingEvent != null ||
152
+ suspenseEvent != null
153
+ ) {
154
+ setLastHoveredEvent(event);
155
+ }
156
+ },
157
+ [contextMenuRef],
158
+ );
159
160
const {searchIndex, searchRegExp, searchResults} = useContext(
161
TimelineSearchContext,
198
const snapshotsViewRef = useRef<null | SnapshotsView>(null);
199
const thrownErrorsViewRef = useRef<null | ThrownErrorsView>(null);
200
213
- const {hideMenu: hideContextMenu} = useContext(RegistryContext);
214
-
201
useLayoutEffect(() => {
202
const surface = surfaceRef.current;
203
const defaultFrame = {origin: zeroPoint, size: {width, height}};
204
205
// Auto hide context menu when panning.
206
viewState.onHorizontalScrollStateChange(scrollState => {
221
- hideContextMenu();
207
+ contextMenuRef.current?.hide();
208
});
209
210
// Initialize horizontal view state
502
503
useCanvasInteraction(canvasRef, interactor);
504
519
- useContextMenu({
520
- data: {
521
- data,
522
- hoveredEvent,
523
- },
524
- id: CONTEXT_MENU_ID,
525
- onChange: setIsContextMenuShown,
526
- ref: canvasRef,
527
- });
528
-
505
const {selectEvent} = useContext(TimelineContext);
506
507
useEffect(() => {
509
if (userTimingMarksView) {
510
userTimingMarksView.onHover = userTimingMark => {
511
if (!hoveredEvent || hoveredEvent.userTimingMark !== userTimingMark) {
536
- setHoveredEvent({
512
+ updateHoveredEvent({
513
...EMPTY_CONTEXT_INFO,
514
userTimingMark,
515
});
521
if (nativeEventsView) {
522
nativeEventsView.onHover = nativeEvent => {
523
if (!hoveredEvent || hoveredEvent.nativeEvent !== nativeEvent) {
548
- setHoveredEvent({
524
+ updateHoveredEvent({
525
...EMPTY_CONTEXT_INFO,
526
nativeEvent,
527
});
533
if (schedulingEventsView) {
534
schedulingEventsView.onHover = schedulingEvent => {
535
if (!hoveredEvent || hoveredEvent.schedulingEvent !== schedulingEvent) {
560
- setHoveredEvent({
536
+ updateHoveredEvent({
537
...EMPTY_CONTEXT_INFO,
538
schedulingEvent,
539
});
551
if (suspenseEventsView) {
552
suspenseEventsView.onHover = suspenseEvent => {
553
if (!hoveredEvent || hoveredEvent.suspenseEvent !== suspenseEvent) {
578
- setHoveredEvent({
554
+ updateHoveredEvent({
555
...EMPTY_CONTEXT_INFO,
556
suspenseEvent,
557
});
563
if (reactMeasuresView) {
564
reactMeasuresView.onHover = measure => {
565
if (!hoveredEvent || hoveredEvent.measure !== measure) {
590
- setHoveredEvent({
566
+ updateHoveredEvent({
567
...EMPTY_CONTEXT_INFO,
568
measure,
569
});
578
!hoveredEvent ||
579
hoveredEvent.componentMeasure !== componentMeasure
580
) {
605
- setHoveredEvent({
581
+ updateHoveredEvent({
582
...EMPTY_CONTEXT_INFO,
583
componentMeasure,
584
});
590
if (snapshotsView) {
591
snapshotsView.onHover = snapshot => {
592
if (!hoveredEvent || hoveredEvent.snapshot !== snapshot) {
617
- setHoveredEvent({
593
+ updateHoveredEvent({
594
...EMPTY_CONTEXT_INFO,
595
snapshot,
596
});
605
!hoveredEvent ||
606
hoveredEvent.flamechartStackFrame !== flamechartStackFrame
607
) {
632
- setHoveredEvent({
608
+ updateHoveredEvent({
609
...EMPTY_CONTEXT_INFO,
610
flamechartStackFrame,
611
});
617
if (networkMeasuresView) {
618
networkMeasuresView.onHover = networkMeasure => {
619
if (!hoveredEvent || hoveredEvent.networkMeasure !== networkMeasure) {
644
- setHoveredEvent({
620
+ updateHoveredEvent({
621
...EMPTY_CONTEXT_INFO,
622
networkMeasure,
623
});
629
if (thrownErrorsView) {
630
thrownErrorsView.onHover = thrownError => {
631
if (!hoveredEvent || hoveredEvent.thrownError !== thrownError) {
656
- setHoveredEvent({
632
+ updateHoveredEvent({
633
...EMPTY_CONTEXT_INFO,
634
thrownError,
635
});
700
return (
701
<Fragment>
702
<canvas ref={canvasRef} height={height} width={width} />
727
- <ContextMenu id={CONTEXT_MENU_ID}>
728
- {contextData => {
729
- if (contextData.hoveredEvent == null) {
730
- return null;
731
- }
732
- const {
733
- componentMeasure,
734
- flamechartStackFrame,
735
- measure,
736
- networkMeasure,
737
- schedulingEvent,
738
- suspenseEvent,
739
- } = contextData.hoveredEvent;
740
- return (
741
- <Fragment>
742
- {componentMeasure !== null && (
743
- <ContextMenuItem
744
- onClick={() => copy(componentMeasure.componentName)}
745
- title="Copy component name">
746
- Copy component name
747
- </ContextMenuItem>
748
- )}
749
- {networkMeasure !== null && (
750
- <ContextMenuItem
751
- onClick={() => copy(networkMeasure.url)}
752
- title="Copy URL">
753
- Copy URL
754
- </ContextMenuItem>
755
- )}
756
- {schedulingEvent !== null && (
757
- <ContextMenuItem
758
- onClick={() => copy(schedulingEvent.componentName)}
759
- title="Copy component name">
760
- Copy component name
761
- </ContextMenuItem>
762
- )}
763
- {suspenseEvent !== null && (
764
- <ContextMenuItem
765
- onClick={() => copy(suspenseEvent.componentName)}
766
- title="Copy component name">
767
- Copy component name
768
- </ContextMenuItem>
769
- )}
770
- {measure !== null && (
771
- <ContextMenuItem
772
- onClick={() =>
773
- zoomToBatch(contextData.data, measure, viewState, width)
774
- }
775
- title="Zoom to batch">
776
- Zoom to batch
777
- </ContextMenuItem>
778
- )}
779
- {measure !== null && (
780
- <ContextMenuItem
781
- onClick={() => copySummary(contextData.data, measure)}
782
- title="Copy summary">
783
- Copy summary
784
- </ContextMenuItem>
785
- )}
786
- {flamechartStackFrame !== null && (
787
- <ContextMenuItem
788
- onClick={() => copy(flamechartStackFrame.scriptUrl)}
789
- title="Copy file path">
790
- Copy file path
791
- </ContextMenuItem>
792
- )}
793
- {flamechartStackFrame !== null && (
794
- <ContextMenuItem
795
- onClick={() =>
796
- copy(
797
- `line ${
798
- flamechartStackFrame.locationLine ?? ''
799
- }, column ${flamechartStackFrame.locationColumn ?? ''}`,
800
- )
801
- }
802
- title="Copy location">
803
- Copy location
804
- </ContextMenuItem>
805
- )}
806
- </Fragment>
807
- );
808
- }}
809
- </ContextMenu>
810
- {!isContextMenuShown && !surfaceRef.current.hasActiveView() && (
811
- <EventTooltip
812
- canvasRef={canvasRef}
813
- data={data}
814
- height={height}
815
- hoveredEvent={hoveredEvent}
816
- origin={mouseLocation}
817
- width={width}
818
- />
819
- )}
703
+
704
+ <CanvasPageContextMenu
705
+ canvasRef={canvasRef}
706
+ hoveredEvent={lastHoveredEvent}
707
+ timelineData={data}
708
+ viewState={viewState}
709
+ canvasWidth={width}
710
+ closedMenuStub={
711
+ !surfaceRef.current.hasActiveView() ? (
712
+ <EventTooltip
713
+ canvasRef={canvasRef}
714
+ data={data}
715
+ height={height}
716
+ hoveredEvent={hoveredEvent}
717
+ origin={mouseLocation}
718
+ width={width}
719
+ />
720
+ ) : null
721
+ }
722
+ ref={contextMenuRef}
723
+ />
724
</Fragment>
725
);
726
}