Files
react/packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.js
T
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

106 lines
2.5 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 {useState} from 'react';
import Store from '../../store';
import EditableName from './EditableName';
import EditableValue from './EditableValue';
import {parseHookPathForEdit} from './utils';
import styles from './NewKeyValue.css';
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
type Props = {
bridge: FrontendBridge,
depth: number,
hidden: boolean,
hookID?: ?number,
inspectedElement: InspectedElement,
path: Array<string | number>,
store: Store,
type: 'props' | 'state' | 'hooks' | 'context',
};
export default function NewKeyValue({
bridge,
depth,
hidden,
hookID,
inspectedElement,
path,
store,
type,
}: Props): React.Node {
const [newPropKey, setNewPropKey] = useState<number>(0);
const [newPropName, setNewPropName] = useState<string>('');
// $FlowFixMe[missing-local-annot]
const overrideNewEntryName = (oldPath: any, newPath) => {
setNewPropName(newPath[newPath.length - 1]);
};
const overrideNewEntryValue = (
newPath: Array<string | number>,
value: any,
) => {
if (!newPropName) {
return;
}
setNewPropName('');
setNewPropKey(newPropKey + 1);
const {id} = inspectedElement;
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
let basePath: Array<string | number> = newPath;
if (hookID != null) {
basePath = parseHookPathForEdit(basePath);
}
bridge.send('overrideValueAtPath', {
type,
hookID,
id,
path: basePath,
rendererID,
value,
});
}
};
return (
<div
data-testname="NewKeyValue"
key={newPropKey}
hidden={hidden}
style={{
paddingLeft: `${(depth - 1) * 0.75}rem`,
}}>
<div className={styles.NewKeyValue}>
<EditableName
autoFocus={newPropKey > 0}
className={styles.EditableName}
overrideName={overrideNewEntryName}
path={[]}
/>
:&nbsp;
<EditableValue
className={styles.EditableValue}
overrideValue={overrideNewEntryValue}
path={[...path, newPropName]}
value={''}
/>
</div>
</div>
);
}