@@ -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
1920import * 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' ;
2226import styles from './ActivityList.css' ;
2327import {
2428 TreeStateContext ,
2529 TreeDispatcherContext ,
2630} from '../Components/TreeContext' ;
2731import { useHighlightHostInstance } from '../hooks' ;
2832import { StoreContext } from '../context' ;
33+ import ButtonIcon from '../ButtonIcon' ;
34+ import Button from '../Button' ;
2935
3036export 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+
65105export 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}
0 commit comments