diff --git a/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/src/components/Layout/Sidebar/SidebarRouteTree.tsx index 72003df74f2..27881b8d058 100644 --- a/src/components/Layout/Sidebar/SidebarRouteTree.tsx +++ b/src/components/Layout/Sidebar/SidebarRouteTree.tsx @@ -2,10 +2,17 @@ * Copyright (c) Facebook, Inc. and its affiliates. */ -import {useRef, useLayoutEffect, Fragment} from 'react'; - +import { + useRef, + useEffect, + Fragment, + useState, + useCallback, + useMemo, +} from 'react'; import cn from 'classnames'; import {useRouter} from 'next/router'; +import {SidebarButton} from './SidebarButton'; import {SidebarLink} from './SidebarLink'; import {useCollapse} from 'react-collapsed'; import usePendingRoute from 'hooks/usePendingRoute'; @@ -19,7 +26,11 @@ interface SidebarRouteTreeProps { level?: number; } -function CollapseWrapper({ +/** + * CollapseWrapper Component: + * Handles smooth expanding and collapsing of sidebar items. + */ +const CollapseWrapper = ({ isExpanded, duration, children, @@ -27,59 +38,62 @@ function CollapseWrapper({ isExpanded: boolean; duration: number; children: any; -}) { +}) => { const ref = useRef(null); const timeoutRef = useRef(null); - const {getCollapseProps} = useCollapse({ - isExpanded, - duration, - }); + const {getCollapseProps} = useCollapse({isExpanded, duration}); - // Disable pointer events while animating. - const isExpandedRef = useRef(isExpanded); - if (typeof window !== 'undefined') { - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/rules-of-hooks - useLayoutEffect(() => { - const wasExpanded = isExpandedRef.current; - if (wasExpanded === isExpanded) { - return; - } - isExpandedRef.current = isExpanded; - if (ref.current !== null) { - const node: HTMLDivElement = ref.current; - node.style.pointerEvents = 'none'; - if (timeoutRef.current !== null) { - window.clearTimeout(timeoutRef.current); - } - timeoutRef.current = window.setTimeout(() => { - node.style.pointerEvents = ''; - }, duration + 100); - } - }); - } + useEffect(() => { + if (typeof window !== 'undefined') { + ref.current && (ref.current.style.pointerEvents = 'none'); + timeoutRef.current = window.setTimeout(() => { + ref.current && (ref.current.style.pointerEvents = ''); + }, duration + 100); + } + }, [isExpanded, duration]); return (
+ style={{transition: `opacity ${duration}ms ease-in-out`}}>
{children}
); -} +}; +/** + * SidebarRouteTree Component: + * Dynamically generates the sidebar menu with collapsible sections. + */ export function SidebarRouteTree({ isForceExpanded, breadcrumbs, routeTree, level = 0, }: SidebarRouteTreeProps) { - const slug = useRouter().asPath.split(/[\?\#]/)[0]; + const router = useRouter(); + const slug = router.asPath.split(/[?#]/)[0]; // Extract current route path const pendingRoute = usePendingRoute(); - const currentRoutes = routeTree.routes as RouteItem[]; + + // Memoize the current route list for performance optimization + const currentRoutes = useMemo( + () => routeTree.routes as RouteItem[], + [routeTree.routes] + ); + + // State to track expanded items + const [expandedItem, setExpandedItem] = useState(null); + + /** + * Toggle function to handle sidebar dropdowns. + * Closes the currently expanded item if clicked again. + * Ensures only one section is open at a time. + */ + const handleToggle = useCallback((path: string) => { + setExpandedItem((prev) => (prev === path ? null : path)); + }, []); + return (
    {currentRoutes.map( @@ -97,8 +111,9 @@ export function SidebarRouteTree({ ) => { const selected = slug === path; let listItem = null; + if (!path || heading) { - // if current route item has no path and children treat it as an API sidebar heading + // Render nested sidebar sections listItem = ( ); } else if (routes) { - // if route has a path and child routes, treat it as an expandable sidebar item + // Handle collapsible sidebar sections const isBreadcrumb = breadcrumbs.length > 1 && breadcrumbs[breadcrumbs.length - 1].path === path; - const isExpanded = isForceExpanded || isBreadcrumb || selected; + const isExpanded = expandedItem === path; + listItem = (
  • - handleToggle(path)} isExpanded={isExpanded} - hideArrow={isForceExpanded} + isBreadcrumb={isBreadcrumb} /> ); } else { - // if route has a path and no child routes, treat it as a sidebar link + // Render individual sidebar links listItem = (
  • ); } + + // Render section headers if applicable if (hasSectionHeader) { - let sectionHeaderText = - sectionHeader != null - ? sectionHeader.replace('{{version}}', siteConfig.version) - : ''; + let sectionHeaderText = sectionHeader + ? sectionHeader.replace('{{version}}', siteConfig.version) + : ''; return ( {index !== 0 && ( @@ -173,9 +188,8 @@ export function SidebarRouteTree({ ); - } else { - return listItem; } + return listItem; } )}