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
27 changes: 27 additions & 0 deletions packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,33 @@ export default class Store extends EventEmitter<{
return timeline;
}

getActivities(): Array<{id: Element['id'], depth: number}> {
const target: Array<{id: Element['id'], depth: number}> = [];
// TODO: Keep a live tree in the backend so we don't need to recalculate
// this each time while also including filtered Activities.
this._pushActivitiesInDocumentOrder(this.roots, target, 0);
return target;
}

_pushActivitiesInDocumentOrder(
children: $ReadOnlyArray<Element['id']>,
target: Array<{id: Element['id'], depth: number}>,
depth: number,
): void {
for (let i = 0; i < children.length; i++) {
const child = this._idToElement.get(children[i]);
if (child === undefined) {
continue;
}
if (child.type === ElementTypeActivity && child.nameProp !== null) {
target.push({id: child.id, depth});
this._pushActivitiesInDocumentOrder(child.children, target, depth + 1);
} else {
this._pushActivitiesInDocumentOrder(child.children, target, depth);
}
}
}

getRendererIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type StateContext = {

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

// Inspection element panel
inspectedElementID: number | null,
Expand Down Expand Up @@ -172,6 +173,7 @@ type State = {

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

// Inspection element panel
inspectedElementID: number | null,
Expand Down Expand Up @@ -809,6 +811,7 @@ function reduceActivityState(
case 'HANDLE_STORE_MUTATION':
let {activityID} = state;
const [, , activitySliceIDChange] = action.payload;
const activities = store.getActivities();
if (activitySliceIDChange === 0 && activityID !== null) {
activityID = null;
} else if (
Expand All @@ -817,10 +820,11 @@ function reduceActivityState(
) {
activityID = activitySliceIDChange;
}
if (activityID !== state.activityID) {
if (activityID !== state.activityID || activities !== state.activities) {
return {
...state,
activityID,
activities,
};
}
}
Expand Down Expand Up @@ -863,6 +867,7 @@ function getInitialState({

// Activity slice
activityID: null,
activities: store.getActivities(),

// Inspection element panel
inspectedElementID:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
.ActivityList {
.ActivityListContaier {
display: flex;
flex-direction: column;
}

.ActivityListHeader {
/* even if empty, provides layout alignment with the main view */
display: flex;
flex: 0 0 42px;
border-bottom: 1px solid var(--color-border);
}

.ActivityListList {
cursor: default;
list-style-type: none;
margin: 0;
padding: 0;
}

.ActivityList[data-pending-activity-slice-selection="true"] {
.ActivityListList[data-pending-activity-slice-selection="true"] {
cursor: wait;
}

.ActivityList:focus {
.ActivityListList:focus {
outline: none;
}

.ActivityListItem {
color: var(--color-component-name);
line-height: var(--line-height-data);
padding: 0 0.25rem;
user-select: none;
}
Expand All @@ -27,7 +40,7 @@
background-color: var(--color-background-inactive);
}

.ActivityList:focus .ActivityListItem[aria-selected="true"] {
.ActivityListList:focus .ActivityListItem[aria-selected="true"] {
background-color: var(--color-background-selected);
color: var(--color-text-selected);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@ import typeof {
SyntheticMouseEvent,
SyntheticKeyboardEvent,
} from 'react-dom-bindings/src/events/SyntheticEvent';
import type Store from 'react-devtools-shared/src/devtools/store';

import * as React from 'react';
import {useContext, useTransition} from 'react';
import {ComponentFilterActivitySlice} from 'react-devtools-shared/src/frontend/types';
import {useContext, useMemo, useTransition} from 'react';
import {
ComponentFilterActivitySlice,
ElementTypeActivity,
} from 'react-devtools-shared/src/frontend/types';
import styles from './ActivityList.css';
import {
TreeStateContext,
TreeDispatcherContext,
} from '../Components/TreeContext';
import {useHighlightHostInstance} from '../hooks';
import {StoreContext} from '../context';
import ButtonIcon from '../ButtonIcon';
import Button from '../Button';

export function useChangeActivitySliceAction(): (
id: Element['id'] | null,
Expand Down Expand Up @@ -62,15 +68,49 @@ export function useChangeActivitySliceAction(): (
return changeActivitySliceAction;
}

function findNearestActivityParentID(
elementID: Element['id'],
store: Store,
): Element['id'] | null {
let currentID: null | Element['id'] = elementID;
while (currentID !== null) {
const element = store.getElementByID(currentID);
if (element === null) {
return null;
}
if (element.type === ElementTypeActivity) {
return element.id;
}
currentID = element.parentID;
}

return currentID;
}

function useSelectedActivityID(): Element['id'] | null {
const {inspectedElementID} = useContext(TreeStateContext);
const store = useContext(StoreContext);
return useMemo(() => {
if (inspectedElementID === null) {
return null;
}
const nearestActivityID = findNearestActivityParentID(
inspectedElementID,
store,
);
return nearestActivityID;
}, [inspectedElementID, store]);
}

export default function ActivityList({
activities,
}: {
activities: $ReadOnlyArray<Element>,
activities: $ReadOnlyArray<{id: Element['id'], depth: number}>,
}): React$Node {
const {inspectedElementID} = useContext(TreeStateContext);
const {activityID, inspectedElementID} = useContext(TreeStateContext);
const treeDispatch = useContext(TreeDispatcherContext);
// TODO: Derive from inspected element
const selectedActivityID = inspectedElementID;
const store = useContext(StoreContext);
const selectedActivityID = useSelectedActivityID();
const {highlightHostInstance, clearHighlightHostInstance} =
useHighlightHostInstance();

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

function handleKeyDown(event: SyntheticKeyboardEvent) {
// TODO: Implement keyboard navigation
switch (event.key) {
case 'Escape':
startActivitySliceSelection(() => {
changeActivitySliceAction(null);
});
event.preventDefault();
break;
case 'Enter':
case ' ':
if (inspectedElementID !== null) {
Expand Down Expand Up @@ -149,25 +194,61 @@ export default function ActivityList({
}

return (
<ol
role="listbox"
className={styles.ActivityList}
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
tabIndex={0}
onKeyDown={handleKeyDown}>
{activities.map(activity => (
<li
key={activity.id}
role="option"
aria-selected={activity.id === selectedActivityID ? 'true' : 'false'}
className={styles.ActivityListItem}
onClick={handleClick.bind(null, activity.id)}
onDoubleClick={handleDoubleClick}
onPointerOver={highlightHostInstance.bind(null, activity.id, false)}
onPointerLeave={clearHighlightHostInstance}>
{activity.nameProp}
</li>
))}
</ol>
<div className={styles.ActivityListContaier}>
<div className={styles.ActivityListHeader}>
{activityID !== null && (
// TODO: Obsolete once filtered Activities are included in this list.
<Button
onClick={startActivitySliceSelection.bind(
null,
changeActivitySliceAction.bind(null, null),
)}
title="Back to full tree view">
<ButtonIcon type="previous" />
</Button>
)}
</div>
<ol
role="listbox"
className={styles.ActivityListList}
data-pending-activity-slice-selection={isPendingActivitySliceSelection}
tabIndex={0}
onKeyDown={handleKeyDown}>
{activities.map(({id, depth}) => {
const activity = store.getElementByID(id);
if (activity === null) {
return null;
}
const name = activity.nameProp;
if (name === null) {
// This shouldn't actually happen. We only want to show activities with a name.
// And hide the whole list if no named Activities are present.
return null;
}

// TODO: Filtered Activities should have dedicated styles once we include
// filtered Activities in this list.
return (
<li
key={activity.id}
role="option"
aria-selected={
activity.id === selectedActivityID ? 'true' : 'false'
}
className={styles.ActivityListItem}
onClick={handleClick.bind(null, activity.id)}
onDoubleClick={handleDoubleClick}
onPointerOver={highlightHostInstance.bind(
null,
activity.id,
false,
)}
onPointerLeave={clearHighlightHostInstance}>
{'\u00A0'.repeat(depth) + name}
</li>
);
})}
</ol>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
}

.ActivityList {
flex: 0 0 var(--horizontal-resize-tree-list-percentage);
flex: 0 0 var(--horizontal-resize-activity-list-percentage);;
border-right: 1px solid var(--color-border);
overflow: auto;
}
Expand Down
Loading
Loading