Support editable props, state, and context values
Brian Vaughn committed
Feb 17, 2019 at 13:07 UTC
a4212dcdce1b5cbaf8d436871b1fe8968814a358
15 files changed
+428
-37
shells/dev/app/EditableProps/EditableProps.css
new
+13
@@ -0,0 +1,13 @@
1
+.App {
2
+ /* GitHub.com frontend fonts */
3
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
4
+ sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
5
+ font-size: 14px;
6
+ line-height: 1.5;
7
+}
8
+
9
+.Header {
10
+ font-size: 1.5rem;
11
+ font-weight: bold;
12
+ margin-bottom: 0.5rem;
13
+}
shells/dev/app/EditableProps/EditableProps.js
new
+57
@@ -0,0 +1,57 @@
1
+// @flow
2
+
3
+import React, { createContext, Component, Fragment } from 'react';
4
+import styles from './EditableProps.css';
5
+
6
+type StatefulFunctionProps = {| count: number |};
7
+
8
+function StatefulFunction({ count }: StatefulFunctionProps) {
9
+ return <li>Count: {count}</li>;
10
+}
11
+
12
+const BoolContext = createContext(true);
13
+// $FlowFixMe Flow does not yet know about Context.displayName
14
+BoolContext.displayName = 'BoolContext';
15
+
16
+type Props = {| name: string, toggle: boolean |};
17
+type State = {| cities: Array<string>, state: string |};
18
+
19
+class StatefulClass extends Component<Props, State> {
20
+ static contextType = BoolContext;
21
+
22
+ state: State = {
23
+ cities: ['San Francisco', 'San Jose'],
24
+ state: 'California',
25
+ };
26
+
27
+ handleChange = ({ target }) =>
28
+ this.setState({
29
+ state: target.value,
30
+ });
31
+
32
+ render() {
33
+ return (
34
+ <Fragment>
35
+ <li>Name: {this.props.name}</li>
36
+ <li>Toggle: {this.props.toggle ? 'true' : 'false'}</li>
37
+ <li>
38
+ State: <input value={this.state.state} onChange={this.handleChange} />
39
+ </li>
40
+ <li>Cities: {this.state.cities.join(', ')}</li>
41
+ <li>Context: {this.context ? 'true' : 'false'}</li>
42
+ </Fragment>
43
+ );
44
+ }
45
+}
46
+
47
+export default function EditableProps() {
48
+ return (
49
+ <div className={styles.App}>
50
+ <div className={styles.Header}>Editable props</div>
51
+ <ul>
52
+ <StatefulClass name="Brian" toggle={true} />
53
+ <StatefulFunction count={1} />
54
+ </ul>
55
+ </div>
56
+ );
57
+}
shells/dev/app/EditableProps/index.js
new
+5
@@ -0,0 +1,5 @@
1
+// @flow
2
+
3
+import EditableProps from './EditableProps';
4
+
5
+export default EditableProps;
shells/dev/app/index.js
+2
@@ -4,6 +4,7 @@
4
5
import { createElement } from 'react';
6
import { render, unmountComponentAtNode } from 'react-dom';
7
+import EditableProps from './EditableProps';
8
import ElementTypes from './ElementTypes';
9
import InspectableElements from './InspectableElements';
10
import ToDoList from './ToDoList';
@@ -24,6 +25,7 @@ function mountTestApp() {
25
mountHelper(ToDoList);
26
mountHelper(InspectableElements);
27
mountHelper(ElementTypes);
28
+ mountHelper(EditableProps);
29
}
30
31
function unmountTestApp() {
src/backend/agent.js
+44
-2
@@ -18,6 +18,18 @@ const debug = (methodName, ...args) => {
18
}
19
};
20
21
+type InspectSelectParams = {|
22
+ id: number,
23
+ rendererID: number,
24
+|};
25
+
26
+type SetInParams = {|
27
+ id: number,
28
+ path: Array<string | number>,
29
+ rendererID: number,
30
+ value: any,
31
+|};
32
+
33
export default class Agent extends EventEmitter {
34
_bridge: Bridge = ((null: any): Bridge);
35
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
@@ -27,6 +39,9 @@ export default class Agent extends EventEmitter {
39
40
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
41
bridge.addListener('inspectElement', this.inspectElement);
42
+ bridge.addListener('overrideContext', this.overrideContext);
43
+ bridge.addListener('overrideProps', this.overrideProps);
44
+ bridge.addListener('overrideState', this.overrideState);
45
bridge.addListener('selectElement', this.selectElement);
46
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
47
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
@@ -80,7 +95,7 @@ export default class Agent extends EventEmitter {
95
}
96
};
97
83
- inspectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
98
+ inspectElement = ({ id, rendererID }: InspectSelectParams) => {
99
const renderer = this._rendererInterfaces[rendererID];
100
if (renderer == null) {
101
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -89,7 +104,7 @@ export default class Agent extends EventEmitter {
104
}
105
};
106
92
- selectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
107
+ selectElement = ({ id, rendererID }: InspectSelectParams) => {
108
const renderer = this._rendererInterfaces[rendererID];
109
if (renderer == null) {
110
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -98,6 +113,33 @@ export default class Agent extends EventEmitter {
113
}
114
};
115
116
+ overrideContext = ({ id, path, rendererID, value }: SetInParams) => {
117
+ const renderer = this._rendererInterfaces[rendererID];
118
+ if (renderer == null) {
119
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
120
+ } else {
121
+ renderer.setInContext(id, path, value);
122
+ }
123
+ };
124
+
125
+ overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
126
+ const renderer = this._rendererInterfaces[rendererID];
127
+ if (renderer == null) {
128
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
129
+ } else {
130
+ renderer.setInProps(id, path, value);
131
+ }
132
+ };
133
+
134
+ overrideState = ({ id, path, rendererID, value }: SetInParams) => {
135
+ const renderer = this._rendererInterfaces[rendererID];
136
+ if (renderer == null) {
137
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
138
+ } else {
139
+ renderer.setInState(id, path, value);
140
+ }
141
+ };
142
+
143
setRendererInterface(
144
rendererID: RendererID,
145
rendererInterface: RendererInterface
src/backend/renderer.js
+62
-7
@@ -2,7 +2,8 @@
2
3
import { gte } from 'semver';
4
import {
5
- ElementTypeClassOrFunction,
5
+ ElementTypeClass,
6
+ ElementTypeFunction,
7
ElementTypeContext,
8
ElementTypeForwardRef,
9
ElementTypeMemo,
@@ -12,7 +13,7 @@ import {
13
ElementTypeSuspense,
14
} from 'src/devtools/types';
15
import { getDisplayName, utfEncodeString } from '../utils';
15
-import { cleanForBridge } from './utils';
16
+import { cleanForBridge, copyWithSet, setInObject } from './utils';
17
import {
18
__DEBUG__,
19
TREE_OPERATION_ADD,
@@ -193,6 +194,8 @@ export function attach(
194
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
195
} = ReactSymbols;
196
197
+ const { overrideProps } = renderer;
198
+
199
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
200
if (__DEBUG__) {
201
const fiberData = getDataForFiber(fiber);
@@ -295,13 +298,19 @@ export function attach(
298
299
switch (tag) {
300
case ClassComponent:
298
- case FunctionComponent:
301
case IncompleteClassComponent:
302
+ fiberData = {
303
+ displayName: getDisplayName(resolvedType),
304
+ key,
305
+ type: ElementTypeClass,
306
+ };
307
+ break;
308
+ case FunctionComponent:
309
case IndeterminateComponent:
310
fiberData = {
311
displayName: getDisplayName(resolvedType),
312
key,
304
- type: ElementTypeClassOrFunction,
313
+ type: ElementTypeFunction,
314
};
315
break;
316
case ForwardRef:
@@ -1072,7 +1081,7 @@ export function attach(
1081
tag === IncompleteClassComponent ||
1082
tag === IndeterminateComponent
1083
) {
1075
- if (stateNode && Object.keys(stateNode.context).length > 0) {
1084
+ if (stateNode && stateNode.context != null) {
1085
context = stateNode.context;
1086
}
1087
} else if (
@@ -1135,7 +1144,7 @@ export function attach(
1144
id,
1145
1146
// Does the current renderer support editable props/state/hooks?
1138
- canEditValues: false, // TODO
1147
+ canEditFunctionProps: typeof overrideProps === 'function',
1148
1149
// Inspectable properties.
1150
// TODO Review sanitization approach for the below inspectable values.
@@ -1156,6 +1165,49 @@ export function attach(
1165
};
1166
}
1167
1168
+ function setInProps(id: number, path: Array<string | number>, value: any) {
1169
+ const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1170
+ if (fiber !== null) {
1171
+ const instance = fiber.stateNode;
1172
+ if (instance === null) {
1173
+ if (typeof overrideProps === 'function') {
1174
+ overrideProps(fiber, path, value);
1175
+ }
1176
+ } else {
1177
+ fiber.pendingProps = copyWithSet(instance.props, path, value);
1178
+ instance.forceUpdate();
1179
+ }
1180
+ }
1181
+ }
1182
+
1183
+ function setInState(id: number, path: Array<string | number>, value: any) {
1184
+ const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1185
+ if (fiber !== null) {
1186
+ const instance = fiber.stateNode;
1187
+ setInObject(instance.state, path, value);
1188
+ instance.forceUpdate();
1189
+ }
1190
+ }
1191
+
1192
+ function setInContext(id: number, path: Array<string | number>, value: any) {
1193
+ // To simplify hydration and display of primative context values (e.g. number, string)
1194
+ // the inspectElement() method wraps context in a {value: ...} object.
1195
+ // We need to remove the first part of the path (the "value") before continuing.
1196
+ path = path.slice(1);
1197
+
1198
+ const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1199
+ if (fiber !== null) {
1200
+ const instance = fiber.stateNode;
1201
+ if (path.length === 0) {
1202
+ // Simple context value
1203
+ instance.context = value;
1204
+ } else {
1205
+ setInObject(instance.context, path, value);
1206
+ }
1207
+ instance.forceUpdate();
1208
+ }
1209
+ }
1210
+
1211
return {
1212
getFiberIDFromNative,
1213
getNativeFromReactElement,
@@ -1164,7 +1216,10 @@ export function attach(
1216
inspectElement,
1217
selectElement,
1218
cleanup,
1167
- walkTree,
1219
renderer,
1220
+ setInContext,
1221
+ setInProps,
1222
+ setInState,
1223
+ walkTree,
1224
};
1225
}
src/backend/types.js
+3
@@ -55,6 +55,9 @@ export type RendererInterface = {
55
inspectElement: (id: number) => InspectedElement | null,
56
renderer: ReactRenderer | null,
57
selectElement: (id: number) => void,
58
+ setInProps: (id: number, path: Array<string | number>, value: any) => void,
59
+ setInState: (id: number, path: Array<string | number>, value: any) => void,
60
+ setInContext: (id: number, path: Array<string | number>, value: any) => void,
61
walkTree: () => void,
62
};
63
src/backend/utils.js
+34
@@ -16,3 +16,37 @@ export function cleanForBridge(data: Object | null): DehydratedData | null {
16
return null;
17
}
18
}
19
+
20
+export function copyWithSet(
21
+ obj: Object | Array<any>,
22
+ path: Array<string | number>,
23
+ value: any,
24
+ index: number = 0
25
+): Object | Array<any> {
26
+ if (index >= path.length) {
27
+ return value;
28
+ }
29
+ const key = path[index];
30
+ const updated = Array.isArray(obj) ? obj.slice() : { ...obj };
31
+ // $FlowFixMe number or string is fine here
32
+ updated[key] = copyWithSet(obj[key], path, value, index + 1);
33
+ return updated;
34
+}
35
+
36
+export function setInObject(
37
+ object: Object,
38
+ path: Array<string | number>,
39
+ value: any
40
+) {
41
+ const last = path.pop();
42
+ if (object != null) {
43
+ const parent: Object = path.reduce(
44
+ // $FlowFixMe
45
+ (reduced, attribute) => reduced[attribute],
46
+ object
47
+ );
48
+ if (parent) {
49
+ parent[last] = value;
50
+ }
51
+ }
52
+}
src/devtools/types.js
+12
-11
@@ -1,18 +1,19 @@
1
// @flow
2
3
-export const ElementTypeClassOrFunction = 1;
4
-export const ElementTypeContext = 2;
5
-export const ElementTypeForwardRef = 3;
6
-export const ElementTypeMemo = 4;
7
-export const ElementTypeOtherOrUnknown = 5;
8
-export const ElementTypeProfiler = 6;
9
-export const ElementTypeRoot = 7;
10
-export const ElementTypeSuspense = 8;
3
+export const ElementTypeClass = 1;
4
+export const ElementTypeFunction = 2;
5
+export const ElementTypeContext = 3;
6
+export const ElementTypeForwardRef = 4;
7
+export const ElementTypeMemo = 5;
8
+export const ElementTypeOtherOrUnknown = 6;
9
+export const ElementTypeProfiler = 7;
10
+export const ElementTypeRoot = 8;
11
+export const ElementTypeSuspense = 9;
12
13
// Different types of elements displayed in the Elements tree.
14
// These types may be used to visually distinguish types,
15
// or to enable/disable certain functionality.
15
-export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
16
+export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
17
18
// Each element on the frontend corresponds to a Fiber on the backend.
19
// Some of its information (e.g. id, type, displayName) come from the backend.
@@ -47,8 +48,8 @@ export type Owner = {|
48
export type InspectedElement = {|
49
id: number,
50
50
- // Does the current renderer support editable props/state/hooks?
51
- canEditValues: boolean,
51
+ // Does the current renderer support editable function props?
52
+ canEditFunctionProps: boolean,
53
54
// Inspectable properties.
55
context: Object | null,
src/devtools/views/Element.js
+3
-2
@@ -1,7 +1,7 @@
1
// @flow
2
3
import React, { Fragment, useCallback, useContext, useMemo } from 'react';
4
-import { ElementTypeClassOrFunction } from 'src/devtools/types';
4
+import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
5
import { createRegExp } from './utils';
6
import { TreeContext } from './TreeContext';
7
@@ -42,7 +42,8 @@ export default function ElementView({ index, style }: Props) {
42
);
43
44
const isSelected = selectedElementID === id;
45
- const showDollarR = isSelected && type === ElementTypeClassOrFunction;
45
+ const showDollarR =
46
+ isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
47
48
// TODO styles.SelectedElement is 100% width but it doesn't take horizontal overflow into account.
49
src/devtools/views/InspectedElementTree.css
+28
@@ -7,16 +7,44 @@
7
}
8
9
.Item {
10
+ display: flex;
11
}
12
13
.Name {
14
color: var(--color-attribute-name);
15
+ flex: 0 0 auto;
16
+}
17
+.Name:after {
18
+ content: ': ';
19
+ color: var(--color-text-color);
20
+ margin-right: 0.5rem;
21
}
22
23
.Value {
24
color: var(--color-attribute-value);
25
}
26
27
+.ValueInputLabel {
28
+ flex: 1 1 100%;
29
+}
30
+.ValueInputLabel:focus-within {
31
+ background-color: var(--color-button-background-focus);
32
+}
33
+
34
+.ValueInput {
35
+ background: none;
36
+ border: 1px solid transparent;
37
+ color: var(--color-attribute-editable-value);
38
+ border-radius: 0.125rem;
39
+ width: 100%;
40
+ font-family: var(--font-family-monospace);
41
+ font-size: var(--font-size-monospace-normal);
42
+}
43
+.ValueInput:focus {
44
+ background-color: var(--color-button-background-focus);
45
+ outline: none;
46
+}
47
+
48
.None {
49
color: var(--color-dimmer);
50
font-style: italic;
src/devtools/views/InspectedElementTree.js
+106
-8
@@ -1,19 +1,23 @@
1
// @flow
2
3
-import React from 'react';
3
+import React, { useCallback, useState } from 'react';
4
import { getMetaValueLabel } from './utils';
5
import { meta } from '../../hydration';
6
import styles from './InspectedElementTree.css';
7
8
+type OverrideValueFn = (path: Array<string | number>, value: any) => void;
9
+
10
type Props = {|
11
data: Object | null,
12
label: string,
13
+ overrideValueFn?: ?OverrideValueFn,
14
showWhenEmpty?: boolean,
15
|};
16
17
export default function InspectedElementTree({
18
data,
19
label,
20
+ overrideValueFn,
21
showWhenEmpty = false,
22
}: Props) {
23
const isEmpty = data === null || Object.keys(data).length === 0;
@@ -33,6 +37,8 @@ export default function InspectedElementTree({
37
key={name}
38
depth={1}
39
name={name}
40
+ overrideValueFn={overrideValueFn}
41
+ path={[name]}
42
value={(data: any)[name]}
43
/>
44
))}
@@ -44,10 +50,18 @@ export default function InspectedElementTree({
50
type KeyValueProps = {|
51
depth: number,
52
name: string,
53
+ overrideValueFn?: ?OverrideValueFn,
54
+ path?: Array<any>,
55
value: any,
56
|};
57
50
-export function KeyValue({ depth, name, value }: KeyValueProps) {
58
+export function KeyValue({
59
+ depth,
60
+ name,
61
+ overrideValueFn,
62
+ path = [],
63
+ value,
64
+}: KeyValueProps) {
65
const dataType = typeof value;
66
const isSimpleType =
67
dataType === 'number' ||
@@ -72,14 +86,24 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
86
87
children = (
88
<div key="root" className={styles.Item} style={{ paddingLeft }}>
75
- <span className={styles.Name}>{name}</span>:{' '}
76
- <span className={styles.Value}>{displayValue}</span>
89
+ <span className={styles.Name}>{name}</span>
90
+ {typeof overrideValueFn === 'function' ? (
91
+ <EditableValue
92
+ dataType={dataType}
93
+ overrideValueFn={overrideValueFn}
94
+ path={path}
95
+ value={value}
96
+ />
97
+ ) : (
98
+ <span className={styles.Value}>{displayValue}</span>
99
+ )}
100
</div>
101
);
102
} else if (value.hasOwnProperty(meta.type)) {
103
+ // TODO Is this type even necessary? Can we just drop it?
104
children = (
105
<div key="root" className={styles.Item} style={{ paddingLeft }}>
82
- <span className={styles.Name}>{name}</span>:{' '}
106
+ <span className={styles.Name}>{name}</span>
107
<span className={styles.Value}>{getMetaValueLabel(value)}</span>
108
</div>
109
);
@@ -90,6 +114,8 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
114
key={index}
115
depth={depth + 1}
116
name={index}
117
+ overrideValueFn={overrideValueFn}
118
+ path={path.concat(index)}
119
value={value[index]}
120
/>
121
));
@@ -99,13 +125,21 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
125
className={styles.Item}
126
style={{ paddingLeft }}
127
>
102
- <span className={styles.Name}>{name}</span>: <span>Array</span>
128
+ <span className={styles.Name}>{name}</span>
129
+ <span>Array</span>
130
</div>
131
);
132
} else {
133
// $FlowFixMe
134
children = Object.entries(value).map(([name, value]) => (
108
- <KeyValue key={name} depth={depth + 1} name={name} value={value} />
135
+ <KeyValue
136
+ key={name}
137
+ depth={depth + 1}
138
+ name={name}
139
+ overrideValueFn={overrideValueFn}
140
+ path={path.concat(name)}
141
+ value={value}
142
+ />
143
));
144
children.unshift(
145
<div
@@ -113,7 +147,8 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
147
className={styles.Item}
148
style={{ paddingLeft }}
149
>
116
- <span className={styles.Name}>{name}</span>: <span>Object</span>
150
+ <span className={styles.Name}>{name}</span>
151
+ <span>Object</span>
152
</div>
153
);
154
}
@@ -121,3 +156,66 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
156
157
return children;
158
}
159
+
160
+type EditableValueProps = {|
161
+ dataType: string,
162
+ overrideValueFn: OverrideValueFn,
163
+ path: Array<string | number>,
164
+ value: any,
165
+|};
166
+
167
+function EditableValue({
168
+ dataType,
169
+ overrideValueFn,
170
+ path,
171
+ value,
172
+}: EditableValueProps) {
173
+ const [editableValue, setEditableValue] = useState(value);
174
+
175
+ const handleChange = useCallback(
176
+ ({ target }) => {
177
+ if (dataType === 'boolean') {
178
+ setEditableValue(target.checked);
179
+ overrideValueFn(path, target.checked);
180
+ } else if (dataType === 'number') {
181
+ setEditableValue(parseFloat(target.value));
182
+ } else {
183
+ setEditableValue(target.value);
184
+ }
185
+ },
186
+ [dataType, setEditableValue]
187
+ );
188
+
189
+ const handleKeyPress = useCallback(
190
+ ({ key }) => {
191
+ if (key === 'Enter') {
192
+ overrideValueFn(path, editableValue);
193
+ }
194
+ },
195
+ [path, editableValue, overrideValueFn]
196
+ );
197
+
198
+ const handleKeyDown = useCallback(event => event.stopPropagation(), []);
199
+
200
+ // Render different input types based on the dataType
201
+ let type = 'text';
202
+ if (dataType === 'boolean') {
203
+ type = 'checkbox';
204
+ } else if (dataType === 'number') {
205
+ type = 'number';
206
+ }
207
+
208
+ return (
209
+ <label className={styles.ValueInputLabel}>
210
+ <input
211
+ checked={dataType === 'boolean' ? editableValue : undefined}
212
+ className={styles.ValueInput}
213
+ onChange={handleChange}
214
+ onKeyDown={handleKeyDown}
215
+ onKeyPress={handleKeyPress}
216
+ type={type}
217
+ value={dataType === 'boolean' ? undefined : editableValue || ''}
218
+ />
219
+ </label>
220
+ );
221
+}
src/devtools/views/SelectedElement.js
+56
-7
@@ -15,9 +15,10 @@ import HooksTree from './HooksTree';
15
import InspectedElementTree from './InspectedElementTree';
16
import { hydrate } from 'src/hydration';
17
import styles from './SelectedElement.css';
18
+import { ElementTypeClass, ElementTypeFunction } from '../types';
19
20
import type { InspectedElement } from '../types';
20
-import type { DehydratedData } from 'src/devtools/types';
21
+import type { DehydratedData, Element } from 'src/devtools/types';
22
23
export type Props = {||};
24
@@ -87,26 +88,74 @@ export default function SelectedElement(_: Props) {
88
)}
89
90
{inspectedElement !== null && (
90
- <InspectedElementView inspectedElement={inspectedElement} />
91
+ <InspectedElementView
92
+ element={element}
93
+ inspectedElement={inspectedElement}
94
+ />
95
)}
96
</div>
97
);
98
}
99
100
type InspectedElementViewProps = {|
101
+ element: Element,
102
inspectedElement: InspectedElement,
103
|};
104
100
-function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
101
- let { context, hooks, owners, props, state } = inspectedElement;
105
+function InspectedElementView({
106
+ element,
107
+ inspectedElement,
108
+}: InspectedElementViewProps) {
109
+ const { id, type } = element;
110
+ const { context, hooks, owners, props, state } = inspectedElement;
111
+
112
const { ownerStack } = useContext(TreeContext);
113
+ const bridge = useContext(BridgeContext);
114
+ const store = useContext(StoreContext);
115
+
116
+ let overrideContextFn = null;
117
+ let overridePropsFn = null;
118
+ let overrideStateFn = null;
119
+ if (type === ElementTypeClass) {
120
+ overrideContextFn = (path: Array<string | number>, value: any) => {
121
+ const rendererID = store.getRendererIDForElement(id);
122
+ bridge.send('overrideContext', { id, path, rendererID, value });
123
+ };
124
+ overridePropsFn = (path: Array<string | number>, value: any) => {
125
+ const rendererID = store.getRendererIDForElement(id);
126
+ bridge.send('overrideProps', { id, path, rendererID, value });
127
+ };
128
+ overrideStateFn = (path: Array<string | number>, value: any) => {
129
+ const rendererID = store.getRendererIDForElement(id);
130
+ bridge.send('overrideState', { id, path, rendererID, value });
131
+ };
132
+ } else if (type === ElementTypeFunction) {
133
+ // TODO Only enable this if renderer.canEditFunctionProps is true!
134
+ overridePropsFn = (path: Array<string | number>, value: any) => {
135
+ const rendererID = store.getRendererIDForElement(id);
136
+ bridge.send('overrideProps', { id, path, rendererID, value });
137
+ };
138
+ }
139
140
return (
141
<div className={styles.InspectedElement}>
106
- <InspectedElementTree label="props" data={props} showWhenEmpty />
107
- <InspectedElementTree label="state" data={state} />
142
+ <InspectedElementTree
143
+ label="props"
144
+ data={props}
145
+ overrideValueFn={overridePropsFn}
146
+ showWhenEmpty
147
+ />
148
+ <InspectedElementTree
149
+ label="state"
150
+ data={state}
151
+ overrideValueFn={overrideStateFn}
152
+ />
153
<HooksTree hooksTree={hooks} />
109
- <InspectedElementTree label="context" data={context} />
154
+ <InspectedElementTree
155
+ label="context"
156
+ data={context}
157
+ overrideValueFn={overrideContextFn}
158
+ />
159
160
{ownerStack.length === 0 && owners !== null && owners.length > 0 && (
161
<div className={styles.Owners}>
src/devtools/views/SettingsContext.js
+1
@@ -118,6 +118,7 @@ function updateDisplayDensity(displayDensity: DisplayDensity): void {
118
function updateThemeVariables(theme: Theme): void {
119
updateStyleHelper(theme, 'color-attribute-name');
120
updateStyleHelper(theme, 'color-attribute-value');
121
+ updateStyleHelper(theme, 'color-attribute-editable-value');
122
updateStyleHelper(theme, 'color-background');
123
updateStyleHelper(theme, 'color-border');
124
updateStyleHelper(theme, 'color-button-background');
src/devtools/views/root.css
+2
@@ -6,6 +6,7 @@
6
/* Light theme */
7
--light-color-attribute-name: #ef6632;
8
--light-color-attribute-value: #1a1aa6;
9
+ --light-color-attribute-editable-value: #1a1aa6;
10
--light-color-background: #ffffff;
11
--light-color-button-background: #ffffff;
12
--light-color-button-background-focus: #ebf1fb;
@@ -31,6 +32,7 @@
32
/* Dark theme */
33
--dark-color-attribute-name: #9d87d2;
34
--dark-color-attribute-value: #cedae0;
35
+ --dark-color-attribute-editable-value: yellow;
36
--dark-color-background: #282c34;
37
--dark-color-button-background: #282c34;
38
--dark-color-button-background-focus: #3d424a;