mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[DevTools] Name root "Transition" when focusing on Activity (#35108)
This commit is contained in:
committed by
GitHub
parent
7f1a085b28
commit
194c12d949
@@ -189,6 +189,8 @@ export default class Store extends EventEmitter<{
|
||||
{errorCount: number, warningCount: number},
|
||||
> = new Map();
|
||||
|
||||
_focusedTransition: 0 | Element['id'] = 0;
|
||||
|
||||
// At least one of the injected renderers contains (DEV only) owner metadata.
|
||||
_hasOwnerMetadata: boolean = false;
|
||||
|
||||
@@ -935,10 +937,9 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rootID
|
||||
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
||||
*/
|
||||
getSuspendableDocumentOrderSuspense(
|
||||
getSuspendableDocumentOrderSuspenseInitialPaint(
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): Array<SuspenseTimelineStep> {
|
||||
const target: Array<SuspenseTimelineStep> = [];
|
||||
@@ -990,6 +991,76 @@ export default class Store extends EventEmitter<{
|
||||
return target;
|
||||
}
|
||||
|
||||
_pushSuspenseChildrenInDocumentOrder(
|
||||
children: Array<Element['id']>,
|
||||
target: Array<SuspenseNode['id']>,
|
||||
): void {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const childID = children[i];
|
||||
const suspense = this.getSuspenseByID(childID);
|
||||
if (suspense !== null) {
|
||||
target.push(suspense.id);
|
||||
} else {
|
||||
const childElement = this.getElementByID(childID);
|
||||
if (childElement !== null) {
|
||||
this._pushSuspenseChildrenInDocumentOrder(
|
||||
childElement.children,
|
||||
target,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSuspenseChildren(id: Element['id']): Array<SuspenseNode['id']> {
|
||||
const transitionChildren: Array<SuspenseNode['id']> = [];
|
||||
|
||||
const root = this._idToElement.get(id);
|
||||
if (root === undefined) {
|
||||
return transitionChildren;
|
||||
}
|
||||
|
||||
this._pushSuspenseChildrenInDocumentOrder(
|
||||
root.children,
|
||||
transitionChildren,
|
||||
);
|
||||
|
||||
return transitionChildren;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
||||
*/
|
||||
getSuspendableDocumentOrderSuspenseTransition(
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): Array<SuspenseTimelineStep> {
|
||||
const target: Array<SuspenseTimelineStep> = [];
|
||||
const focusedTransitionID = this._focusedTransition;
|
||||
if (focusedTransitionID === null) {
|
||||
return target;
|
||||
}
|
||||
|
||||
target.push({
|
||||
id: focusedTransitionID,
|
||||
// TODO: Get environment for Activity
|
||||
environment: null,
|
||||
endTime: 0,
|
||||
});
|
||||
|
||||
const transitionChildren = this.getSuspenseChildren(focusedTransitionID);
|
||||
|
||||
this.pushTimelineStepsInDocumentOrder(
|
||||
transitionChildren,
|
||||
target,
|
||||
uniqueSuspendersOnly,
|
||||
// TODO: Get environment for Activity
|
||||
[],
|
||||
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
|
||||
);
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
pushTimelineStepsInDocumentOrder(
|
||||
children: Array<SuspenseNode['id']>,
|
||||
target: Array<SuspenseTimelineStep>,
|
||||
@@ -1045,7 +1116,14 @@ export default class Store extends EventEmitter<{
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseTimelineStep> {
|
||||
const timeline =
|
||||
this.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||
this._focusedTransition === 0
|
||||
? this.getSuspendableDocumentOrderSuspenseInitialPaint(
|
||||
uniqueSuspendersOnly,
|
||||
)
|
||||
: this.getSuspendableDocumentOrderSuspenseTransition(
|
||||
uniqueSuspendersOnly,
|
||||
);
|
||||
|
||||
if (timeline.length === 0) {
|
||||
return timeline;
|
||||
}
|
||||
@@ -1271,7 +1349,7 @@ export default class Store extends EventEmitter<{
|
||||
const removedElementIDs: Map<number, number> = new Map();
|
||||
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
|
||||
new Map();
|
||||
let nextActivitySliceID = null;
|
||||
let nextActivitySliceID: Element['id'] | null = null;
|
||||
|
||||
let i = 2;
|
||||
|
||||
@@ -2146,6 +2224,10 @@ export default class Store extends EventEmitter<{
|
||||
}
|
||||
}
|
||||
|
||||
if (nextActivitySliceID !== null) {
|
||||
this._focusedTransition = nextActivitySliceID;
|
||||
}
|
||||
|
||||
this.emit('mutated', [
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
|
||||
@@ -12,7 +12,10 @@ import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/Syntheti
|
||||
|
||||
import * as React from 'react';
|
||||
import {useContext} from 'react';
|
||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
||||
import {
|
||||
TreeDispatcherContext,
|
||||
TreeStateContext,
|
||||
} from '../Components/TreeContext';
|
||||
import {StoreContext} from '../context';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import styles from './SuspenseBreadcrumbs.css';
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
|
||||
export default function SuspenseBreadcrumbs(): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const {activityID} = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
const {selectedSuspenseID, lineage, roots} = useContext(
|
||||
@@ -42,8 +46,8 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
||||
<ol className={styles.SuspenseBreadcrumbsList}>
|
||||
{lineage === null ? null : lineage.length === 0 ? (
|
||||
// We selected the root. This means that we're currently viewing the Transition
|
||||
// that rendered the whole screen. In laymans terms this is really "Initial Paint".
|
||||
// TODO: Once we add subtree selection, then the equivalent should be called
|
||||
// that rendered the whole screen. In laymans terms this is really "Initial Paint" .
|
||||
// When we're looking at a subtree selection, then the equivalent is a
|
||||
// "Transition" since in that case it's really about a Transition within the page.
|
||||
roots.length > 0 ? (
|
||||
<li
|
||||
@@ -51,9 +55,12 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
||||
aria-current="true">
|
||||
<button
|
||||
className={styles.SuspenseBreadcrumbsButton}
|
||||
onClick={handleClick.bind(null, roots[0])}
|
||||
onClick={handleClick.bind(
|
||||
null,
|
||||
activityID === null ? roots[0] : activityID,
|
||||
)}
|
||||
type="button">
|
||||
Initial Paint
|
||||
{activityID === null ? 'Initial Paint' : 'Transition'}
|
||||
</button>
|
||||
</li>
|
||||
) : null
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import type Store from 'react-devtools-shared/src/devtools/store';
|
||||
import type {
|
||||
Element,
|
||||
SuspenseNode,
|
||||
Rect,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
@@ -18,7 +19,7 @@ import typeof {
|
||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
import * as React from 'react';
|
||||
import {createContext, useContext, useLayoutEffect} from 'react';
|
||||
import {createContext, useContext, useLayoutEffect, useMemo} from 'react';
|
||||
import {
|
||||
TreeDispatcherContext,
|
||||
TreeStateContext,
|
||||
@@ -426,6 +427,30 @@ function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
|
||||
});
|
||||
}
|
||||
|
||||
function SuspenseRectsInitialPaint(): React$Node {
|
||||
const {roots} = useContext(SuspenseTreeStateContext);
|
||||
return roots.map(rootID => {
|
||||
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
|
||||
});
|
||||
}
|
||||
|
||||
function SuspenseRectsTransition({id}: {id: Element['id']}): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const children = useMemo(() => {
|
||||
return store.getSuspenseChildren(id);
|
||||
}, [id, store]);
|
||||
|
||||
return children.map(suspenseID => {
|
||||
return (
|
||||
<SuspenseRects
|
||||
key={suspenseID}
|
||||
suspenseID={suspenseID}
|
||||
parentRects={null}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const ViewBox = createContext<Rect>((null: any));
|
||||
|
||||
function SuspenseRectsContainer({
|
||||
@@ -434,14 +459,25 @@ function SuspenseRectsContainer({
|
||||
scaleRef: {current: number},
|
||||
}): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const {inspectedElementID} = useContext(TreeStateContext);
|
||||
const {activityID, inspectedElementID} = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
||||
const {roots, timeline, hoveredTimelineIndex, uniqueSuspendersOnly} =
|
||||
useContext(SuspenseTreeStateContext);
|
||||
|
||||
// TODO: bbox does not consider uniqueSuspendersOnly filter
|
||||
const activityChildren: $ReadOnlyArray<SuspenseNode['id']> | null =
|
||||
useMemo(() => {
|
||||
if (activityID === null) {
|
||||
return null;
|
||||
}
|
||||
return store.getSuspenseChildren(activityID);
|
||||
}, [activityID, store]);
|
||||
const transitionChildren =
|
||||
activityChildren === null ? roots : activityChildren;
|
||||
|
||||
// We're using the bounding box of the entire document to anchor the Transition
|
||||
// in the actual document.
|
||||
const boundingBox = getDocumentBoundingRect(store, roots);
|
||||
|
||||
const boundingBoxWidth = boundingBox.width;
|
||||
@@ -456,14 +492,18 @@ function SuspenseRectsContainer({
|
||||
// Already clicked on an inner rect
|
||||
return;
|
||||
}
|
||||
if (roots.length === 0) {
|
||||
if (transitionChildren.length === 0) {
|
||||
// Nothing to select
|
||||
return;
|
||||
}
|
||||
const arbitraryRootID = roots[0];
|
||||
const transitionRoot = activityID === null ? arbitraryRootID : activityID;
|
||||
|
||||
event.preventDefault();
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: arbitraryRootID});
|
||||
treeDispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: transitionRoot,
|
||||
});
|
||||
suspenseTreeDispatch({
|
||||
type: 'SET_SUSPENSE_LINEAGE',
|
||||
payload: arbitraryRootID,
|
||||
@@ -483,7 +523,8 @@ function SuspenseRectsContainer({
|
||||
}
|
||||
|
||||
const isRootSelected = roots.includes(inspectedElementID);
|
||||
const isRootHovered = hoveredTimelineIndex === 0;
|
||||
// When we're focusing a Transition, the first timeline step will not be a root.
|
||||
const isRootHovered = activityID === null && hoveredTimelineIndex === 0;
|
||||
|
||||
let hasRootSuspenders = false;
|
||||
if (!uniqueSuspendersOnly) {
|
||||
@@ -536,7 +577,13 @@ function SuspenseRectsContainer({
|
||||
<div
|
||||
className={
|
||||
styles.SuspenseRectsContainer +
|
||||
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
|
||||
(hasRootSuspenders &&
|
||||
// We don't want to draw attention to the root if we're looking at a Transition.
|
||||
// TODO: Draw bounding rect of Transition and check if the Transition
|
||||
// has unique suspenders.
|
||||
activityID === null
|
||||
? ' ' + styles.SuspenseRectsRoot
|
||||
: '') +
|
||||
(isRootSelected ? ' ' + styles.SuspenseRectsRootOutline : '') +
|
||||
' ' +
|
||||
getClassNameForEnvironment(rootEnvironment)
|
||||
@@ -548,9 +595,11 @@ function SuspenseRectsContainer({
|
||||
<div
|
||||
className={styles.SuspenseRectsViewBox}
|
||||
style={{aspectRatio, width}}>
|
||||
{roots.map(rootID => {
|
||||
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
|
||||
})}
|
||||
{activityID === null ? (
|
||||
<SuspenseRectsInitialPaint />
|
||||
) : (
|
||||
<SuspenseRectsTransition id={activityID} />
|
||||
)}
|
||||
{selectedBoundingBox !== null ? (
|
||||
<ScaledRect
|
||||
className={
|
||||
|
||||
@@ -12,13 +12,15 @@ import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/type
|
||||
import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
import * as React from 'react';
|
||||
import {useRef} from 'react';
|
||||
import {useContext, useRef} from 'react';
|
||||
import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
import styles from './SuspenseScrubber.css';
|
||||
|
||||
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
|
||||
|
||||
import Tooltip from '../Components/reach-ui/tooltip';
|
||||
import {StoreContext} from '../context';
|
||||
|
||||
export default function SuspenseScrubber({
|
||||
min,
|
||||
@@ -43,6 +45,7 @@ export default function SuspenseScrubber({
|
||||
onHoverSegment: (index: number) => void,
|
||||
onHoverLeave: () => void,
|
||||
}): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const inputRef = useRef();
|
||||
function handleChange(event: SyntheticEvent) {
|
||||
const newValue = +event.currentTarget.value;
|
||||
@@ -60,12 +63,16 @@ export default function SuspenseScrubber({
|
||||
}
|
||||
const steps = [];
|
||||
for (let index = min; index <= max; index++) {
|
||||
const environment = timeline[index].environment;
|
||||
const step = timeline[index];
|
||||
const environment = step.environment;
|
||||
const element = store.getElementByID(step.id);
|
||||
const label =
|
||||
index === min
|
||||
? // The first step in the timeline is always a Transition (Initial Paint).
|
||||
'Initial Paint' +
|
||||
(environment === null ? '' : ' (' + environment + ')')
|
||||
element === null || element.type === ElementTypeRoot
|
||||
? 'Initial Paint'
|
||||
: 'Transition' +
|
||||
(environment === null ? '' : ' (' + environment + ')')
|
||||
: // TODO: Consider adding the name of this specific boundary if this step has only one.
|
||||
environment === null
|
||||
? 'Suspense'
|
||||
|
||||
@@ -204,7 +204,11 @@ export type Rect = {
|
||||
};
|
||||
|
||||
export type SuspenseTimelineStep = {
|
||||
id: SuspenseNode['id'], // TODO: Will become a group.
|
||||
/**
|
||||
* The first step is either a host root (initial paint) or Activity (Transition).
|
||||
* Subsequent steps are always Suspense nodes.
|
||||
*/
|
||||
id: SuspenseNode['id'] | Element['id'], // TODO: Will become a group.
|
||||
environment: null | string,
|
||||
endTime: number,
|
||||
};
|
||||
|
||||
@@ -75,22 +75,26 @@ function Root({children}: {children: React.Node}): React.Node {
|
||||
);
|
||||
}
|
||||
|
||||
const dynamicData = deferred(10, 'Dynamic Data: 📈📉📊', 'dynamicData');
|
||||
export default function Segments(): React.Node {
|
||||
return (
|
||||
<React.Activity name="root" mode="visible">
|
||||
<Root>
|
||||
<React.Activity name="outer" mode="visible">
|
||||
<OuterSegment>
|
||||
<React.Activity name="inner" mode="visible">
|
||||
<InnerSegment>
|
||||
<React.Activity name="slot" mode="visible">
|
||||
<Page />
|
||||
</React.Activity>
|
||||
</InnerSegment>
|
||||
</React.Activity>
|
||||
</OuterSegment>
|
||||
</React.Activity>
|
||||
</Root>
|
||||
</React.Activity>
|
||||
<>
|
||||
<p>{dynamicData}</p>
|
||||
<React.Activity name="root" mode="visible">
|
||||
<Root>
|
||||
<React.Activity name="outer" mode="visible">
|
||||
<OuterSegment>
|
||||
<React.Activity name="inner" mode="visible">
|
||||
<InnerSegment>
|
||||
<React.Activity name="slot" mode="visible">
|
||||
<Page />
|
||||
</React.Activity>
|
||||
</InnerSegment>
|
||||
</React.Activity>
|
||||
</OuterSegment>
|
||||
</React.Activity>
|
||||
</Root>
|
||||
</React.Activity>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user