Files
react/packages/react-devtools-timeline/src/CanvasPageContextMenu.js
Ruslan Lesiutin d14ce51327 refactor[react-devtools]: rewrite context menus (#29049)
## Summary
- While rolling out RDT 5.2.0 on Fusebox, we've discovered that context
menus don't work well with this environment. The reason for it is the
context menu state implementation - in a global context we define a map
of registered context menus, basically what is shown at the moment (see
deleted Contexts.js file). These maps are not invalidated on each
re-initialization of DevTools frontend, since the bundle
(react-devtools-fusebox module) is not reloaded, and this results into
RDT throwing an error that some context menu was already registered.
- We should not keep such data in a global state, since there is no
guarantee that this will be invalidated with each re-initialization of
DevTools (like with browser extension, for example).
- The new implementation is based on a `ContextMenuContainer` component,
which will add all required `contextmenu` event listeners to the
anchor-element. This component will also receive a list of `items` that
will be displayed in the shown context menu.
- The `ContextMenuContainer` component is also using
`useImperativeHandle` hook to extend the instance of the component, so
context menus can be managed imperatively via `ref`:
`contextMenu.current?.hide()`, for example.
- **Changed**: The option for copying value to clipboard is now hidden
for functions. The reasons for it are:
- It is broken in the current implementation, because we call
`JSON.stringify` on the value, see
`packages/react-devtools-shared/src/backend/utils.js`.
- I don't see any reasonable value in doing this for the user, since `Go
to definition` option is available and you can inspect the real code and
then copy it.
- We already filter out fields from objects, if their value is a
function, because the whole object is passed to `JSON.stringify`.

## How did you test this change?
### Works with element props and hooks:
- All context menu items work reliably for props items
- All context menu items work reliably or hooks items


https://github.com/facebook/react/assets/28902667/5e2d58b0-92fa-4624-ad1e-2bbd7f12678f

### Works with timeline profiler:
- All context menu items work reliably: copying, zooming, ...
- Context menu automatically closes on the scroll event


https://github.com/facebook/react/assets/28902667/de744cd0-372a-402a-9fa0-743857048d24

### Works with Fusebox:
- Produces no errors
- Copy to clipboard context menu item works reliably


https://github.com/facebook/react/assets/28902667/0288f5bf-0d44-435c-8842-6b57bc8a7a24
2024-05-20 15:12:21 +01:00

177 lines
4.2 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {useMemo} from 'react';
import {copy} from 'clipboard-js';
import prettyMilliseconds from 'pretty-ms';
import ContextMenuContainer from 'react-devtools-shared/src/devtools/ContextMenu/ContextMenuContainer';
import {getBatchRange} from './utils/getBatchRange';
import {moveStateToRange} from './view-base/utils/scrollState';
import {MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL} from './view-base/constants';
import type {
ContextMenuItem,
ContextMenuRef,
} from 'react-devtools-shared/src/devtools/ContextMenu/types';
import type {
ReactEventInfo,
ReactMeasure,
TimelineData,
ViewState,
} from './types';
function zoomToBatch(
data: TimelineData,
measure: ReactMeasure,
viewState: ViewState,
width: number,
) {
const {batchUID} = measure;
const [rangeStart, rangeEnd] = getBatchRange(batchUID, data);
// Convert from time range to ScrollState
const scrollState = moveStateToRange({
state: viewState.horizontalScrollState,
rangeStart,
rangeEnd,
contentLength: data.duration,
minContentLength: data.duration * MIN_ZOOM_LEVEL,
maxContentLength: data.duration * MAX_ZOOM_LEVEL,
containerLength: width,
});
viewState.updateHorizontalScrollState(scrollState);
}
function copySummary(data: TimelineData, measure: ReactMeasure) {
const {batchUID, duration, timestamp, type} = measure;
const [startTime, stopTime] = getBatchRange(batchUID, data);
copy(
JSON.stringify({
type,
timestamp: prettyMilliseconds(timestamp),
duration: prettyMilliseconds(duration),
batchDuration: prettyMilliseconds(stopTime - startTime),
}),
);
}
type Props = {
canvasRef: {current: HTMLCanvasElement | null},
hoveredEvent: ReactEventInfo | null,
timelineData: TimelineData,
viewState: ViewState,
canvasWidth: number,
closedMenuStub: React.Node,
ref: ContextMenuRef,
};
export default function CanvasPageContextMenu({
canvasRef,
timelineData,
hoveredEvent,
viewState,
canvasWidth,
closedMenuStub,
ref,
}: Props): React.Node {
const menuItems = useMemo<ContextMenuItem[]>(() => {
if (hoveredEvent == null) {
return [];
}
const {
componentMeasure,
flamechartStackFrame,
measure,
networkMeasure,
schedulingEvent,
suspenseEvent,
} = hoveredEvent;
const items: ContextMenuItem[] = [];
if (componentMeasure != null) {
items.push({
onClick: () => copy(componentMeasure.componentName),
content: 'Copy component name',
});
}
if (networkMeasure != null) {
items.push({
onClick: () => copy(networkMeasure.url),
content: 'Copy URL',
});
}
if (schedulingEvent != null) {
items.push({
onClick: () => copy(schedulingEvent.componentName),
content: 'Copy component name',
});
}
if (suspenseEvent != null) {
items.push({
onClick: () => copy(suspenseEvent.componentName),
content: 'Copy component name',
});
}
if (measure != null) {
items.push(
{
onClick: () =>
zoomToBatch(timelineData, measure, viewState, canvasWidth),
content: 'Zoom to batch',
},
{
onClick: () => copySummary(timelineData, measure),
content: 'Copy summary',
},
);
}
if (flamechartStackFrame != null) {
items.push(
{
onClick: () => copy(flamechartStackFrame.scriptUrl),
content: 'Copy file path',
},
{
onClick: () =>
copy(
`line ${flamechartStackFrame.locationLine ?? ''}, column ${
flamechartStackFrame.locationColumn ?? ''
}`,
),
content: 'Copy location',
},
);
}
return items;
}, [hoveredEvent, viewState, canvasWidth]);
return (
<ContextMenuContainer
anchorElementRef={canvasRef}
items={menuItems}
closedMenuStub={closedMenuStub}
ref={ref}
/>
);
}