@@ -11,7 +11,16 @@ import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
1111import typeof { SyntheticMouseEvent } from 'react-dom-bindings/src/events/SyntheticEvent' ;
1212
1313import * as React from 'react' ;
14- import { useContext , useLayoutEffect , useState } from 'react' ;
14+ import { useContext , useLayoutEffect , useRef , useState } from 'react' ;
15+ import Button from '../Button' ;
16+ import ButtonIcon from '../ButtonIcon' ;
17+ import Tooltip from '../Components/reach-ui/tooltip' ;
18+ import {
19+ Menu ,
20+ MenuList ,
21+ MenuButton ,
22+ MenuItem ,
23+ } from '../Components/reach-ui/menu-button' ;
1524import {
1625 TreeDispatcherContext ,
1726 TreeStateContext ,
@@ -99,8 +108,146 @@ type SuspenseBreadcrumbsMenuProps = {
99108 onItemPointerLeave : ( event : SyntheticMouseEvent ) => void ,
100109} ;
101110
102- function SuspenseBreadcrumbsMenu ( { } : SuspenseBreadcrumbsMenuProps ) : React$Node {
103- return < div > TODO: Suspense Breadcrumbs Menu</ div > ;
111+ function SuspenseBreadcrumbsMenu ( {
112+ onItemClick,
113+ onItemPointerEnter,
114+ onItemPointerLeave,
115+ } : SuspenseBreadcrumbsMenuProps ) : React$Node {
116+ const store = useContext ( StoreContext ) ;
117+ const { activityID} = useContext ( TreeStateContext ) ;
118+ const { selectedSuspenseID, lineage, roots} = useContext (
119+ SuspenseTreeStateContext ,
120+ ) ;
121+ const selectedSuspenseNode =
122+ selectedSuspenseID !== null
123+ ? store . getSuspenseByID ( selectedSuspenseID )
124+ : null ;
125+
126+ return (
127+ < >
128+ { lineage === null ? null : lineage . length === 0 ? (
129+ // We selected the root. This means that we're currently viewing the Transition
130+ // that rendered the whole screen. In laymans terms this is really "Initial Paint" .
131+ // When we're looking at a subtree selection, then the equivalent is a
132+ // "Transition" since in that case it's really about a Transition within the page.
133+ roots . length > 0 ? (
134+ < button
135+ className = { styles . SuspenseBreadcrumbsButton }
136+ onClick = { onItemClick . bind (
137+ null ,
138+ activityID === null ? roots [ 0 ] : activityID ,
139+ ) }
140+ type = "button" >
141+ { activityID === null ? 'Initial Paint' : 'Transition' }
142+ </ button >
143+ ) : null
144+ ) : (
145+ < >
146+ < SuspenseBreadcrumbsDropdown
147+ lineage = { lineage }
148+ selectElement = { onItemClick }
149+ />
150+ < SuspenseBreadcrumbsToParentButton
151+ lineage = { lineage }
152+ selectedSuspenseID = { selectedSuspenseID }
153+ selectElement = { onItemClick }
154+ />
155+ { selectedSuspenseNode != null && (
156+ < button
157+ className = { styles . SuspenseBreadcrumbsButton }
158+ onClick = { onItemClick . bind ( null , selectedSuspenseNode . id ) }
159+ onPointerEnter = { onItemPointerEnter . bind (
160+ null ,
161+ selectedSuspenseNode . id ,
162+ false ,
163+ ) }
164+ onPointerLeave = { onItemPointerLeave }
165+ type = "button" >
166+ { selectedSuspenseNode === null
167+ ? 'Unknown'
168+ : selectedSuspenseNode . name || 'Unknown' }
169+ </ button >
170+ ) }
171+ </ >
172+ ) }
173+ </ >
174+ ) ;
175+ }
176+
177+ type SuspenseBreadcrumbsDropdownProps = {
178+ lineage : $ReadOnlyArray < SuspenseNode [ 'id' ] > ,
179+ selectedIndex : number ,
180+ selectElement : ( id : SuspenseNode [ 'id' ] ) => void ,
181+ } ;
182+ function SuspenseBreadcrumbsDropdown ( {
183+ lineage,
184+ selectElement,
185+ } : SuspenseBreadcrumbsDropdownProps ) {
186+ const store = useContext ( StoreContext ) ;
187+
188+ const menuItems = [ ] ;
189+ for ( let index = lineage . length - 1 ; index >= 0 ; index -- ) {
190+ const suspenseNodeID = lineage [ index ] ;
191+ const node = store . getSuspenseByID ( suspenseNodeID ) ;
192+ menuItems . push (
193+ < MenuItem
194+ key = { suspenseNodeID }
195+ className = { `${ styles . Component } ` }
196+ onSelect = { selectElement . bind ( null , suspenseNodeID ) } >
197+ { node === null ? 'Unknown' : node . name || 'Unknown' }
198+ </ MenuItem > ,
199+ ) ;
200+ }
201+
202+ return (
203+ < Menu >
204+ < MenuButton className = { styles . SuspenseBreadcrumbsMenuButton } >
205+ < Tooltip label = "Open elements dropdown" >
206+ < span
207+ className = { styles . SuspenseBreadcrumbsMenuButtonContent }
208+ tabIndex = { - 1 } >
209+ < ButtonIcon type = "more" />
210+ </ span >
211+ </ Tooltip >
212+ </ MenuButton >
213+ < MenuList className = { styles . SuspenseBreadcrumbsModal } >
214+ { menuItems }
215+ </ MenuList >
216+ </ Menu >
217+ ) ;
218+ }
219+
220+ type SuspenseBreadcrumbsToParentButtonProps = {
221+ lineage : $ReadOnlyArray < SuspenseNode [ 'id' ] > ,
222+ selectedSuspenseID : SuspenseNode [ 'id' ] | null ,
223+ selectElement : ( id : SuspenseNode [ 'id' ] , event : SyntheticMouseEvent ) => void ,
224+ } ;
225+ function SuspenseBreadcrumbsToParentButton ( {
226+ lineage,
227+ selectedSuspenseID,
228+ selectElement,
229+ } : SuspenseBreadcrumbsToParentButtonProps ) {
230+ const store = useContext ( StoreContext ) ;
231+ const selectedIndex =
232+ selectedSuspenseID === null
233+ ? lineage . length - 1
234+ : lineage . indexOf ( selectedSuspenseID ) ;
235+
236+ if ( selectedIndex <= 0 ) {
237+ return null ;
238+ }
239+
240+ const parentID = lineage [ selectedIndex - 1 ] ;
241+ const parent = store . getSuspenseByID ( parentID ) ;
242+
243+ return (
244+ < Button
245+ className = { parent !== null ? undefined : styles . NotInStore }
246+ onClick = { parent !== null ? selectElement . bind ( null , parentID ) : null }
247+ title = { `Up to ${ parent === null ? 'Unknown' : parent . name || 'Unknown' } ` } >
248+ < ButtonIcon type = "previous" />
249+ </ Button >
250+ ) ;
104251}
105252
106253export default function SuspenseBreadcrumbs ( ) : React$Node {
@@ -110,26 +257,44 @@ export default function SuspenseBreadcrumbs(): React$Node {
110257 const { highlightHostInstance, clearHighlightHostInstance} =
111258 useHighlightHostInstance ( ) ;
112259
113- function handleClick ( id : SuspenseNode [ 'id' ] , event : SyntheticMouseEvent ) {
114- event . preventDefault ( ) ;
260+ function handleClick ( id : SuspenseNode [ 'id' ] , event ?: SyntheticMouseEvent ) {
261+ if ( event !== undefined ) {
262+ // E.g. 3rd party component libraries might omit the event and already prevent default
263+ // like Reach's MenuItem does.
264+ event . preventDefault ( ) ;
265+ }
115266 treeDispatch ( { type : 'SELECT_ELEMENT_BY_ID' , payload : id } ) ;
116267 suspenseTreeDispatch ( { type : 'SELECT_SUSPENSE_BY_ID' , payload : id } ) ;
117268 }
118269
119270 const [ elementsTotalWidth , setElementsTotalWidth ] = useState ( 0 ) ;
120- const containerRef = React . useRef < HTMLDivElement | null > ( null ) ;
271+ const containerRef = useRef < HTMLDivElement | null > ( null ) ;
121272 const isOverflowing = useIsOverflowing ( containerRef , elementsTotalWidth ) ;
122273
123274 useLayoutEffect ( ( ) => {
124275 const container = containerRef . current ;
125- if ( container === null ) {
276+
277+ if (
278+ container === null ||
279+ // We want to measure the size of the flat list only when it's being used.
280+ isOverflowing
281+ ) {
126282 return ;
127283 }
128284
129285 const ResizeObserver = container . ownerDocument . defaultView . ResizeObserver ;
130- const observer = new ResizeObserver ( entries => {
131- const entry = entries [ 0 ] ;
132- setElementsTotalWidth ( entry . contentRect . width ) ;
286+ const observer = new ResizeObserver ( ( ) => {
287+ let totalWidth = 0 ;
288+ for ( let i = 0 ; i < container . children . length ; i ++ ) {
289+ const element = container . children [ i ] ;
290+ const computedStyle = getComputedStyle ( element ) ;
291+
292+ totalWidth +=
293+ element . offsetWidth +
294+ parseInt ( computedStyle . marginLeft , 10 ) +
295+ parseInt ( computedStyle . marginRight , 10 ) ;
296+ }
297+ setElementsTotalWidth ( totalWidth ) ;
133298 } ) ;
134299
135300 observer . observe ( container ) ;
@@ -140,7 +305,11 @@ export default function SuspenseBreadcrumbs(): React$Node {
140305 return (
141306 < div className = { styles . SuspenseBreadcrumbsContainer } ref = { containerRef } >
142307 { isOverflowing ? (
143- < SuspenseBreadcrumbsMenu />
308+ < SuspenseBreadcrumbsMenu
309+ onItemClick = { handleClick }
310+ onItemPointerEnter = { highlightHostInstance }
311+ onItemPointerLeave = { clearHighlightHostInstance }
312+ />
144313 ) : (
145314 < SuspenseBreadcrumbsFlatList
146315 onItemClick = { handleClick }
0 commit comments