From 59bb5e2c57b36c9c89675347b1d701533a73a9b4 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 18 Nov 2025 10:12:54 +0100 Subject: [PATCH 1/2] [DevTools] Move "Back to full tree view" into Activity list item --- .../views/SuspenseTab/ActivityList.js | 85 ++++++++++--------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js index 3a6935b4d00..acef66d3eda 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/ActivityList.js @@ -30,8 +30,6 @@ import { } 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, @@ -118,6 +116,8 @@ export default function ActivityList({ useTransition(); const changeActivitySliceAction = useChangeActivitySliceAction(); + const includeAllOption = activityID !== null; + function handleKeyDown(event: SyntheticKeyboardEvent) { switch (event.key) { case 'Escape': @@ -128,15 +128,16 @@ export default function ActivityList({ break; case 'Enter': case ' ': - if (inspectedElementID !== null) { - startActivitySliceSelection(() => { - changeActivitySliceAction(inspectedElementID); - }); - } + startActivitySliceSelection(() => { + changeActivitySliceAction(inspectedElementID); + }); event.preventDefault(); break; case 'Home': - treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: activities[0].id}); + treeDispatch({ + type: 'SELECT_ELEMENT_BY_ID', + payload: includeAllOption ? null : activities[0].id, + }); event.preventDefault(); break; case 'End': @@ -150,15 +151,21 @@ export default function ActivityList({ const currentIndex = activities.findIndex( activity => activity.id === selectedActivityID, ); - if (currentIndex !== undefined) { - const nextIndex = - (currentIndex + activities.length - 1) % activities.length; - - treeDispatch({ - type: 'SELECT_ELEMENT_BY_ID', - payload: activities[nextIndex].id, - }); + let nextIndex: number; + if (currentIndex === -1) { + // Currently selecting "All", wrap around to last Activity. + nextIndex = activities.length - 1; + } else { + nextIndex = currentIndex - 1; + if (!includeAllOption) { + nextIndex = (nextIndex + activities.length) % activities.length; + } } + + treeDispatch({ + type: 'SELECT_ELEMENT_BY_ID', + payload: nextIndex === -1 ? null : activities[nextIndex].id, + }); event.preventDefault(); break; } @@ -166,14 +173,17 @@ export default function ActivityList({ const currentIndex = activities.findIndex( activity => activity.id === selectedActivityID, ); - if (currentIndex !== undefined) { - const nextIndex = (currentIndex + 1) % activities.length; - - treeDispatch({ - type: 'SELECT_ELEMENT_BY_ID', - payload: activities[nextIndex].id, - }); + let nextIndex: number; + if (includeAllOption && currentIndex === activities.length - 1) { + // Currently selecting last Activity, wrap around to "All". + nextIndex = -1; + } else { + nextIndex = (currentIndex + 1) % activities.length; } + treeDispatch({ + type: 'SELECT_ELEMENT_BY_ID', + payload: nextIndex === -1 ? null : activities[nextIndex].id, + }); event.preventDefault(); break; } @@ -182,7 +192,7 @@ export default function ActivityList({ } } - function handleClick(id: Element['id'], event: SyntheticMouseEvent) { + function handleClick(id: Element['id'] | null, event: SyntheticMouseEvent) { event.preventDefault(); treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: id}); } @@ -195,25 +205,24 @@ export default function ActivityList({ return (
-
- {activityID !== null && ( - // TODO: Obsolete once filtered Activities are included in this list. - - )} -
+
    + {includeAllOption && ( + // TODO: Obsolete once filtered Activities are included in this list. +
  1. + All +
  2. + )} {activities.map(({id, depth}) => { const activity = store.getElementByID(id); if (activity === null) { @@ -244,7 +253,7 @@ export default function ActivityList({ false, )} onPointerLeave={clearHighlightHostInstance}> - {'\u00A0'.repeat(depth) + name} + {'\u00A0'.repeat(depth + (includeAllOption ? 1 : 0)) + name} ); })} From 639a1c20b236933294b8f2f440ef510ca19997a0 Mon Sep 17 00:00:00 2001 From: Sebastian Sebbie Silbermann Date: Tue, 18 Nov 2025 12:32:20 +0100 Subject: [PATCH 2/2] [DevTools] Inlcude Activity list in Components tab --- .../devtools/views/Components/Components.css | 46 ++- .../devtools/views/Components/Components.js | 339 +++++++++++++----- .../src/devtools/views/Components/Tree.css | 7 +- .../src/devtools/views/Components/Tree.js | 7 +- 4 files changed, 302 insertions(+), 97 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index 8df59f72f16..b2492442a0e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -15,11 +15,16 @@ } .TreeWrapper { - flex: 0 0 var(--horizontal-resize-percentage); + border-top: 1px solid var(--color-border); + flex: 1 1 65%; + display: flex; + flex-direction: row; + height: 100%; + overflow: auto; } .InspectedElementWrapper { - flex: 1 1 35%; + flex: 0 0 calc(100% - var(--horizontal-resize-tree-percentage)); overflow-x: hidden; overflow-y: auto; } @@ -42,6 +47,11 @@ width: 5px; height: 100%; cursor: ew-resize; + /* + * The tree in the Components tab has no inherent padding. Make sure these are + * grabbable by elevating the element. + */ + z-index: 1; } @container devtools (width < 600px) { @@ -50,20 +60,46 @@ } .TreeWrapper { - flex: 0 0 var(--vertical-resize-percentage); + border-top: 1px solid var(--color-border); + flex: 1 1 50%; + overflow: hidden; } .InspectedElementWrapper { - flex: 1 1 50%; + flex: 0 0 calc(100% - var(--vertical-resize-tree-percentage)); } - .ResizeBar { + .TreeWrapper + .ResizeBarWrapper .ResizeBar { top: 1px; left: 0; width: 100%; height: 5px; cursor: ns-resize; } + + .ToggleInspectedElement[data-orientation="horizontal"] { + display: none; + } +} + +@container devtools (width >= 600px) { + .ToggleInspectedElement[data-orientation="vertical"] { + display: none; + } +} + +.ActivityList { + flex: 0 0 var(--horizontal-resize-activity-list-percentage);; + border-right: 1px solid var(--color-border); + overflow: auto; +} + +.TreeView { + flex: 1 1 35%; + display: flex; + flex-direction: column; + height: 100%; + overflow: auto; } .Loading { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index 1f0927de98a..54f80157630 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -8,7 +8,13 @@ */ import * as React from 'react'; -import {Fragment, useEffect, useLayoutEffect, useReducer, useRef} from 'react'; +import { + useContext, + useEffect, + useLayoutEffect, + useReducer, + useRef, +} from 'react'; import Tree from './Tree'; import {OwnersListContextController} from './OwnersListContext'; import portaledContent from '../portaledContent'; @@ -19,7 +25,11 @@ import { } from 'react-devtools-shared/src/storage'; import InspectedElementErrorBoundary from './InspectedElementErrorBoundary'; import InspectedElement from './InspectedElement'; +import {TreeStateContext} from './TreeContext'; +import Button from '../Button'; +import ButtonIcon from '../ButtonIcon'; import {ModalDialog} from '../ModalDialog'; +import ActivityList from '../SuspenseTab/ActivityList'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; import {NativeStyleContextController} from './NativeStyleEditor/context'; @@ -28,57 +38,116 @@ import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/Synthe type Orientation = 'horizontal' | 'vertical'; -type ResizeActionType = - | 'ACTION_SET_DID_MOUNT' - | 'ACTION_SET_HORIZONTAL_PERCENTAGE' - | 'ACTION_SET_VERTICAL_PERCENTAGE'; - -type ResizeAction = { - type: ResizeActionType, +type LayoutActionType = + | 'ACTION_SET_ACTIVITY_LIST_TOGGLE' + | 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION' + | 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION' + | 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION'; +type LayoutAction = { + type: LayoutActionType, payload: any, }; -type ResizeState = { - horizontalPercentage: number, - verticalPercentage: number, +type LayoutState = { + activityListHidden: boolean, + activityListHorizontalFraction: number, + inspectedElementHorizontalFraction: number, + inspectedElementVerticalFraction: number, }; +type LayoutDispatch = (action: LayoutAction) => void; + +function ToggleActivityList({ + dispatch, + state, +}: { + dispatch: LayoutDispatch, + state: LayoutState, +}) { + return ( + + ); +} function Components(_: {}) { - const wrapperElementRef = useRef(null); - const resizeElementRef = useRef(null); - - const [state, dispatch] = useReducer( - resizeReducer, + const [state, dispatch] = useReducer( + layoutReducer, null, - initResizeState, + initLayoutState, ); - const {horizontalPercentage, verticalPercentage} = state; + const {activities} = useContext(TreeStateContext); + // If there are no named Activity boundaries, we don't have any tree list and we should hide + // both the panel and the button to toggle it. + const activityListDisabled = activities.length === 0; + + const wrapperTreeRef = useRef(null); + const resizeTreeRef = useRef(null); + const resizeActivityListRef = useRef(null); + + const { + inspectedElementHorizontalFraction, + inspectedElementVerticalFraction, + activityListHidden, + activityListHorizontalFraction, + } = state; useLayoutEffect(() => { - const resizeElement = resizeElementRef.current; + const wrapperElement = wrapperTreeRef.current; setResizeCSSVariable( - resizeElement, + wrapperElement, + 'tree', 'horizontal', - horizontalPercentage * 100, + inspectedElementHorizontalFraction * 100, + ); + setResizeCSSVariable( + wrapperElement, + 'tree', + 'vertical', + inspectedElementVerticalFraction * 100, ); - setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100); - }, []); + const resizeActivityListElement = resizeActivityListRef.current; + setResizeCSSVariable( + resizeActivityListElement, + 'activity-list', + 'horizontal', + activityListHorizontalFraction * 100, + ); + }, []); useEffect(() => { const timeoutID = setTimeout(() => { localStorageSetItem( LOCAL_STORAGE_KEY, JSON.stringify({ - horizontalPercentage, - verticalPercentage, + inspectedElementHorizontalFraction, + inspectedElementVerticalFraction, + activityListHidden, + activityListHorizontalFraction, }), ); }, 500); return () => clearTimeout(timeoutID); - }, [horizontalPercentage, verticalPercentage]); + }, [ + inspectedElementHorizontalFraction, + inspectedElementVerticalFraction, + activityListHidden, + activityListHorizontalFraction, + ]); const onResizeStart = (event: SyntheticPointerEvent) => { const element = event.currentTarget; @@ -90,15 +159,15 @@ function Components(_: {}) { element.releasePointerCapture(event.pointerId); }; - const onResize = (event: SyntheticPointerEvent) => { + const onResizeTree = (event: SyntheticPointerEvent) => { const element = event.currentTarget; const isResizing = element.hasPointerCapture(event.pointerId); if (!isResizing) { return; } - const resizeElement = resizeElementRef.current; - const wrapperElement = wrapperElementRef.current; + const resizeElement = resizeTreeRef.current; + const wrapperElement = wrapperTreeRef.current; if (wrapperElement === null || resizeElement === null) { return; @@ -106,18 +175,18 @@ function Components(_: {}) { event.preventDefault(); - const orientation = getOrientation(wrapperElement); + const orientation = getTreeOrientation(wrapperElement); const {height, width, left, top} = wrapperElement.getBoundingClientRect(); const currentMousePosition = orientation === 'horizontal' ? event.clientX - left : event.clientY - top; - const boundaryMin = MINIMUM_SIZE; + const boundaryMin = MINIMUM_TREE_SIZE; const boundaryMax = orientation === 'horizontal' - ? width - MINIMUM_SIZE - : height - MINIMUM_SIZE; + ? width - MINIMUM_TREE_SIZE + : height - MINIMUM_TREE_SIZE; const isMousePositionInBounds = currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; @@ -127,11 +196,64 @@ function Components(_: {}) { orientation === 'horizontal' ? width : height; const actionType = orientation === 'horizontal' - ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' - : 'ACTION_SET_VERTICAL_PERCENTAGE'; + ? 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION' + : 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION'; + const fraction = currentMousePosition / resizedElementDimension; + const percentage = fraction * 100; + + setResizeCSSVariable(wrapperElement, 'tree', orientation, percentage); + + dispatch({ + type: actionType, + payload: fraction, + }); + } + }; + + const onResizeActivityList = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + const isResizing = element.hasPointerCapture(event.pointerId); + if (!isResizing) { + return; + } + + const resizeElement = resizeActivityListRef.current; + const wrapperElement = resizeTreeRef.current; + + if (wrapperElement === null || resizeElement === null) { + return; + } + + event.preventDefault(); + + const orientation = 'horizontal'; + + const {height, width, left, top} = wrapperElement.getBoundingClientRect(); + + const currentMousePosition = + orientation === 'horizontal' ? event.clientX - left : event.clientY - top; + + const boundaryMin = MINIMUM_ACTIVITY_LIST_SIZE; + const boundaryMax = + orientation === 'horizontal' + ? width - MINIMUM_ACTIVITY_LIST_SIZE + : height - MINIMUM_ACTIVITY_LIST_SIZE; + + const isMousePositionInBounds = + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; + + if (isMousePositionInBounds) { + const resizedElementDimension = + orientation === 'horizontal' ? width : height; + const actionType = 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION'; const percentage = (currentMousePosition / resizedElementDimension) * 100; - setResizeCSSVariable(resizeElement, orientation, percentage); + setResizeCSSVariable( + resizeElement, + 'activity-list', + orientation, + percentage, + ); dispatch({ type: actionType, @@ -143,29 +265,53 @@ function Components(_: {}) { return ( -
    - -
    - -
    -
    +
    +
    + {activityListDisabled ? null : ( + + )} + {activityListDisabled ? null : (
    -
    -
    - - - - - -
    - - - + className={styles.ResizeBarWrapper} + hidden={activityListHidden}> +
    +
    + )} + + ) + } + /> +
    +
    +
    +
    +
    + + + + + +
    + +
    @@ -173,63 +319,82 @@ function Components(_: {}) { } const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; -const VERTICAL_MODE_MAX_WIDTH = 600; -const MINIMUM_SIZE = 100; +const VERTICAL_TREE_MODE_MAX_WIDTH = 600; +const MINIMUM_TREE_SIZE = 100; +const MINIMUM_ACTIVITY_LIST_SIZE = 100; -function initResizeState(): ResizeState { - let horizontalPercentage = 0.65; - let verticalPercentage = 0.5; - - try { - let data = localStorageGetItem(LOCAL_STORAGE_KEY); - if (data != null) { - data = JSON.parse(data); - horizontalPercentage = data.horizontalPercentage; - verticalPercentage = data.verticalPercentage; - } - } catch (error) {} - - return { - horizontalPercentage, - verticalPercentage, - }; -} - -function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { +function layoutReducer(state: LayoutState, action: LayoutAction): LayoutState { switch (action.type) { - case 'ACTION_SET_HORIZONTAL_PERCENTAGE': + case 'ACTION_SET_ACTIVITY_LIST_TOGGLE': + return { + ...state, + activityListHidden: !state.activityListHidden, + }; + case 'ACTION_SET_ACTIVITY_LIST_HORIZONTAL_FRACTION': return { ...state, - horizontalPercentage: action.payload, + activityListHorizontalFraction: action.payload, }; - case 'ACTION_SET_VERTICAL_PERCENTAGE': + case 'ACTION_SET_INSPECTED_ELEMENT_HORIZONTAL_FRACTION': return { ...state, - verticalPercentage: action.payload, + inspectedElementHorizontalFraction: action.payload, + }; + case 'ACTION_SET_INSPECTED_ELEMENT_VERTICAL_FRACTION': + return { + ...state, + inspectedElementVerticalFraction: action.payload, }; default: return state; } } -function getOrientation( +function initLayoutState(): LayoutState { + let inspectedElementHorizontalFraction = 0.65; + let inspectedElementVerticalFraction = 0.5; + let activityListHidden = false; + let activityListHorizontalFraction = 0.35; + + try { + let data = localStorageGetItem(LOCAL_STORAGE_KEY); + if (data != null) { + data = JSON.parse(data); + inspectedElementHorizontalFraction = + data.inspectedElementHorizontalFraction; + inspectedElementVerticalFraction = data.inspectedElementVerticalFraction; + activityListHidden = data.activityListHidden; + activityListHorizontalFraction = data.activityListHorizontalFraction; + } + } catch (error) {} + + return { + inspectedElementHorizontalFraction, + inspectedElementVerticalFraction, + activityListHidden, + activityListHorizontalFraction, + }; +} + +function getTreeOrientation( wrapperElement: null | HTMLElement, ): null | Orientation { if (wrapperElement != null) { const {width} = wrapperElement.getBoundingClientRect(); - return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical'; + return width > VERTICAL_TREE_MODE_MAX_WIDTH ? 'horizontal' : 'vertical'; } return null; } function setResizeCSSVariable( resizeElement: null | HTMLElement, + name: 'tree' | 'activity-list', orientation: null | Orientation, percentage: number, ): void { if (resizeElement !== null && orientation !== null) { resizeElement.style.setProperty( - `--${orientation}-resize-percentage`, + `--${orientation}-resize-${name}-percentage`, `${percentage}%`, ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css index cb2799d4a9c..59d99de30d8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.css @@ -1,10 +1,9 @@ .Tree { - position: relative; - height: 100%; - width: 100%; + flex: 1 1 35%; display: flex; flex-direction: column; - border-top: 1px solid var(--color-border); + height: 100%; + overflow: auto; } .InnerElementType { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 5623d507a3b..673d005ca2a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -73,7 +73,11 @@ function calculateInitialScrollOffset( return (inspectedElementIndex - 3) * elementHeight; } -export default function Tree(): React.Node { +export type TreeProps = { + toggleActivityList: React.Node, +}; + +export default function Tree({toggleActivityList}: TreeProps): React.Node { const dispatch = useContext(TreeDispatcherContext); const { activityID, @@ -453,6 +457,7 @@ export default function Tree(): React.Node {
    + {toggleActivityList} {store.supportsClickToInspect && (