Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 86 additions & 4 deletions packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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> = [];
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -2146,6 +2224,10 @@ export default class Store extends EventEmitter<{
}
}

if (nextActivitySliceID !== null) {
this._focusedTransition = nextActivitySliceID;
}

this.emit('mutated', [
addedElementIDs,
removedElementIDs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
Expand All @@ -42,18 +46,21 @@ 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
className={styles.SuspenseBreadcrumbsListItem}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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({
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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'
Expand Down
6 changes: 5 additions & 1 deletion packages/react-devtools-shared/src/frontend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Loading
Loading