22 * Copyright (c) Facebook, Inc. and its affiliates.
33 */
44
5- import { useRef , useLayoutEffect , Fragment } from 'react' ;
6-
5+ import {
6+ useRef ,
7+ useEffect ,
8+ Fragment ,
9+ useState ,
10+ useCallback ,
11+ useMemo ,
12+ } from 'react' ;
713import cn from 'classnames' ;
814import { useRouter } from 'next/router' ;
15+ import { SidebarButton } from './SidebarButton' ;
916import { SidebarLink } from './SidebarLink' ;
1017import { useCollapse } from 'react-collapsed' ;
1118import usePendingRoute from 'hooks/usePendingRoute' ;
@@ -19,67 +26,74 @@ interface SidebarRouteTreeProps {
1926 level ?: number ;
2027}
2128
22- function CollapseWrapper ( {
29+ /**
30+ * CollapseWrapper Component:
31+ * Handles smooth expanding and collapsing of sidebar items.
32+ */
33+ const CollapseWrapper = ( {
2334 isExpanded,
2435 duration,
2536 children,
2637} : {
2738 isExpanded : boolean ;
2839 duration : number ;
2940 children : any ;
30- } ) {
41+ } ) => {
3142 const ref = useRef < HTMLDivElement | null > ( null ) ;
3243 const timeoutRef = useRef < number | null > ( null ) ;
33- const { getCollapseProps} = useCollapse ( {
34- isExpanded,
35- duration,
36- } ) ;
44+ const { getCollapseProps} = useCollapse ( { isExpanded, duration} ) ;
3745
38- // Disable pointer events while animating.
39- const isExpandedRef = useRef ( isExpanded ) ;
40- if ( typeof window !== 'undefined' ) {
41- // eslint-disable-next-line react-compiler/react-compiler
42- // eslint-disable-next-line react-hooks/rules-of-hooks
43- useLayoutEffect ( ( ) => {
44- const wasExpanded = isExpandedRef . current ;
45- if ( wasExpanded === isExpanded ) {
46- return ;
47- }
48- isExpandedRef . current = isExpanded ;
49- if ( ref . current !== null ) {
50- const node : HTMLDivElement = ref . current ;
51- node . style . pointerEvents = 'none' ;
52- if ( timeoutRef . current !== null ) {
53- window . clearTimeout ( timeoutRef . current ) ;
54- }
55- timeoutRef . current = window . setTimeout ( ( ) => {
56- node . style . pointerEvents = '' ;
57- } , duration + 100 ) ;
58- }
59- } ) ;
60- }
46+ useEffect ( ( ) => {
47+ if ( typeof window !== 'undefined' ) {
48+ ref . current && ( ref . current . style . pointerEvents = 'none' ) ;
49+ timeoutRef . current = window . setTimeout ( ( ) => {
50+ ref . current && ( ref . current . style . pointerEvents = '' ) ;
51+ } , duration + 100 ) ;
52+ }
53+ } , [ isExpanded , duration ] ) ;
6154
6255 return (
6356 < div
6457 ref = { ref }
6558 className = { cn ( isExpanded ? 'opacity-100' : 'opacity-50' ) }
66- style = { {
67- transition : `opacity ${ duration } ms ease-in-out` ,
68- } } >
59+ style = { { transition : `opacity ${ duration } ms ease-in-out` } } >
6960 < div { ...getCollapseProps ( ) } > { children } </ div >
7061 </ div >
7162 ) ;
72- }
63+ } ;
7364
65+ /**
66+ * SidebarRouteTree Component:
67+ * Dynamically generates the sidebar menu with collapsible sections.
68+ */
7469export function SidebarRouteTree ( {
7570 isForceExpanded,
7671 breadcrumbs,
7772 routeTree,
7873 level = 0 ,
7974} : SidebarRouteTreeProps ) {
80- const slug = useRouter ( ) . asPath . split ( / [ \? \# ] / ) [ 0 ] ;
75+ const router = useRouter ( ) ;
76+ const slug = router . asPath . split ( / [ ? # ] / ) [ 0 ] ; // Extract current route path
8177 const pendingRoute = usePendingRoute ( ) ;
82- const currentRoutes = routeTree . routes as RouteItem [ ] ;
78+
79+ // Memoize the current route list for performance optimization
80+ const currentRoutes = useMemo (
81+ ( ) => routeTree . routes as RouteItem [ ] ,
82+ [ routeTree . routes ]
83+ ) ;
84+
85+ // State to track expanded items
86+ const [ expandedItem , setExpandedItem ] = useState < string | null > ( null ) ;
87+
88+ /**
89+ * Toggle function to handle sidebar dropdowns.
90+ * Closes the currently expanded item if clicked again.
91+ * Ensures only one section is open at a time.
92+ */
93+ const handleToggle = useCallback ( ( path : string ) => {
94+ setExpandedItem ( ( prev ) => ( prev === path ? null : path ) ) ;
95+ } , [ ] ) ;
96+
8397 return (
8498 < ul >
8599 { currentRoutes . map (
@@ -97,8 +111,9 @@ export function SidebarRouteTree({
97111 ) => {
98112 const selected = slug === path ;
99113 let listItem = null ;
114+
100115 if ( ! path || heading ) {
101- // if current route item has no path and children treat it as an API sidebar heading
116+ // Render nested sidebar sections
102117 listItem = (
103118 < SidebarRouteTree
104119 level = { level + 1 }
@@ -108,23 +123,22 @@ export function SidebarRouteTree({
108123 />
109124 ) ;
110125 } else if ( routes ) {
111- // if route has a path and child routes, treat it as an expandable sidebar item
126+ // Handle collapsible sidebar sections
112127 const isBreadcrumb =
113128 breadcrumbs . length > 1 &&
114129 breadcrumbs [ breadcrumbs . length - 1 ] . path === path ;
115- const isExpanded = isForceExpanded || isBreadcrumb || selected ;
130+ const isExpanded = expandedItem === path ;
131+
116132 listItem = (
117133 < li key = { `${ title } -${ path } -${ level } -heading` } >
118- < SidebarLink
134+ < SidebarButton
119135 key = { `${ title } -${ path } -${ level } -link` }
120- href = { path }
121- isPending = { pendingRoute === path }
122- selected = { selected }
123- level = { level }
124136 title = { title }
125- version = { version }
137+ heading = { false }
138+ level = { level }
139+ onClick = { ( ) => handleToggle ( path ) }
126140 isExpanded = { isExpanded }
127- hideArrow = { isForceExpanded }
141+ isBreadcrumb = { isBreadcrumb }
128142 />
129143 < CollapseWrapper duration = { 250 } isExpanded = { isExpanded } >
130144 < SidebarRouteTree
@@ -137,7 +151,7 @@ export function SidebarRouteTree({
137151 </ li >
138152 ) ;
139153 } else {
140- // if route has a path and no child routes, treat it as a sidebar link
154+ // Render individual sidebar links
141155 listItem = (
142156 < li key = { `${ title } -${ path } -${ level } -link` } >
143157 < SidebarLink
@@ -151,11 +165,12 @@ export function SidebarRouteTree({
151165 </ li >
152166 ) ;
153167 }
168+
169+ // Render section headers if applicable
154170 if ( hasSectionHeader ) {
155- let sectionHeaderText =
156- sectionHeader != null
157- ? sectionHeader . replace ( '{{version}}' , siteConfig . version )
158- : '' ;
171+ let sectionHeaderText = sectionHeader
172+ ? sectionHeader . replace ( '{{version}}' , siteConfig . version )
173+ : '' ;
159174 return (
160175 < Fragment key = { `${ sectionHeaderText } -${ level } -separator` } >
161176 { index !== 0 && (
@@ -173,9 +188,8 @@ export function SidebarRouteTree({
173188 </ h3 >
174189 </ Fragment >
175190 ) ;
176- } else {
177- return listItem ;
178191 }
192+ return listItem ;
179193 }
180194 ) }
181195 </ ul >
0 commit comments