-
Notifications
You must be signed in to change notification settings - Fork 68.1k
Expand file tree
/
Copy pathSidebarCollapseContext.tsx
More file actions
135 lines (120 loc) · 4.69 KB
/
Copy pathSidebarCollapseContext.tsx
File metadata and controls
135 lines (120 loc) · 4.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { useRouter } from 'next/router'
import Cookies from '@/frame/components/lib/cookies'
import { SIDEBAR_COLLAPSED_COOKIE_NAME } from '@/frame/lib/constants'
// Persists whether the desktop doc-tree rail is collapsed, and holds the
// (non-persisted) open state of the inline mobile nav. Mirrors the per-branch
// expand persistence in src/landings/components/useSidebarExpandState.tsx:
// collapsed state is kept in a cookie and shared through context so the
// secondary bar's toggle, the layout that renders the rail, and the mobile nav
// trigger all stay in sync.
//
// SSR-safety: the cookie is read server-side in getMainContext and passed to the
// provider as `initialCollapsed`, so the first render (server + client hydration)
// already reflects the persisted state and markup matches — no flash of the open
// rail before it collapses. When no initial is supplied, it falls back to reading
// the cookie client-side via the SSR-safe cookie lib.
function readCollapsed(): boolean {
try {
return Cookies.get(SIDEBAR_COLLAPSED_COOKIE_NAME) === 'true'
} catch {
return false
}
}
function persistCollapsed(collapsed: boolean) {
try {
Cookies.set(SIDEBAR_COLLAPSED_COOKIE_NAME, String(collapsed))
} catch {
// Cookie writes may fail (disabled cookies, etc.) — degrade to non-persisted
// state rather than throwing.
}
}
type SidebarCollapseContextValue = {
// Desktop: whether the left rail is collapsed (persisted).
collapsed: boolean
toggleCollapsed: () => void
setCollapsed: (collapsed: boolean) => void
// Mobile: whether the doc-tree nav is expanded inline (not persisted). The
// nav renders in the page flow, same as desktop — not in a dialog overlay.
mobileNavOpen: boolean
toggleMobileNav: () => void
closeMobileNav: () => void
}
const SidebarCollapseContext = createContext<SidebarCollapseContextValue | null>(null)
export function SidebarCollapseProvider({
children,
initialCollapsed,
}: {
children: ReactNode
initialCollapsed?: boolean
}) {
const { asPath } = useRouter()
// Seed from the SSR-read cookie value so server and first client render agree.
// When no initial is supplied, fall back to reading the cookie client-side.
const [collapsed, setCollapsedState] = useState(() => initialCollapsed ?? readCollapsed())
const [mobileNavOpen, setMobileNavOpen] = useState(false)
const setCollapsed = useCallback((next: boolean) => {
setCollapsedState(next)
persistCollapsed(next)
}, [])
const toggleCollapsed = useCallback(() => {
setCollapsedState((prev) => {
const next = !prev
persistCollapsed(next)
return next
})
}, [])
const toggleMobileNav = useCallback(() => setMobileNavOpen((prev) => !prev), [])
const closeMobileNav = useCallback(() => setMobileNavOpen(false), [])
// Client-side navigation doesn't unmount the inline mobile nav, so close it
// when the route (or REST in-page hash) changes.
useEffect(() => {
setMobileNavOpen(false)
}, [asPath])
// Growing the viewport to the desktop (xxl) layout takes over from the inline
// mobile nav and hides its toggle, so close the mobile nav when we cross the
// breakpoint — otherwise its open state stays stuck and keeps the content
// column hidden. 1400px mirrors breakpoint-xxl.scss.
useEffect(() => {
if (typeof window === 'undefined' || !window.matchMedia) return
const mql = window.matchMedia('(min-width: 1400px)')
const handle = (e: MediaQueryListEvent | MediaQueryList) => {
if (e.matches) setMobileNavOpen(false)
}
handle(mql) // close immediately if already at/above xxl on mount
mql.addEventListener('change', handle)
return () => mql.removeEventListener('change', handle)
}, [])
const value = useMemo(
() => ({
collapsed,
toggleCollapsed,
setCollapsed,
mobileNavOpen,
toggleMobileNav,
closeMobileNav,
}),
[collapsed, toggleCollapsed, setCollapsed, mobileNavOpen, toggleMobileNav, closeMobileNav],
)
return <SidebarCollapseContext.Provider value={value}>{children}</SidebarCollapseContext.Provider>
}
/**
* Read/toggle the desktop rail's collapsed state and the inline mobile nav's
* open state. Falls back to a no-op expanded/closed state if used outside the
* provider.
*/
export function useSidebarCollapsed(): SidebarCollapseContextValue {
const ctx = useContext(SidebarCollapseContext)
if (!ctx) {
return {
collapsed: false,
toggleCollapsed: () => {},
setCollapsed: () => {},
mobileNavOpen: false,
toggleMobileNav: () => {},
closeMobileNav: () => {},
}
}
return ctx
}