11"use client" ;
2- import { useTreeContext , useTreePath } from "fumadocs-ui/provider" ;
3- import { createContext , ReactNode , useContext , useMemo , useState , useCallback , useEffect } from "react" ;
2+ import { useSidebar , useTreeContext , useTreePath } from "fumadocs-ui/provider" ;
3+ import { createContext , ReactNode , useContext , useMemo , useState , useCallback , ReactElement } from "react" ;
44import { useDocsNavigation } from "@/app/docs/DocsNavigationProvider" ;
55import * as PageTree from "fumadocs-core/page-tree" ;
66import Link from "next/link" ;
77import { usePathname } from "next/navigation" ;
88import { ChevronDown } from "lucide-react" ;
99import { cn } from "@/lib/utils" ;
1010import { Collapsible , CollapsibleContent , CollapsibleTrigger , } from "@/components/ui/collapsible" ;
11+ import { ScrollArea , ScrollViewport } from "@/components/ui/scroll-area" ;
12+ import { useOnChange } from "fumadocs-core/utils/use-on-change" ;
1113import { ncIsObject } from "@/utils/is" ;
14+ import { RemoveScroll } from "react-remove-scroll" ;
15+ import { useMediaQuery } from "@/utils/use-media-query" ;
1216
1317interface FolderContextType {
1418 open : boolean ;
@@ -25,16 +29,70 @@ const useFolderContext = () => {
2529 return context ;
2630} ;
2731
32+ interface InternalContext {
33+ level : number ;
34+ isMobile ?: boolean ;
35+ closeSidebar ?: ( ) => void ;
36+ }
37+
38+ const InternalContext = createContext < InternalContext | null > ( null ) ;
39+
40+ const useInternalContext = ( ) => {
41+ const context = useContext ( InternalContext ) ;
42+ if ( ! context ) {
43+ throw new Error ( "useInternalContext must be used within an InternalContext Provider" ) ;
44+ }
45+ return context ;
46+ } ;
47+
48+
49+ export const SidebarList = ( {
50+ as,
51+ blockScrollingWidth,
52+ removeScrollOn = blockScrollingWidth
53+ ? `(width < ${ blockScrollingWidth } px)`
54+ : undefined ,
55+ ...props
56+ } : {
57+ as ?: string ;
58+ blockScrollingWidth ?: number ;
59+ removeScrollOn ?: string ;
60+ children : ReactNode ;
61+ } & React . HTMLAttributes < HTMLDivElement > ) : ReactElement => {
62+ const { open } = useSidebar ( ) ;
63+ const isBlocking =
64+ useMediaQuery ( removeScrollOn ?? '' , ! removeScrollOn ) ?? false ;
65+
66+ return (
67+ < RemoveScroll
68+ as = { as ?? 'aside' }
69+ data-open = { open }
70+ enabled = { isBlocking && open }
71+ { ...props }
72+ >
73+ { props . children }
74+ </ RemoveScroll >
75+ ) ;
76+ }
77+
2878const Sidebar = ( { isMobile} : { isMobile ?: boolean } ) => {
2979 const { root} = useTreeContext ( ) ;
3080 const pathname = usePathname ( ) ;
3181 const { setIsOpen } = useDocsNavigation ( ) ;
3282
83+ const { open} = useSidebar ( )
84+
3385 const closeSidebar = useCallback ( ( ) => {
3486 if ( isMobile ) {
3587 setIsOpen ( false ) ;
3688 }
3789 } , [ isMobile , setIsOpen ] ) ;
90+
91+ const context = useMemo < InternalContext > ( ( ) => ( {
92+ level : 1 ,
93+ isMobile,
94+ closeSidebar
95+ } ) , [ isMobile , closeSidebar ] ) ;
3896
3997 const children = useMemo ( ( ) => {
4098 function renderItems ( items : PageTree . Node [ ] , level : number ) {
@@ -54,11 +112,15 @@ const Sidebar = ({isMobile}: {isMobile?: boolean}) => {
54112 } , [ pathname ] ) ;
55113
56114 return (
57- < div className = { cn ( "xl:flex sticky py-4 mr-3 flex-col shrink-0 " , isMobile ? 'block' : 'hidden xl:block' , isMobile ? 'w-64' : 'top-[120px] h-[calc(100dvh-120px)] w-64' ) } >
58- < div className = "flex flex-col gap-2" >
59- { children }
60- </ div >
61- </ div >
115+ < InternalContext . Provider value = { context } >
116+ < SidebarList removeScrollOn = "(width < 768px)" className = { cn ( "xl:flex sticky py-4 mr-3 flex-col shrink-0 " , open ? 'block' : 'hidden xl:block' , isMobile ? 'block' : 'hidden top-[120px] h-[calc(100dvh-120px)] w-64' ) } >
117+ < ScrollArea className = "h-full" >
118+ < ScrollViewport >
119+ { children }
120+ </ ScrollViewport >
121+ </ ScrollArea >
122+ </ SidebarList >
123+ </ InternalContext . Provider >
62124 ) ;
63125} ;
64126
@@ -68,14 +130,14 @@ function SidebarItem({item, children, level,}: {
68130 pathname : string ;
69131 level : number ;
70132} ) {
133+ const context = useInternalContext ( ) ;
71134 const path = useTreePath ( ) ;
72- const active = path . includes ( item ) ;
73135
136+ const active = path . includes ( item ) ;
137+
74138 const handleLinkClick = ( ) => {
75- // Close sidebar on mobile navigation
76- const sidebar = document . querySelector ( '[data-sidebar="sidebar"]' ) ;
77- if ( sidebar && window . innerWidth < 768 ) {
78- // Mobile sidebar close logic will be handled by Shadcn sidebar
139+ if ( context . closeSidebar ) {
140+ context . closeSidebar ( ) ;
79141 }
80142 } ;
81143
@@ -91,7 +153,7 @@ function SidebarItem({item, children, level,}: {
91153 href = { item . url }
92154 onClick = { handleLinkClick }
93155 >
94- { ncIsObject ( item . icon ) ? item . icon : '' }
156+ { ncIsObject ( item . icon ) ? item . icon : '' }
95157 { item . name }
96158 </ Link >
97159 ) ;
@@ -100,12 +162,11 @@ function SidebarItem({item, children, level,}: {
100162 if ( item . type === "separator" ) {
101163 return (
102164 < p className = "text-fd-muted-foreground mt-6 mb-2 first:mt-0" >
103- { ncIsObject ( item . icon ) ? item . icon : '' }
165+ { ncIsObject ( item . icon ) ? item . icon : '' }
104166 { item . name }
105167 </ p >
106168 ) ;
107169 }
108-
109170 if ( item . type === "folder" ) {
110171 return (
111172 < SidebarFolder defaultOpen = { ( active || ( item . defaultOpen ?? false ) ) } >
@@ -118,14 +179,14 @@ function SidebarItem({item, children, level,}: {
118179 active
119180 ? "text-nc-content-grey-emphasis"
120181 : "text-nc-content-grey-subtle-2 hover:bg-nc-background-grey-light"
121- ) }
122- href = { item . index . url }
123- onClick = { handleLinkClick }
124- >
125- { ncIsObject ( item . index . icon ) ? item . index . icon : '' }
126- { item . index . name }
127- </ Link >
128- </ div >
182+ ) }
183+ href = { item . index . url }
184+ onClick = { handleLinkClick }
185+ >
186+ { ncIsObject ( item . index . icon ) ? item . index . icon : '' }
187+ { item . index . name }
188+ </ Link >
189+ </ div >
129190 </ SidebarFolderTrigger >
130191 ) : (
131192 < SidebarFolderTrigger >
@@ -135,7 +196,7 @@ function SidebarItem({item, children, level,}: {
135196 active ? "text-nc-content-grey-subtle-2 font-[600]" : ""
136197 ) }
137198 >
138- { ncIsObject ( item . icon ) ? item . icon : '' }
199+ { ncIsObject ( item . icon ) ? item . icon : '' }
139200 { item . name }
140201 </ div >
141202 </ SidebarFolderTrigger >
@@ -160,9 +221,9 @@ function SidebarFolder({defaultOpen = false, children}: {
160221} ) {
161222 const [ open , setOpen ] = useState ( defaultOpen ) ;
162223
163- useEffect ( ( ) => {
164- if ( defaultOpen ) setOpen ( defaultOpen ) ;
165- } , [ defaultOpen ] ) ;
224+ useOnChange ( defaultOpen , ( v ) => {
225+ if ( v ) setOpen ( v ) ;
226+ } ) ;
166227
167228 return (
168229 < Collapsible open = { open } onOpenChange = { setOpen } >
@@ -197,9 +258,19 @@ function SidebarFolderTrigger({children}: { children: ReactNode }) {
197258}
198259
199260function SidebarFolderContent ( { children} : { children : ReactNode } ) {
261+ const ctx = useInternalContext ( ) ;
262+
200263 return (
201264 < CollapsibleContent >
202- { children }
265+ < InternalContext . Provider
266+ value = { useMemo ( ( ) => ( {
267+ level : ctx . level + 1 ,
268+ isMobile : ctx . isMobile ,
269+ closeSidebar : ctx . closeSidebar
270+ } ) , [ ctx . level , ctx . isMobile , ctx . closeSidebar ] ) }
271+ >
272+ { children }
273+ </ InternalContext . Provider >
203274 </ CollapsibleContent >
204275 ) ;
205276}
0 commit comments