mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added trace updates feature (DOM only) (#16989)
* Added trace updates feature (DOM only) * Updated DevTools CHANGELOG
This commit is contained in:
+1
-1
@@ -114,7 +114,7 @@ export function connectToDevTools(options: ?ConnectOptions) {
|
||||
}
|
||||
|
||||
if (bridge !== null) {
|
||||
bridge.emit('shutdown');
|
||||
bridge.shutdown();
|
||||
}
|
||||
|
||||
scheduleRetry();
|
||||
|
||||
@@ -67,6 +67,10 @@ function setup(hook) {
|
||||
|
||||
initBackend(hook, agent, window);
|
||||
|
||||
// Let the frontend know that the backend has attached listeners and is ready for messages.
|
||||
// This covers the case of of syncing saved values after reloading/navigating while DevTools remain open.
|
||||
bridge.send('extensionBackendInitialized');
|
||||
|
||||
// Setup React Native style editor if a renderer like react-native-web has injected it.
|
||||
if (hook.resolveRNStyle) {
|
||||
setupNativeStyleEditor(
|
||||
|
||||
+12
@@ -9,6 +9,7 @@ import {
|
||||
getBrowserName,
|
||||
getBrowserTheme,
|
||||
} from './utils';
|
||||
import {LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY} from 'react-devtools-shared/src/constants';
|
||||
import {
|
||||
getSavedComponentFilters,
|
||||
getAppendComponentStack,
|
||||
@@ -125,10 +126,21 @@ function createPanelIfReactLoaded() {
|
||||
profilingData = store.profilerStore.profilingData;
|
||||
}
|
||||
|
||||
bridge.addListener('extensionBackendInitialized', () => {
|
||||
// Initialize the renderer's trace-updates setting.
|
||||
// This handles the case of navigating to a new page after the DevTools have already been shown.
|
||||
bridge.send(
|
||||
'setTraceUpdatesEnabled',
|
||||
localStorageGetItem(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY) ===
|
||||
'true',
|
||||
);
|
||||
});
|
||||
|
||||
store = new Store(bridge, {
|
||||
isProfiling,
|
||||
supportsReloadAndProfile: isChrome,
|
||||
supportsProfiling,
|
||||
supportsTraceUpdates: true,
|
||||
});
|
||||
store.profilerStore.profilingData = profilingData;
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ export function initialize(
|
||||
},
|
||||
});
|
||||
|
||||
const store: Store = new Store(bridge);
|
||||
const store: Store = new Store(bridge, {supportsTraceUpdates: true});
|
||||
|
||||
const ForwardRef = forwardRef<Props, mixed>((props, ref) => (
|
||||
<DevTools ref={ref} bridge={bridge} store={store} {...props} />
|
||||
|
||||
@@ -21,6 +21,10 @@ import {
|
||||
sessionStorageSetItem,
|
||||
} from 'react-devtools-shared/src/storage';
|
||||
import setupHighlighter from './views/Highlighter';
|
||||
import {
|
||||
initialize as setupTraceUpdates,
|
||||
toggleEnabled as setTraceUpdatesEnabled,
|
||||
} from './views/TraceUpdates';
|
||||
import {patch as patchConsole, unpatch as unpatchConsole} from './console';
|
||||
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
@@ -87,6 +91,7 @@ export default class Agent extends EventEmitter<{|
|
||||
hideNativeHighlight: [],
|
||||
showNativeHighlight: [NativeType],
|
||||
shutdown: [],
|
||||
traceUpdates: [Set<NativeType>],
|
||||
|}> {
|
||||
_bridge: BackendBridge;
|
||||
_isProfiling: boolean = false;
|
||||
@@ -94,6 +99,7 @@ export default class Agent extends EventEmitter<{|
|
||||
_rendererInterfaces: {[key: RendererID]: RendererInterface} = {};
|
||||
_persistedSelection: PersistedSelection | null = null;
|
||||
_persistedSelectionMatch: PathMatch | null = null;
|
||||
_traceUpdatesEnabled: boolean = false;
|
||||
|
||||
constructor(bridge: BackendBridge) {
|
||||
super();
|
||||
@@ -131,6 +137,7 @@ export default class Agent extends EventEmitter<{|
|
||||
bridge.addListener('overrideState', this.overrideState);
|
||||
bridge.addListener('overrideSuspense', this.overrideSuspense);
|
||||
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
|
||||
bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
|
||||
bridge.addListener('startProfiling', this.startProfiling);
|
||||
bridge.addListener('stopProfiling', this.stopProfiling);
|
||||
bridge.addListener(
|
||||
@@ -159,6 +166,7 @@ export default class Agent extends EventEmitter<{|
|
||||
bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported);
|
||||
|
||||
setupHighlighter(bridge, this);
|
||||
setupTraceUpdates(this);
|
||||
}
|
||||
|
||||
get rendererInterfaces(): {[key: RendererID]: RendererInterface} {
|
||||
@@ -340,6 +348,8 @@ export default class Agent extends EventEmitter<{|
|
||||
rendererInterface.startProfiling(this._recordChangeDescriptions);
|
||||
}
|
||||
|
||||
rendererInterface.setTraceUpdatesEnabled(this._traceUpdatesEnabled);
|
||||
|
||||
// When the renderer is attached, we need to tell it whether
|
||||
// we remember the previous selection that we'd like to restore.
|
||||
// It'll start tracking mounts for matches to the last selection path.
|
||||
@@ -349,6 +359,19 @@ export default class Agent extends EventEmitter<{|
|
||||
}
|
||||
}
|
||||
|
||||
setTraceUpdatesEnabled = (traceUpdatesEnabled: boolean) => {
|
||||
this._traceUpdatesEnabled = traceUpdatesEnabled;
|
||||
|
||||
setTraceUpdatesEnabled(traceUpdatesEnabled);
|
||||
|
||||
for (let rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
renderer.setTraceUpdatesEnabled(traceUpdatesEnabled);
|
||||
}
|
||||
};
|
||||
|
||||
syncSelectionFromNativeElementsPanel = () => {
|
||||
const target = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0;
|
||||
if (target == null) {
|
||||
@@ -416,6 +439,10 @@ export default class Agent extends EventEmitter<{|
|
||||
}
|
||||
};
|
||||
|
||||
onTraceUpdates = (nodes: Set<NativeType>) => {
|
||||
this.emit('traceUpdates', nodes);
|
||||
};
|
||||
|
||||
onHookOperations = (operations: Array<number>) => {
|
||||
if (__DEBUG__) {
|
||||
debug('onHookOperations', operations);
|
||||
|
||||
@@ -44,6 +44,7 @@ export function initBackend(
|
||||
}),
|
||||
|
||||
hook.sub('operations', agent.onHookOperations),
|
||||
hook.sub('traceUpdates', agent.onTraceUpdates),
|
||||
|
||||
// TODO Add additional subscriptions required for profiling mode
|
||||
];
|
||||
|
||||
@@ -912,6 +912,10 @@ export function attach(
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
function setTraceUpdatesEnabled(enabled: boolean) {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
function setTrackedPath(path: Array<PathFrame> | null) {
|
||||
// Not implemented.
|
||||
}
|
||||
@@ -945,6 +949,7 @@ export function attach(
|
||||
setInHook,
|
||||
setInProps,
|
||||
setInState,
|
||||
setTraceUpdatesEnabled,
|
||||
setTrackedPath,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
|
||||
+100
-9
@@ -57,6 +57,7 @@ import type {
|
||||
InspectedElement,
|
||||
InspectedElementPayload,
|
||||
InstanceAndStyle,
|
||||
NativeType,
|
||||
Owner,
|
||||
PathFrame,
|
||||
PathMatch,
|
||||
@@ -532,6 +533,10 @@ export function attach(
|
||||
const hideElementsWithPaths: Set<RegExp> = new Set();
|
||||
const hideElementsWithTypes: Set<ElementType> = new Set();
|
||||
|
||||
// Highlight updates
|
||||
let traceUpdatesEnabled: boolean = false;
|
||||
let traceUpdatesForNodes: Set<NativeType> = new Set();
|
||||
|
||||
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
|
||||
hideElementsWithTypes.clear();
|
||||
hideElementsWithDisplayNames.clear();
|
||||
@@ -613,7 +618,7 @@ export function attach(
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
currentRootID = getFiberID(getPrimaryFiber(root.current));
|
||||
setRootPseudoKey(currentRootID, root.current);
|
||||
mountFiberRecursively(root.current, null);
|
||||
mountFiberRecursively(root.current, null, false, false);
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
@@ -1227,7 +1232,8 @@ export function attach(
|
||||
function mountFiberRecursively(
|
||||
fiber: Fiber,
|
||||
parentFiber: Fiber | null,
|
||||
traverseSiblings = false,
|
||||
traverseSiblings: boolean,
|
||||
traceNearestHostComponentUpdate: boolean,
|
||||
) {
|
||||
if (__DEBUG__) {
|
||||
debug('mountFiberRecursively()', fiber, parentFiber);
|
||||
@@ -1244,6 +1250,20 @@ export function attach(
|
||||
recordMount(fiber, parentFiber);
|
||||
}
|
||||
|
||||
if (traceUpdatesEnabled) {
|
||||
if (traceNearestHostComponentUpdate) {
|
||||
const elementType = getElementTypeForFiber(fiber);
|
||||
// If an ancestor updated, we should mark the nearest host nodes for highlighting.
|
||||
if (elementType === ElementTypeHostComponent) {
|
||||
traceUpdatesForNodes.add(fiber.stateNode);
|
||||
traceNearestHostComponentUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
// We intentionally do not re-enable the traceNearestHostComponentUpdate flag in this branch,
|
||||
// because we don't want to highlight every host node inside of a newly mounted subtree.
|
||||
}
|
||||
|
||||
const isTimedOutSuspense =
|
||||
fiber.tag === ReactTypeOfWork.SuspenseComponent &&
|
||||
fiber.memoizedState !== null;
|
||||
@@ -1264,6 +1284,7 @@ export function attach(
|
||||
fallbackChild,
|
||||
shouldIncludeInTree ? fiber : parentFiber,
|
||||
true,
|
||||
traceNearestHostComponentUpdate,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -1272,6 +1293,7 @@ export function attach(
|
||||
fiber.child,
|
||||
shouldIncludeInTree ? fiber : parentFiber,
|
||||
true,
|
||||
traceNearestHostComponentUpdate,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1281,7 +1303,12 @@ export function attach(
|
||||
updateTrackedPathStateAfterMount(mightSiblingsBeOnTrackedPath);
|
||||
|
||||
if (traverseSiblings && fiber.sibling !== null) {
|
||||
mountFiberRecursively(fiber.sibling, parentFiber, true);
|
||||
mountFiberRecursively(
|
||||
fiber.sibling,
|
||||
parentFiber,
|
||||
true,
|
||||
traceNearestHostComponentUpdate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,11 +1457,35 @@ export function attach(
|
||||
nextFiber: Fiber,
|
||||
prevFiber: Fiber,
|
||||
parentFiber: Fiber | null,
|
||||
traceNearestHostComponentUpdate: boolean,
|
||||
): boolean {
|
||||
if (__DEBUG__) {
|
||||
debug('updateFiberRecursively()', nextFiber, parentFiber);
|
||||
}
|
||||
|
||||
if (traceUpdatesEnabled) {
|
||||
const elementType = getElementTypeForFiber(nextFiber);
|
||||
if (traceNearestHostComponentUpdate) {
|
||||
// If an ancestor updated, we should mark the nearest host nodes for highlighting.
|
||||
if (elementType === ElementTypeHostComponent) {
|
||||
traceUpdatesForNodes.add(nextFiber.stateNode);
|
||||
traceNearestHostComponentUpdate = false;
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
elementType === ElementTypeFunction ||
|
||||
elementType === ElementTypeClass ||
|
||||
elementType === ElementTypeContext
|
||||
) {
|
||||
// Otherwise if this is a traced ancestor, flag for the nearest host descendant(s).
|
||||
traceNearestHostComponentUpdate = didFiberRender(
|
||||
prevFiber,
|
||||
nextFiber,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
mostRecentlyInspectedElement !== null &&
|
||||
mostRecentlyInspectedElement.id ===
|
||||
@@ -1481,6 +1532,7 @@ export function attach(
|
||||
nextFallbackChildSet,
|
||||
prevFallbackChildSet,
|
||||
nextFiber,
|
||||
traceNearestHostComponentUpdate,
|
||||
)
|
||||
) {
|
||||
shouldResetChildren = true;
|
||||
@@ -1492,7 +1544,12 @@ export function attach(
|
||||
// 2. Mount primary set
|
||||
const nextPrimaryChildSet = nextFiber.child;
|
||||
if (nextPrimaryChildSet !== null) {
|
||||
mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
|
||||
mountFiberRecursively(
|
||||
nextPrimaryChildSet,
|
||||
nextFiber,
|
||||
true,
|
||||
traceNearestHostComponentUpdate,
|
||||
);
|
||||
}
|
||||
shouldResetChildren = true;
|
||||
} else if (!prevDidTimeout && nextDidTimeOut) {
|
||||
@@ -1507,7 +1564,12 @@ export function attach(
|
||||
? nextFiberChild.sibling
|
||||
: null;
|
||||
if (nextFallbackChildSet != null) {
|
||||
mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
|
||||
mountFiberRecursively(
|
||||
nextFallbackChildSet,
|
||||
nextFiber,
|
||||
true,
|
||||
traceNearestHostComponentUpdate,
|
||||
);
|
||||
shouldResetChildren = true;
|
||||
}
|
||||
} else {
|
||||
@@ -1530,6 +1592,7 @@ export function attach(
|
||||
nextChild,
|
||||
prevChild,
|
||||
shouldIncludeInTree ? nextFiber : parentFiber,
|
||||
traceNearestHostComponentUpdate,
|
||||
)
|
||||
) {
|
||||
// If a nested tree child order changed but it can't handle its own
|
||||
@@ -1547,6 +1610,8 @@ export function attach(
|
||||
mountFiberRecursively(
|
||||
nextChild,
|
||||
shouldIncludeInTree ? nextFiber : parentFiber,
|
||||
false,
|
||||
traceNearestHostComponentUpdate,
|
||||
);
|
||||
shouldResetChildren = true;
|
||||
}
|
||||
@@ -1562,6 +1627,19 @@ export function attach(
|
||||
if (prevChildAtSameIndex !== null) {
|
||||
shouldResetChildren = true;
|
||||
}
|
||||
} else {
|
||||
if (traceUpdatesEnabled) {
|
||||
// If we're tracing updates and we've bailed out before reaching a host node,
|
||||
// we should fall back to recursively marking the nearest host descendates for highlight.
|
||||
if (traceNearestHostComponentUpdate) {
|
||||
const hostFibers = findAllCurrentHostFibers(
|
||||
getFiberID(getPrimaryFiber(nextFiber)),
|
||||
);
|
||||
hostFibers.forEach(hostFiber => {
|
||||
traceUpdatesForNodes.add(hostFiber.stateNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldIncludeInTree) {
|
||||
@@ -1645,7 +1723,7 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
mountFiberRecursively(root.current, null);
|
||||
mountFiberRecursively(root.current, null, false, false);
|
||||
flushPendingEvents(root);
|
||||
currentRootID = -1;
|
||||
});
|
||||
@@ -1671,6 +1749,10 @@ export function attach(
|
||||
mightBeOnTrackedPath = true;
|
||||
}
|
||||
|
||||
if (traceUpdatesEnabled) {
|
||||
traceUpdatesForNodes.clear();
|
||||
}
|
||||
|
||||
// Checking root.memoizedInteractions handles multi-renderer edge-case-
|
||||
// where some v16 renderers support profiling and others don't.
|
||||
const isProfilingSupported = root.memoizedInteractions != null;
|
||||
@@ -1704,10 +1786,10 @@ export function attach(
|
||||
if (!wasMounted && isMounted) {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRootID, current);
|
||||
mountFiberRecursively(current, null);
|
||||
mountFiberRecursively(current, null, false, false);
|
||||
} else if (wasMounted && isMounted) {
|
||||
// Update an existing root.
|
||||
updateFiberRecursively(current, alternate, null);
|
||||
updateFiberRecursively(current, alternate, null, false);
|
||||
} else if (wasMounted && !isMounted) {
|
||||
// Unmount an existing root.
|
||||
removeRootPseudoKey(currentRootID);
|
||||
@@ -1716,7 +1798,7 @@ export function attach(
|
||||
} else {
|
||||
// Mount a new root.
|
||||
setRootPseudoKey(currentRootID, current);
|
||||
mountFiberRecursively(current, null);
|
||||
mountFiberRecursively(current, null, false, false);
|
||||
}
|
||||
|
||||
if (isProfiling && isProfilingSupported) {
|
||||
@@ -1738,6 +1820,10 @@ export function attach(
|
||||
// We're done here.
|
||||
flushPendingEvents(root);
|
||||
|
||||
if (traceUpdatesEnabled) {
|
||||
hook.emit('traceUpdates', traceUpdatesForNodes);
|
||||
}
|
||||
|
||||
currentRootID = -1;
|
||||
}
|
||||
|
||||
@@ -3015,6 +3101,10 @@ export function attach(
|
||||
}
|
||||
};
|
||||
|
||||
function setTraceUpdatesEnabled(isEnabled: boolean): void {
|
||||
traceUpdatesEnabled = isEnabled;
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
findNativeNodesForFiberID,
|
||||
@@ -3036,6 +3126,7 @@ export function attach(
|
||||
setInHook,
|
||||
setInProps,
|
||||
setInState,
|
||||
setTraceUpdatesEnabled,
|
||||
setTrackedPath,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
|
||||
+2
-1
@@ -252,10 +252,11 @@ export type RendererInterface = {
|
||||
) => void,
|
||||
setInProps: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setInState: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setTraceUpdatesEnabled: (enabled: boolean) => void,
|
||||
setTrackedPath: (path: Array<PathFrame> | null) => void,
|
||||
startProfiling: (recordChangeDescriptions: boolean) => void,
|
||||
stopProfiling: () => void,
|
||||
updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
|
||||
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
|
||||
};
|
||||
|
||||
export type Handler = (data: any) => void;
|
||||
|
||||
@@ -8,15 +8,9 @@
|
||||
*/
|
||||
|
||||
import assign from 'object-assign';
|
||||
import {getElementDimensions, getNestedBoundingClientRect} from '../utils';
|
||||
|
||||
type Rect = {
|
||||
bottom: number,
|
||||
height: number,
|
||||
left: number,
|
||||
right: number,
|
||||
top: number,
|
||||
width: number,
|
||||
};
|
||||
import type {Rect} from '../utils';
|
||||
|
||||
type Box = {|top: number, left: number, width: number, height: number|};
|
||||
|
||||
@@ -333,116 +327,6 @@ function findTipPos(dims, bounds, tipSize) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getElementDimensions(domElement: Element) {
|
||||
const calculatedStyle = window.getComputedStyle(domElement);
|
||||
return {
|
||||
borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
|
||||
borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
|
||||
borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
|
||||
borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
|
||||
marginLeft: parseInt(calculatedStyle.marginLeft, 10),
|
||||
marginRight: parseInt(calculatedStyle.marginRight, 10),
|
||||
marginTop: parseInt(calculatedStyle.marginTop, 10),
|
||||
marginBottom: parseInt(calculatedStyle.marginBottom, 10),
|
||||
paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
|
||||
paddingRight: parseInt(calculatedStyle.paddingRight, 10),
|
||||
paddingTop: parseInt(calculatedStyle.paddingTop, 10),
|
||||
paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
|
||||
};
|
||||
}
|
||||
|
||||
// Get the window object for the document that a node belongs to,
|
||||
// or return null if it cannot be found (node not attached to DOM,
|
||||
// etc).
|
||||
function getOwnerWindow(node: HTMLElement): typeof window | null {
|
||||
if (!node.ownerDocument) {
|
||||
return null;
|
||||
}
|
||||
return node.ownerDocument.defaultView;
|
||||
}
|
||||
|
||||
// Get the iframe containing a node, or return null if it cannot
|
||||
// be found (node not within iframe, etc).
|
||||
function getOwnerIframe(node: HTMLElement): HTMLElement | null {
|
||||
const nodeWindow = getOwnerWindow(node);
|
||||
if (nodeWindow) {
|
||||
return nodeWindow.frameElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get a bounding client rect for a node, with an
|
||||
// offset added to compensate for its border.
|
||||
function getBoundingClientRectWithBorderOffset(node: HTMLElement) {
|
||||
const dimensions = getElementDimensions(node);
|
||||
return mergeRectOffsets([
|
||||
node.getBoundingClientRect(),
|
||||
{
|
||||
top: dimensions.borderTop,
|
||||
left: dimensions.borderLeft,
|
||||
bottom: dimensions.borderBottom,
|
||||
right: dimensions.borderRight,
|
||||
// This width and height won't get used by mergeRectOffsets (since this
|
||||
// is not the first rect in the array), but we set them so that this
|
||||
// object typechecks as a ClientRect.
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Add together the top, left, bottom, and right properties of
|
||||
// each ClientRect, but keep the width and height of the first one.
|
||||
function mergeRectOffsets(rects: Array<Rect>): Rect {
|
||||
return rects.reduce((previousRect, rect) => {
|
||||
if (previousRect == null) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
return {
|
||||
top: previousRect.top + rect.top,
|
||||
left: previousRect.left + rect.left,
|
||||
width: previousRect.width,
|
||||
height: previousRect.height,
|
||||
bottom: previousRect.bottom + rect.bottom,
|
||||
right: previousRect.right + rect.right,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate a boundingClientRect for a node relative to boundaryWindow,
|
||||
// taking into account any offsets caused by intermediate iframes.
|
||||
function getNestedBoundingClientRect(
|
||||
node: HTMLElement,
|
||||
boundaryWindow: typeof window,
|
||||
): Rect {
|
||||
const ownerIframe = getOwnerIframe(node);
|
||||
if (ownerIframe && ownerIframe !== boundaryWindow) {
|
||||
const rects = [node.getBoundingClientRect()];
|
||||
let currentIframe = ownerIframe;
|
||||
let onlyOneMore = false;
|
||||
while (currentIframe) {
|
||||
const rect = getBoundingClientRectWithBorderOffset(currentIframe);
|
||||
rects.push(rect);
|
||||
currentIframe = getOwnerIframe(currentIframe);
|
||||
|
||||
if (onlyOneMore) {
|
||||
break;
|
||||
}
|
||||
// We don't want to calculate iframe offsets upwards beyond
|
||||
// the iframe containing the boundaryWindow, but we
|
||||
// need to calculate the offset relative to the boundaryWindow.
|
||||
if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
|
||||
onlyOneMore = true;
|
||||
}
|
||||
}
|
||||
|
||||
return mergeRectOffsets(rects);
|
||||
} else {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
function boxWrap(dims, what, node) {
|
||||
assign(node.style, {
|
||||
borderTopWidth: dims[what + 'Top'] + 'px',
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 type {Data} from './index';
|
||||
import type {Rect} from '../utils';
|
||||
import type {NativeType} from '../../types';
|
||||
|
||||
const OUTLINE_COLOR = '#f0f0f0';
|
||||
|
||||
// Note these colors are in sync with DevTools Profiler chart colors.
|
||||
const COLORS = [
|
||||
'#37afa9',
|
||||
'#63b19e',
|
||||
'#80b393',
|
||||
'#97b488',
|
||||
'#abb67d',
|
||||
'#beb771',
|
||||
'#cfb965',
|
||||
'#dfba57',
|
||||
'#efbb49',
|
||||
'#febc38',
|
||||
];
|
||||
|
||||
let canvas: HTMLCanvasElement | null = null;
|
||||
|
||||
export function draw(nodeToData: Map<NativeType, Data>): void {
|
||||
if (canvas === null) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
const canvasFlow: HTMLCanvasElement = ((canvas: any): HTMLCanvasElement);
|
||||
canvasFlow.width = window.screen.availWidth;
|
||||
canvasFlow.height = window.screen.availHeight;
|
||||
|
||||
const context = canvasFlow.getContext('2d');
|
||||
context.clearRect(0, 0, canvasFlow.width, canvasFlow.height);
|
||||
|
||||
nodeToData.forEach(({count, rect}) => {
|
||||
if (rect !== null) {
|
||||
const colorIndex = Math.min(COLORS.length - 1, count - 1);
|
||||
const color = COLORS[colorIndex];
|
||||
|
||||
drawBorder(context, rect, color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawBorder(
|
||||
context: CanvasRenderingContext2D,
|
||||
rect: Rect,
|
||||
color: string,
|
||||
): void {
|
||||
const {height, left, top, width} = rect;
|
||||
|
||||
// outline
|
||||
context.lineWidth = 1;
|
||||
context.strokeStyle = OUTLINE_COLOR;
|
||||
|
||||
context.strokeRect(left - 1, top - 1, width + 2, height + 2);
|
||||
|
||||
// inset
|
||||
context.lineWidth = 1;
|
||||
context.strokeStyle = OUTLINE_COLOR;
|
||||
context.strokeRect(left + 1, top + 1, width - 1, height - 1);
|
||||
context.strokeStyle = color;
|
||||
|
||||
context.setLineDash([0]);
|
||||
|
||||
// border
|
||||
context.lineWidth = 1;
|
||||
context.strokeRect(left, top, width - 1, height - 1);
|
||||
|
||||
context.setLineDash([0]);
|
||||
}
|
||||
|
||||
export function destroy(): void {
|
||||
if (canvas !== null) {
|
||||
if (canvas.parentNode != null) {
|
||||
canvas.parentNode.removeChild(canvas);
|
||||
}
|
||||
canvas = null;
|
||||
}
|
||||
}
|
||||
|
||||
function initialize(): void {
|
||||
canvas = window.document.createElement('canvas');
|
||||
canvas.style.cssText = `
|
||||
xx-background-color: red;
|
||||
xx-opacity: 0.5;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1000000000;
|
||||
`;
|
||||
|
||||
const root = window.document.documentElement;
|
||||
root.insertBefore(canvas, root.firstChild);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its 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 Agent from 'react-devtools-shared/src/backend/agent';
|
||||
import {destroy as destroyCanvas, draw} from './canvas';
|
||||
import {getNestedBoundingClientRect} from '../utils';
|
||||
|
||||
import type {NativeType} from '../../types';
|
||||
import type {Rect} from '../utils';
|
||||
|
||||
// How long the rect should be shown for?
|
||||
const DISPLAY_DURATION = 250;
|
||||
|
||||
// What's the longest we are willing to show the overlay for?
|
||||
// This can be important if we're getting a flurry of events (e.g. scroll update).
|
||||
const MAX_DISPLAY_DURATION = 3000;
|
||||
|
||||
// How long should a rect be considered valid for?
|
||||
const REMEASUREMENT_AFTER_DURATION = 250;
|
||||
|
||||
// Some environments (e.g. React Native / Hermes) don't support the performace API yet.
|
||||
const getCurrentTime =
|
||||
typeof performance === 'object' && typeof performance.now === 'function'
|
||||
? () => performance.now()
|
||||
: () => Date.now();
|
||||
|
||||
export type Data = {|
|
||||
count: number,
|
||||
expirationTime: number,
|
||||
lastMeasuredAt: number,
|
||||
rect: Rect | null,
|
||||
|};
|
||||
|
||||
const nodeToData: Map<NativeType, Data> = new Map();
|
||||
|
||||
let agent: Agent = ((null: any): Agent);
|
||||
let drawAnimationFrameID: AnimationFrameID | null = null;
|
||||
let isEnabled: boolean = false;
|
||||
let redrawTimeoutID: TimeoutID | null = null;
|
||||
|
||||
export function initialize(injectedAgent: Agent): void {
|
||||
agent = injectedAgent;
|
||||
agent.addListener('traceUpdates', traceUpdates);
|
||||
}
|
||||
|
||||
export function toggleEnabled(value: boolean): void {
|
||||
isEnabled = value;
|
||||
|
||||
if (!isEnabled) {
|
||||
nodeToData.clear();
|
||||
|
||||
if (drawAnimationFrameID !== null) {
|
||||
cancelAnimationFrame(drawAnimationFrameID);
|
||||
drawAnimationFrameID = null;
|
||||
}
|
||||
|
||||
if (redrawTimeoutID !== null) {
|
||||
clearTimeout(redrawTimeoutID);
|
||||
redrawTimeoutID = null;
|
||||
}
|
||||
|
||||
destroyCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
function traceUpdates(nodes: Set<NativeType>): void {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.forEach(node => {
|
||||
const data = nodeToData.get(node);
|
||||
const now = getCurrentTime();
|
||||
|
||||
let lastMeasuredAt = data != null ? data.lastMeasuredAt : 0;
|
||||
let rect = data != null ? data.rect : null;
|
||||
if (rect === null || lastMeasuredAt + REMEASUREMENT_AFTER_DURATION < now) {
|
||||
lastMeasuredAt = now;
|
||||
rect = measureNode(node);
|
||||
}
|
||||
|
||||
nodeToData.set(node, {
|
||||
count: data != null ? data.count + 1 : 1,
|
||||
expirationTime:
|
||||
data != null
|
||||
? Math.min(
|
||||
now + MAX_DISPLAY_DURATION,
|
||||
data.expirationTime + DISPLAY_DURATION,
|
||||
)
|
||||
: now + DISPLAY_DURATION,
|
||||
lastMeasuredAt,
|
||||
rect,
|
||||
});
|
||||
});
|
||||
|
||||
if (redrawTimeoutID !== null) {
|
||||
clearTimeout(redrawTimeoutID);
|
||||
redrawTimeoutID = null;
|
||||
}
|
||||
|
||||
if (drawAnimationFrameID === null) {
|
||||
drawAnimationFrameID = requestAnimationFrame(prepareToDraw);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareToDraw(): void {
|
||||
drawAnimationFrameID = null;
|
||||
redrawTimeoutID = null;
|
||||
|
||||
const now = getCurrentTime();
|
||||
let earliestExpiration = Number.MAX_VALUE;
|
||||
|
||||
// Remove any items that have already expired.
|
||||
nodeToData.forEach((data, node) => {
|
||||
if (data.expirationTime < now) {
|
||||
nodeToData.delete(node);
|
||||
} else {
|
||||
earliestExpiration = Math.min(earliestExpiration, data.expirationTime);
|
||||
}
|
||||
});
|
||||
|
||||
draw(nodeToData);
|
||||
|
||||
redrawTimeoutID = setTimeout(prepareToDraw, earliestExpiration - now);
|
||||
}
|
||||
|
||||
function measureNode(node: Object): Rect | null {
|
||||
if (!node || typeof node.getBoundingClientRect !== 'function') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let currentWindow = window.__REACT_DEVTOOLS_TARGET_WINDOW__ || window;
|
||||
|
||||
return getNestedBoundingClientRect(node, currentWindow);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export type Rect = {
|
||||
bottom: number,
|
||||
height: number,
|
||||
left: number,
|
||||
right: number,
|
||||
top: number,
|
||||
width: number,
|
||||
};
|
||||
|
||||
// Get the window object for the document that a node belongs to,
|
||||
// or return null if it cannot be found (node not attached to DOM,
|
||||
// etc).
|
||||
export function getOwnerWindow(node: HTMLElement): typeof window | null {
|
||||
if (!node.ownerDocument) {
|
||||
return null;
|
||||
}
|
||||
return node.ownerDocument.defaultView;
|
||||
}
|
||||
|
||||
// Get the iframe containing a node, or return null if it cannot
|
||||
// be found (node not within iframe, etc).
|
||||
export function getOwnerIframe(node: HTMLElement): HTMLElement | null {
|
||||
const nodeWindow = getOwnerWindow(node);
|
||||
if (nodeWindow) {
|
||||
return nodeWindow.frameElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get a bounding client rect for a node, with an
|
||||
// offset added to compensate for its border.
|
||||
export function getBoundingClientRectWithBorderOffset(node: HTMLElement) {
|
||||
const dimensions = getElementDimensions(node);
|
||||
return mergeRectOffsets([
|
||||
node.getBoundingClientRect(),
|
||||
{
|
||||
top: dimensions.borderTop,
|
||||
left: dimensions.borderLeft,
|
||||
bottom: dimensions.borderBottom,
|
||||
right: dimensions.borderRight,
|
||||
// This width and height won't get used by mergeRectOffsets (since this
|
||||
// is not the first rect in the array), but we set them so that this
|
||||
// object typechecks as a ClientRect.
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// Add together the top, left, bottom, and right properties of
|
||||
// each ClientRect, but keep the width and height of the first one.
|
||||
export function mergeRectOffsets(rects: Array<Rect>): Rect {
|
||||
return rects.reduce((previousRect, rect) => {
|
||||
if (previousRect == null) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
return {
|
||||
top: previousRect.top + rect.top,
|
||||
left: previousRect.left + rect.left,
|
||||
width: previousRect.width,
|
||||
height: previousRect.height,
|
||||
bottom: previousRect.bottom + rect.bottom,
|
||||
right: previousRect.right + rect.right,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate a boundingClientRect for a node relative to boundaryWindow,
|
||||
// taking into account any offsets caused by intermediate iframes.
|
||||
export function getNestedBoundingClientRect(
|
||||
node: HTMLElement,
|
||||
boundaryWindow: typeof window,
|
||||
): Rect {
|
||||
const ownerIframe = getOwnerIframe(node);
|
||||
if (ownerIframe && ownerIframe !== boundaryWindow) {
|
||||
const rects = [node.getBoundingClientRect()];
|
||||
let currentIframe = ownerIframe;
|
||||
let onlyOneMore = false;
|
||||
while (currentIframe) {
|
||||
const rect = getBoundingClientRectWithBorderOffset(currentIframe);
|
||||
rects.push(rect);
|
||||
currentIframe = getOwnerIframe(currentIframe);
|
||||
|
||||
if (onlyOneMore) {
|
||||
break;
|
||||
}
|
||||
// We don't want to calculate iframe offsets upwards beyond
|
||||
// the iframe containing the boundaryWindow, but we
|
||||
// need to calculate the offset relative to the boundaryWindow.
|
||||
if (currentIframe && getOwnerWindow(currentIframe) === boundaryWindow) {
|
||||
onlyOneMore = true;
|
||||
}
|
||||
}
|
||||
|
||||
return mergeRectOffsets(rects);
|
||||
} else {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
export function getElementDimensions(domElement: Element) {
|
||||
const calculatedStyle = window.getComputedStyle(domElement);
|
||||
return {
|
||||
borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10),
|
||||
borderRight: parseInt(calculatedStyle.borderRightWidth, 10),
|
||||
borderTop: parseInt(calculatedStyle.borderTopWidth, 10),
|
||||
borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10),
|
||||
marginLeft: parseInt(calculatedStyle.marginLeft, 10),
|
||||
marginRight: parseInt(calculatedStyle.marginRight, 10),
|
||||
marginTop: parseInt(calculatedStyle.marginTop, 10),
|
||||
marginBottom: parseInt(calculatedStyle.marginBottom, 10),
|
||||
paddingLeft: parseInt(calculatedStyle.paddingLeft, 10),
|
||||
paddingRight: parseInt(calculatedStyle.paddingRight, 10),
|
||||
paddingTop: parseInt(calculatedStyle.paddingTop, 10),
|
||||
paddingBottom: parseInt(calculatedStyle.paddingBottom, 10),
|
||||
};
|
||||
}
|
||||
@@ -70,6 +70,7 @@ type NativeStyleEditor_SetValueParams = {|
|
||||
|};
|
||||
|
||||
type BackendEvents = {|
|
||||
extensionBackendInitialized: [],
|
||||
inspectedElement: [InspectedElementPayload],
|
||||
isBackendStorageAPISupported: [boolean],
|
||||
operations: [Array<number>],
|
||||
@@ -108,6 +109,7 @@ type FrontendEvents = {|
|
||||
profilingData: [ProfilingDataBackend],
|
||||
reloadAndProfile: [boolean],
|
||||
selectFiber: [number],
|
||||
setTraceUpdatesEnabled: [boolean],
|
||||
shutdown: [],
|
||||
startInspectingNative: [],
|
||||
startProfiling: [boolean],
|
||||
|
||||
@@ -30,6 +30,9 @@ export const SESSION_STORAGE_RELOAD_AND_PROFILE_KEY =
|
||||
export const LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY =
|
||||
'React::DevTools::appendComponentStack';
|
||||
|
||||
export const LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY =
|
||||
'React::DevTools::traceUpdatesEnabled';
|
||||
|
||||
export const PROFILER_EXPORT_VERSION = 4;
|
||||
|
||||
export const CHANGE_LOG_URL =
|
||||
|
||||
+10
-1
@@ -53,6 +53,7 @@ type Config = {|
|
||||
supportsNativeInspection?: boolean,
|
||||
supportsReloadAndProfile?: boolean,
|
||||
supportsProfiling?: boolean,
|
||||
supportsTraceUpdates?: boolean,
|
||||
|};
|
||||
|
||||
export type Capabilities = {|
|
||||
@@ -125,6 +126,7 @@ export default class Store extends EventEmitter<{|
|
||||
_supportsNativeInspection: boolean = true;
|
||||
_supportsProfiling: boolean = false;
|
||||
_supportsReloadAndProfile: boolean = false;
|
||||
_supportsTraceUpdates: boolean = false;
|
||||
|
||||
_unsupportedRendererVersionDetected: boolean = false;
|
||||
|
||||
@@ -157,6 +159,7 @@ export default class Store extends EventEmitter<{|
|
||||
supportsNativeInspection,
|
||||
supportsProfiling,
|
||||
supportsReloadAndProfile,
|
||||
supportsTraceUpdates,
|
||||
} = config;
|
||||
this._supportsNativeInspection = supportsNativeInspection !== false;
|
||||
if (supportsProfiling) {
|
||||
@@ -165,6 +168,9 @@ export default class Store extends EventEmitter<{|
|
||||
if (supportsReloadAndProfile) {
|
||||
this._supportsReloadAndProfile = true;
|
||||
}
|
||||
if (supportsTraceUpdates) {
|
||||
this._supportsTraceUpdates = true;
|
||||
}
|
||||
}
|
||||
|
||||
this._bridge = bridge;
|
||||
@@ -336,7 +342,6 @@ export default class Store extends EventEmitter<{|
|
||||
get supportsProfiling(): boolean {
|
||||
return this._supportsProfiling;
|
||||
}
|
||||
|
||||
get supportsReloadAndProfile(): boolean {
|
||||
// Does the DevTools shell support reloading and eagerly injecting the renderer interface?
|
||||
// And if so, can the backend use the localStorage API?
|
||||
@@ -344,6 +349,10 @@ export default class Store extends EventEmitter<{|
|
||||
return this._supportsReloadAndProfile && this._isBackendStorageAPISupported;
|
||||
}
|
||||
|
||||
get supportsTraceUpdates(): boolean {
|
||||
return this._supportsTraceUpdates;
|
||||
}
|
||||
|
||||
get unsupportedRendererVersionDetected(): boolean {
|
||||
return this._unsupportedRendererVersionDetected;
|
||||
}
|
||||
|
||||
+33
-12
@@ -9,31 +9,33 @@
|
||||
|
||||
import React, {useContext} from 'react';
|
||||
import {SettingsContext} from './SettingsContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {CHANGE_LOG_URL} from 'react-devtools-shared/src/constants';
|
||||
|
||||
import styles from './SettingsShared.css';
|
||||
|
||||
export default function GeneralSettings(_: {||}) {
|
||||
const {
|
||||
displayDensity,
|
||||
setDisplayDensity,
|
||||
theme,
|
||||
setTheme,
|
||||
appendComponentStack,
|
||||
displayDensity,
|
||||
setAppendComponentStack,
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
setTraceUpdatesEnabled,
|
||||
theme,
|
||||
traceUpdatesEnabled,
|
||||
} = useContext(SettingsContext);
|
||||
|
||||
const updateDisplayDensity = ({currentTarget}) =>
|
||||
setDisplayDensity(currentTarget.value);
|
||||
const updateTheme = ({currentTarget}) => setTheme(currentTarget.value);
|
||||
const updateappendComponentStack = ({currentTarget}) =>
|
||||
setAppendComponentStack(currentTarget.checked);
|
||||
const {supportsTraceUpdates} = useContext(StoreContext);
|
||||
|
||||
return (
|
||||
<div className={styles.Settings}>
|
||||
<div className={styles.Setting}>
|
||||
<div className={styles.RadioLabel}>Theme</div>
|
||||
<select className={styles.Select} value={theme} onChange={updateTheme}>
|
||||
<select
|
||||
className={styles.Select}
|
||||
value={theme}
|
||||
onChange={({currentTarget}) => setTheme(currentTarget.value)}>
|
||||
<option value="auto">Auto</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
@@ -45,18 +47,37 @@ export default function GeneralSettings(_: {||}) {
|
||||
<select
|
||||
className={styles.Select}
|
||||
value={displayDensity}
|
||||
onChange={updateDisplayDensity}>
|
||||
onChange={({currentTarget}) =>
|
||||
setDisplayDensity(currentTarget.value)
|
||||
}>
|
||||
<option value="compact">Compact</option>
|
||||
<option value="comfortable">Comfortable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{supportsTraceUpdates && (
|
||||
<div className={styles.Setting}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={traceUpdatesEnabled}
|
||||
onChange={({currentTarget}) =>
|
||||
setTraceUpdatesEnabled(currentTarget.checked)
|
||||
}
|
||||
/>{' '}
|
||||
Highlight updates when components render.
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.Setting}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appendComponentStack}
|
||||
onChange={updateappendComponentStack}
|
||||
onChange={({currentTarget}) =>
|
||||
setAppendComponentStack(currentTarget.checked)
|
||||
}
|
||||
/>{' '}
|
||||
Append component stacks to console warnings and errors.
|
||||
</label>
|
||||
|
||||
+28
-10
@@ -18,6 +18,7 @@ import {
|
||||
COMFORTABLE_LINE_HEIGHT,
|
||||
COMPACT_LINE_HEIGHT,
|
||||
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
|
||||
LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
|
||||
} from 'react-devtools-shared/src/constants';
|
||||
import {useLocalStorage} from '../hooks';
|
||||
import {BridgeContext} from '../context';
|
||||
@@ -40,6 +41,9 @@ type Context = {|
|
||||
|
||||
theme: Theme,
|
||||
setTheme(value: Theme): void,
|
||||
|
||||
traceUpdatesEnabled: boolean,
|
||||
setTraceUpdatesEnabled: (value: boolean) => void,
|
||||
|};
|
||||
|
||||
const SettingsContext = createContext<Context>(((null: any): Context));
|
||||
@@ -73,6 +77,9 @@ function SettingsContextController({
|
||||
const [appendComponentStack, setAppendComponentStack] = useLocalStorage<
|
||||
boolean,
|
||||
>(LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY, true);
|
||||
const [traceUpdatesEnabled, setTraceUpdatesEnabled] = useLocalStorage<
|
||||
boolean,
|
||||
>(LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY, false);
|
||||
|
||||
const documentElements = useMemo<DocumentElements>(
|
||||
() => {
|
||||
@@ -138,26 +145,37 @@ function SettingsContextController({
|
||||
[bridge, appendComponentStack],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
|
||||
},
|
||||
[bridge, traceUpdatesEnabled],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
displayDensity,
|
||||
setDisplayDensity,
|
||||
theme,
|
||||
setTheme,
|
||||
appendComponentStack,
|
||||
setAppendComponentStack,
|
||||
displayDensity,
|
||||
lineHeight:
|
||||
displayDensity === 'compact'
|
||||
? COMPACT_LINE_HEIGHT
|
||||
: COMFORTABLE_LINE_HEIGHT,
|
||||
}),
|
||||
[
|
||||
displayDensity,
|
||||
setAppendComponentStack,
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
appendComponentStack,
|
||||
setAppendComponentStack,
|
||||
setTraceUpdatesEnabled,
|
||||
theme,
|
||||
traceUpdatesEnabled,
|
||||
}),
|
||||
[
|
||||
appendComponentStack,
|
||||
displayDensity,
|
||||
setAppendComponentStack,
|
||||
setDisplayDensity,
|
||||
setTheme,
|
||||
setTraceUpdatesEnabled,
|
||||
theme,
|
||||
traceUpdatesEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
<!-- Upcoming changes go here -->
|
||||
|
||||
#### Features
|
||||
* "Highlight updates" feature added for browser extensions and `react-devtools-inline` NPM package. ([bvaughn](https://github.com/bvaughn) in [#16989](https://github.com/facebook/react/pull/16989))
|
||||
</details>
|
||||
|
||||
## 4.1.3 (September 30, 2019)
|
||||
|
||||
Reference in New Issue
Block a user