Skip to content

Commit 7f1a085

Browse files
authored
[DevTools] Show list of named Activities in Suspense tab (facebook#35092)
1 parent ea4899e commit 7f1a085

File tree

7 files changed

+221
-99
lines changed

7 files changed

+221
-99
lines changed

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,33 @@ export default class Store extends EventEmitter<{
10581058
return timeline;
10591059
}
10601060

1061+
getActivities(): Array<{id: Element['id'], depth: number}> {
1062+
const target: Array<{id: Element['id'], depth: number}> = [];
1063+
// TODO: Keep a live tree in the backend so we don't need to recalculate
1064+
// this each time while also including filtered Activities.
1065+
this._pushActivitiesInDocumentOrder(this.roots, target, 0);
1066+
return target;
1067+
}
1068+
1069+
_pushActivitiesInDocumentOrder(
1070+
children: $ReadOnlyArray<Element['id']>,
1071+
target: Array<{id: Element['id'], depth: number}>,
1072+
depth: number,
1073+
): void {
1074+
for (let i = 0; i < children.length; i++) {
1075+
const child = this._idToElement.get(children[i]);
1076+
if (child === undefined) {
1077+
continue;
1078+
}
1079+
if (child.type === ElementTypeActivity && child.nameProp !== null) {
1080+
target.push({id: child.id, depth});
1081+
this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);
1082+
} else {
1083+
this._pushActivitiesInDocumentOrder(child.children, target, depth);
1084+
}
1085+
}
1086+
}
1087+
10611088
getRendererIDForElement(id: number): number | null {
10621089
let current = this._idToElement.get(id);
10631090
while (current !== undefined) {

packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export type StateContext = {
5959

6060
// Activity slice
6161
activityID: Element['id'] | null,
62+
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
6263

6364
// Inspection element panel
6465
inspectedElementID: number | null,
@@ -172,6 +173,7 @@ type State = {
172173

173174
// Activity slice
174175
activityID: Element['id'] | null,
176+
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
175177

176178
// Inspection element panel
177179
inspectedElementID: number | null,
@@ -809,6 +811,7 @@ function reduceActivityState(
809811
case 'HANDLE_STORE_MUTATION':
810812
let {activityID} = state;
811813
const [, , activitySliceIDChange] = action.payload;
814+
const activities = store.getActivities();
812815
if (activitySliceIDChange === 0 && activityID !== null) {
813816
activityID = null;
814817
} else if (
@@ -817,10 +820,11 @@ function reduceActivityState(
817820
) {
818821
activityID = activitySliceIDChange;
819822
}
820-
if (activityID !== state.activityID) {
823+
if (activityID !== state.activityID || activities !== state.activities) {
821824
return {
822825
...state,
823826
activityID,
827+
activities,
824828
};
825829
}
826830
}
@@ -863,6 +867,7 @@ function getInitialState({
863867

864868
// Activity slice
865869
activityID: null,
870+
activities: store.getActivities(),
866871

867872
// Inspection element panel
868873
inspectedElementID:

packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.css

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
1-
.ActivityList {
1+
.ActivityListContaier {
2+
display: flex;
3+
flex-direction: column;
4+
}
5+
6+
.ActivityListHeader {
7+
/* even if empty, provides layout alignment with the main view */
8+
display: flex;
9+
flex: 0 0 42px;
10+
border-bottom: 1px solid var(--color-border);
11+
}
12+
13+
.ActivityListList {
214
cursor: default;
315
list-style-type: none;
416
margin: 0;
517
padding: 0;
618
}
719

8-
.ActivityList[data-pending-activity-slice-selection="true"] {
20+
.ActivityListList[data-pending-activity-slice-selection="true"] {
921
cursor: wait;
1022
}
1123

12-
.ActivityList:focus {
24+
.ActivityListList:focus {
1325
outline: none;
1426
}
1527

1628
.ActivityListItem {
1729
color: var(--color-component-name);
30+
line-height: var(--line-height-data);
1831
padding: 0 0.25rem;
1932
user-select: none;
2033
}
@@ -27,7 +40,7 @@
2740
background-color: var(--color-background-inactive);
2841
}
2942

30-
.ActivityList:focus .ActivityListItem[aria-selected="true"] {
43+
.ActivityListList:focus .ActivityListItem[aria-selected="true"] {
3144
background-color: var(--color-background-selected);
3245
color: var(--color-text-selected);
3346

packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js

Lines changed: 108 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@ import typeof {
1515
SyntheticMouseEvent,
1616
SyntheticKeyboardEvent,
1717
} from 'react-dom-bindings/src/events/SyntheticEvent';
18+
import type Store from 'react-devtools-shared/src/devtools/store';
1819

1920
import * as React from 'react';
20-
import {useContext, useTransition} from 'react';
21-
import {ComponentFilterActivitySlice} from 'react-devtools-shared/src/frontend/types';
21+
import {useContext, useMemo, useTransition} from 'react';
22+
import {
23+
ComponentFilterActivitySlice,
24+
ElementTypeActivity,
25+
} from 'react-devtools-shared/src/frontend/types';
2226
import styles from './ActivityList.css';
2327
import {
2428
TreeStateContext,
2529
TreeDispatcherContext,
2630
} from '../Components/TreeContext';
2731
import {useHighlightHostInstance} from '../hooks';
2832
import {StoreContext} from '../context';
33+
import ButtonIcon from '../ButtonIcon';
34+
import Button from '../Button';
2935

3036
export function useChangeActivitySliceAction(): (
3137
id: Element['id'] | null,
@@ -62,15 +68,49 @@ export function useChangeActivitySliceAction(): (
6268
return changeActivitySliceAction;
6369
}
6470

71+
function findNearestActivityParentID(
72+
elementID: Element['id'],
73+
store: Store,
74+
): Element['id'] | null {
75+
let currentID: null | Element['id'] = elementID;
76+
while (currentID !== null) {
77+
const element = store.getElementByID(currentID);
78+
if (element === null) {
79+
return null;
80+
}
81+
if (element.type === ElementTypeActivity) {
82+
return element.id;
83+
}
84+
currentID = element.parentID;
85+
}
86+
87+
return currentID;
88+
}
89+
90+
function useSelectedActivityID(): Element['id'] | null {
91+
const {inspectedElementID} = useContext(TreeStateContext);
92+
const store = useContext(StoreContext);
93+
return useMemo(() => {
94+
if (inspectedElementID === null) {
95+
return null;
96+
}
97+
const nearestActivityID = findNearestActivityParentID(
98+
inspectedElementID,
99+
store,
100+
);
101+
return nearestActivityID;
102+
}, [inspectedElementID, store]);
103+
}
104+
65105
export default function ActivityList({
66106
activities,
67107
}: {
68-
activities: $ReadOnlyArray<Element>,
108+
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
69109
}): React$Node {
70-
const {inspectedElementID} = useContext(TreeStateContext);
110+
const {activityID, inspectedElementID} = useContext(TreeStateContext);
71111
const treeDispatch = useContext(TreeDispatcherContext);
72-
// TODO: Derive from inspected element
73-
const selectedActivityID = inspectedElementID;
112+
const store = useContext(StoreContext);
113+
const selectedActivityID = useSelectedActivityID();
74114
const {highlightHostInstance, clearHighlightHostInstance} =
75115
useHighlightHostInstance();
76116

@@ -79,8 +119,13 @@ export default function ActivityList({
79119
const changeActivitySliceAction = useChangeActivitySliceAction();
80120

81121
function handleKeyDown(event: SyntheticKeyboardEvent) {
82-
// TODO: Implement keyboard navigation
83122
switch (event.key) {
123+
case 'Escape':
124+
startActivitySliceSelection(() => {
125+
changeActivitySliceAction(null);
126+
});
127+
event.preventDefault();
128+
break;
84129
case 'Enter':
85130
case ' ':
86131
if (inspectedElementID !== null) {
@@ -149,25 +194,61 @@ export default function ActivityList({
149194
}
150195

151196
return (
152-
<ol
153-
role="listbox"
154-
className={styles.ActivityList}
155-
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
156-
tabIndex={0}
157-
onKeyDown={handleKeyDown}>
158-
{activities.map(activity => (
159-
<li
160-
key={activity.id}
161-
role="option"
162-
aria-selected={activity.id === selectedActivityID ? 'true' : 'false'}
163-
className={styles.ActivityListItem}
164-
onClick={handleClick.bind(null, activity.id)}
165-
onDoubleClick={handleDoubleClick}
166-
onPointerOver={highlightHostInstance.bind(null, activity.id, false)}
167-
onPointerLeave={clearHighlightHostInstance}>
168-
{activity.nameProp}
169-
</li>
170-
))}
171-
</ol>
197+
<div className={styles.ActivityListContaier}>
198+
<div className={styles.ActivityListHeader}>
199+
{activityID !== null && (
200+
// TODO: Obsolete once filtered Activities are included in this list.
201+
<Button
202+
onClick={startActivitySliceSelection.bind(
203+
null,
204+
changeActivitySliceAction.bind(null, null),
205+
)}
206+
title="Back to full tree view">
207+
<ButtonIcon type="previous" />
208+
</Button>
209+
)}
210+
</div>
211+
<ol
212+
role="listbox"
213+
className={styles.ActivityListList}
214+
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
215+
tabIndex={0}
216+
onKeyDown={handleKeyDown}>
217+
{activities.map(({id, depth}) => {
218+
const activity = store.getElementByID(id);
219+
if (activity === null) {
220+
return null;
221+
}
222+
const name = activity.nameProp;
223+
if (name === null) {
224+
// This shouldn't actually happen. We only want to show activities with a name.
225+
// And hide the whole list if no named Activities are present.
226+
return null;
227+
}
228+
229+
// TODO: Filtered Activities should have dedicated styles once we include
230+
// filtered Activities in this list.
231+
return (
232+
<li
233+
key={activity.id}
234+
role="option"
235+
aria-selected={
236+
activity.id === selectedActivityID ? 'true' : 'false'
237+
}
238+
className={styles.ActivityListItem}
239+
onClick={handleClick.bind(null, activity.id)}
240+
onDoubleClick={handleDoubleClick}
241+
onPointerOver={highlightHostInstance.bind(
242+
null,
243+
activity.id,
244+
false,
245+
)}
246+
onPointerLeave={clearHighlightHostInstance}>
247+
{'\u00A0'.repeat(depth) + name}
248+
</li>
249+
);
250+
})}
251+
</ol>
252+
</div>
172253
);
173254
}

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
}
9393

9494
.ActivityList {
95-
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
95+
flex: 0 0 var(--horizontal-resize-activity-list-percentage);;
9696
border-right: 1px solid var(--color-border);
9797
overflow: auto;
9898
}

0 commit comments

Comments
 (0)