Skip to content

Commit 4fb2452

Browse files
author
Dimitri POSTOLOV
authoredOct 10, 2023
[v3] fix invisible mobile menu when layout: "raw" is specified in _meta file (#2413)

File tree

4 files changed

+205
-213
lines changed

4 files changed

+205
-213
lines changed
 

‎.changeset/tidy-avocados-trade.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'nextra-theme-docs': patch
3+
---
4+
5+
fix invisible mobile menu when `layout: "raw"` is specified in _meta file

‎packages/nextra-theme-docs/src/contexts/config.tsx

+31-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { ThemeProvider } from 'next-themes'
22
import { useRouter } from 'next/router'
33
import type { FrontMatter, PageMapItem, PageOpts } from 'nextra'
4+
import { useFSRoute } from 'nextra/hooks'
5+
import { normalizePages } from 'nextra/normalize-pages'
46
import { metaSchema } from 'nextra/schemas'
57
import type { ReactElement, ReactNode } from 'react'
6-
import { createContext, useContext, useEffect, useState } from 'react'
8+
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
79
import type { ZodError } from 'zod'
810
import type { DocsThemeConfig } from '../constants'
911
import { DEEP_OBJECT_KEYS, DEFAULT_THEME } from '../constants'
@@ -12,11 +14,20 @@ import type { Context } from '../types'
1214
import { MenuProvider } from './menu'
1315

1416
type Config<FrontMatterType = FrontMatter> = DocsThemeConfig &
15-
Pick<PageOpts<FrontMatterType>, 'title' | 'frontMatter'>
17+
Pick<
18+
PageOpts<FrontMatterType>,
19+
'title' | 'frontMatter' | 'filePath' | 'timestamp'
20+
> & {
21+
hideSidebar: boolean
22+
normalizePagesResult: ReturnType<typeof normalizePages>
23+
}
1624

1725
const ConfigContext = createContext<Config>({
1826
title: '',
1927
frontMatter: {},
28+
filePath: '',
29+
hideSidebar: false,
30+
normalizePagesResult: {} as ReturnType<typeof normalizePages>,
2031
...DEFAULT_THEME
2132
})
2233
ConfigContext.displayName = 'Config'
@@ -100,10 +111,27 @@ export function ConfigProvider({
100111
// validateMeta(pageOpts.pageMap)
101112
isValidated = true
102113
}
114+
115+
const fsPath = useFSRoute()
116+
117+
const normalizePagesResult = useMemo(
118+
() => normalizePages({ list: pageOpts.pageMap, route: fsPath }),
119+
[pageOpts.pageMap, fsPath]
120+
)
121+
122+
const { activeType, activeThemeContext: themeContext } = normalizePagesResult
123+
103124
const extendedConfig: Config = {
104125
...theme,
105126
title: pageOpts.title,
106-
frontMatter: pageOpts.frontMatter
127+
frontMatter: pageOpts.frontMatter,
128+
filePath: pageOpts.filePath,
129+
timestamp: pageOpts.timestamp,
130+
hideSidebar:
131+
!themeContext.sidebar ||
132+
themeContext.layout === 'raw' ||
133+
activeType === 'page',
134+
normalizePagesResult
107135
}
108136

109137
const { nextThemes } = extendedConfig

‎packages/nextra-theme-docs/src/index.tsx

+9-205
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,28 @@
1-
import type { NextraMDXContent, NextraThemeLayoutProps, PageOpts } from 'nextra'
1+
import type { NextraThemeLayoutProps } from 'nextra'
22
import type { ReactElement, ReactNode } from 'react'
3-
import { useMemo } from 'react'
43
import 'focus-visible'
5-
import cn from 'clsx'
6-
import { useFSRoute, useMounted, useRouter } from 'nextra/hooks'
4+
import { useRouter } from 'nextra/hooks'
75
import { MDXProvider } from 'nextra/mdx'
86
import './polyfill'
9-
import type { PageTheme } from 'nextra/normalize-pages'
10-
import { normalizePages } from 'nextra/normalize-pages'
11-
import {
12-
Banner,
13-
Breadcrumb,
14-
Head,
15-
NavLinks,
16-
Sidebar,
17-
SkipNavContent
18-
} from './components'
7+
import { Banner, Head } from './components'
198
import { PartialDocsThemeConfig } from './constants'
209
import { ActiveAnchorProvider, ConfigProvider, useConfig } from './contexts'
2110
import { getComponents } from './mdx-components'
2211
import { renderComponent } from './utils'
2312

24-
interface BodyProps {
25-
themeContext: PageTheme
26-
breadcrumb: ReactNode
27-
timestamp?: number
28-
navigation: ReactNode
29-
children: ReactNode
30-
}
31-
32-
const classes = {
33-
toc: cn(
34-
'nextra-toc _order-last max-xl:_hidden _w-64 _shrink-0 print:_hidden'
35-
),
36-
main: cn('_w-full _break-words')
37-
}
38-
39-
function Body({
40-
themeContext,
41-
breadcrumb,
42-
timestamp,
43-
navigation,
44-
children
45-
}: BodyProps): ReactElement {
46-
const config = useConfig()
47-
const mounted = useMounted()
48-
49-
if (themeContext.layout === 'raw') {
50-
return <div className={classes.main}>{children}</div>
51-
}
52-
53-
const date =
54-
themeContext.timestamp && config.gitTimestamp && timestamp
55-
? new Date(timestamp)
56-
: null
57-
58-
const gitTimestampEl =
59-
// Because a user's time zone may be different from the server page
60-
mounted && date ? (
61-
<div className="_mt-12 _mb-8 _block _text-xs _text-gray-500 ltr:_text-right rtl:_text-left dark:_text-gray-400">
62-
{renderComponent(config.gitTimestamp, { timestamp: date })}
63-
</div>
64-
) : (
65-
<div className="_mt-16" />
66-
)
67-
68-
const content = (
69-
<>
70-
{children}
71-
{gitTimestampEl}
72-
{navigation}
73-
</>
74-
)
75-
76-
const body = config.main?.({ children: content }) || content
77-
78-
if (themeContext.layout === 'full') {
79-
return (
80-
<article
81-
className={cn(
82-
classes.main,
83-
'nextra-content _min-h-[calc(100vh-var(--nextra-navbar-height))] _pl-[max(env(safe-area-inset-left),1.5rem)] _pr-[max(env(safe-area-inset-right),1.5rem)]'
84-
)}
85-
>
86-
{body}
87-
</article>
88-
)
89-
}
90-
91-
return (
92-
<article
93-
className={cn(
94-
classes.main,
95-
'nextra-content _flex _min-h-[calc(100vh-var(--nextra-navbar-height))] _min-w-0 _justify-center _pb-8 _pr-[calc(env(safe-area-inset-right)-1.5rem)]',
96-
themeContext.typesetting === 'article' &&
97-
'nextra-body-typesetting-article'
98-
)}
99-
>
100-
<main className="_w-full _min-w-0 _max-w-6xl _px-6 _pt-4 md:_px-12">
101-
{breadcrumb}
102-
{body}
103-
</main>
104-
</article>
105-
)
106-
}
107-
108-
function InnerLayout({
109-
filePath,
110-
pageMap,
111-
timestamp,
112-
children
113-
}: PageOpts & { children: ReactNode }): ReactElement {
13+
function InnerLayout({ children }: { children: ReactNode }): ReactElement {
11414
const config = useConfig()
11515
const { locale } = useRouter()
116-
const fsPath = useFSRoute()
11716

118-
const {
119-
activeType,
120-
activeIndex,
121-
activeThemeContext: themeContext,
122-
activePath,
123-
directories,
124-
docsDirectories,
125-
flatDocsDirectories,
126-
topLevelNavbarItems
127-
} = useMemo(
128-
() => normalizePages({ list: pageMap, route: fsPath }),
129-
[pageMap, fsPath]
130-
)
131-
132-
const hideSidebar =
133-
!themeContext.sidebar ||
134-
themeContext.layout === 'raw' ||
135-
activeType === 'page'
13617
const { direction } = config.i18n.find(l => l.locale === locale) || config
13718
const dir = direction === 'rtl' ? 'rtl' : 'ltr'
13819

139-
const wrapper: NextraMDXContent = useMemo(
140-
() =>
141-
function NextraWrapper({ toc, children }) {
142-
const tocEl =
143-
activeType === 'page' ||
144-
!themeContext.toc ||
145-
themeContext.layout !== 'default' ? (
146-
themeContext.layout !== 'full' &&
147-
themeContext.layout !== 'raw' && (
148-
<nav className={classes.toc} aria-label="table of contents" />
149-
)
150-
) : (
151-
<nav
152-
className={cn(classes.toc, '_px-4')}
153-
aria-label="table of contents"
154-
>
155-
{renderComponent(config.toc.component, {
156-
toc: config.toc.float ? toc : [],
157-
filePath
158-
})}
159-
</nav>
160-
)
161-
return (
162-
<div
163-
className={cn(
164-
'_mx-auto _flex',
165-
themeContext.layout !== 'raw' && '_max-w-[90rem]'
166-
)}
167-
>
168-
<Sidebar
169-
docsDirectories={docsDirectories}
170-
fullDirectories={directories}
171-
toc={toc}
172-
asPopover={hideSidebar}
173-
includePlaceholder={themeContext.layout === 'default'}
174-
/>
175-
{tocEl}
176-
<SkipNavContent />
177-
<Body
178-
themeContext={themeContext}
179-
breadcrumb={
180-
activeType !== 'page' && themeContext.breadcrumb ? (
181-
<Breadcrumb activePath={activePath} />
182-
) : null
183-
}
184-
timestamp={timestamp}
185-
navigation={
186-
activeType !== 'page' && themeContext.pagination ? (
187-
<NavLinks
188-
flatDirectories={flatDocsDirectories}
189-
currentIndex={activeIndex}
190-
/>
191-
) : null
192-
}
193-
>
194-
{children}
195-
</Body>
196-
</div>
197-
)
198-
},
199-
[
200-
activeIndex,
201-
activePath,
202-
activeType,
203-
config.toc.component,
204-
config.toc.float,
205-
directories,
206-
docsDirectories,
207-
filePath,
208-
flatDocsDirectories,
209-
hideSidebar,
210-
themeContext,
211-
timestamp
212-
]
213-
)
20+
const { activeThemeContext: themeContext, topLevelNavbarItems } =
21+
config.normalizePagesResult
21422

21523
const components = getComponents({
21624
isRawLayout: themeContext.layout === 'raw',
217-
components: {
218-
...config.components,
219-
// @ts-expect-error fixme
220-
wrapper
221-
}
25+
components: config.components
22226
})
22327

22428
return (
@@ -243,7 +47,7 @@ function InnerLayout({
24347
</MDXProvider>
24448
</ActiveAnchorProvider>
24549
{themeContext.footer &&
246-
renderComponent(config.footer.component, { menu: hideSidebar })}
50+
renderComponent(config.footer.component, { menu: config.hideSidebar })}
24751
</div>
24852
)
24953
}
@@ -254,7 +58,7 @@ export default function Layout({
25458
}: NextraThemeLayoutProps): ReactElement {
25559
return (
25660
<ConfigProvider value={context}>
257-
<InnerLayout {...context.pageOpts}>{children}</InnerLayout>
61+
<InnerLayout>{children}</InnerLayout>
25862
</ConfigProvider>
25963
)
26064
}

‎packages/nextra-theme-docs/src/mdx-components.tsx

+160-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import cn from 'clsx'
2+
import type { NextraMDXContent } from 'nextra'
23
import { Code, Pre, Table, Td, Th, Tr } from 'nextra/components'
4+
import { useMounted } from 'nextra/hooks'
35
import { ArrowRightIcon } from 'nextra/icons'
46
import type { Components } from 'nextra/mdx'
5-
import type { ComponentProps, ReactElement } from 'react'
7+
import type { ComponentProps, ReactElement, ReactNode } from 'react'
68
import {
79
Children,
810
cloneElement,
@@ -11,11 +13,19 @@ import {
1113
useRef,
1214
useState
1315
} from 'react'
14-
import { Anchor, Collapse } from './components'
16+
import {
17+
Anchor,
18+
Breadcrumb,
19+
Collapse,
20+
NavLinks,
21+
Sidebar,
22+
SkipNavContent
23+
} from './components'
1524
import type { AnchorProps } from './components/anchor'
1625
import type { DocsThemeConfig } from './constants'
17-
import { useSetActiveAnchor } from './contexts'
26+
import { useConfig, useSetActiveAnchor } from './contexts'
1827
import { useIntersectionObserver, useSlugs } from './contexts/active-anchor'
28+
import { renderComponent } from './utils'
1929

2030
// Anchor links
2131
const createHeading = (
@@ -175,6 +185,83 @@ const A = ({ href = '', ...props }) => (
175185
<Anchor href={href} newWindow={EXTERNAL_HREF_REGEX.test(href)} {...props} />
176186
)
177187

188+
interface BodyProps {
189+
breadcrumb: ReactNode
190+
navigation: ReactNode
191+
children: ReactNode
192+
}
193+
194+
const classes = {
195+
toc: cn(
196+
'nextra-toc _order-last max-xl:_hidden _w-64 _shrink-0 print:_hidden'
197+
),
198+
main: cn('_w-full _break-words')
199+
}
200+
201+
function Body({ breadcrumb, navigation, children }: BodyProps): ReactElement {
202+
const config = useConfig()
203+
const mounted = useMounted()
204+
const themeContext = config.normalizePagesResult.activeThemeContext
205+
206+
if (themeContext.layout === 'raw') {
207+
return <div className={classes.main}>{children}</div>
208+
}
209+
210+
const date =
211+
themeContext.timestamp && config.gitTimestamp && config.timestamp
212+
? new Date(config.timestamp)
213+
: null
214+
215+
const gitTimestampEl =
216+
// Because a user's time zone may be different from the server page
217+
mounted && date ? (
218+
<div className="_mt-12 _mb-8 _block _text-xs _text-gray-500 ltr:_text-right rtl:_text-left dark:_text-gray-400">
219+
{renderComponent(config.gitTimestamp, { timestamp: date })}
220+
</div>
221+
) : (
222+
<div className="_mt-16" />
223+
)
224+
225+
const content = (
226+
<>
227+
{children}
228+
{gitTimestampEl}
229+
{navigation}
230+
</>
231+
)
232+
233+
const body = config.main?.({ children: content }) || content
234+
235+
if (themeContext.layout === 'full') {
236+
return (
237+
<article
238+
className={cn(
239+
classes.main,
240+
'nextra-content _min-h-[calc(100vh-var(--nextra-navbar-height))] _pl-[max(env(safe-area-inset-left),1.5rem)] _pr-[max(env(safe-area-inset-right),1.5rem)]'
241+
)}
242+
>
243+
{body}
244+
</article>
245+
)
246+
}
247+
248+
return (
249+
<article
250+
className={cn(
251+
classes.main,
252+
'nextra-content _flex _min-h-[calc(100vh-var(--nextra-navbar-height))] _min-w-0 _justify-center _pb-8 _pr-[calc(env(safe-area-inset-right)-1.5rem)]',
253+
themeContext.typesetting === 'article' &&
254+
'nextra-body-typesetting-article'
255+
)}
256+
>
257+
<main className="_w-full _min-w-0 _max-w-6xl _px-6 _pt-4 md:_px-12">
258+
{breadcrumb}
259+
{body}
260+
</main>
261+
</article>
262+
)
263+
}
264+
178265
const DEFAULT_COMPONENTS: Components = {
179266
h1: props => (
180267
<h1
@@ -221,7 +308,74 @@ const DEFAULT_COMPONENTS: Components = {
221308
details: Details,
222309
summary: Summary,
223310
pre: Pre,
224-
code: Code
311+
code: Code,
312+
wrapper: function NextraWrapper({ toc, children }) {
313+
const config = useConfig()
314+
const {
315+
activeType,
316+
activeThemeContext: themeContext,
317+
docsDirectories,
318+
directories,
319+
activePath,
320+
flatDocsDirectories,
321+
activeIndex
322+
} = config.normalizePagesResult
323+
324+
const tocEl =
325+
activeType === 'page' ||
326+
!themeContext.toc ||
327+
themeContext.layout !== 'default' ? (
328+
themeContext.layout !== 'full' &&
329+
themeContext.layout !== 'raw' && (
330+
<nav className={classes.toc} aria-label="table of contents" />
331+
)
332+
) : (
333+
<nav
334+
className={cn(classes.toc, '_px-4')}
335+
aria-label="table of contents"
336+
>
337+
{renderComponent(config.toc.component, {
338+
toc: config.toc.float ? toc : [],
339+
filePath: config.filePath
340+
})}
341+
</nav>
342+
)
343+
return (
344+
<div
345+
className={cn(
346+
'_mx-auto _flex',
347+
themeContext.layout !== 'raw' && '_max-w-[90rem]'
348+
)}
349+
>
350+
<Sidebar
351+
docsDirectories={docsDirectories}
352+
fullDirectories={directories}
353+
toc={toc}
354+
asPopover={config.hideSidebar}
355+
includePlaceholder={themeContext.layout === 'default'}
356+
/>
357+
{tocEl}
358+
<SkipNavContent />
359+
<Body
360+
breadcrumb={
361+
activeType !== 'page' &&
362+
themeContext.breadcrumb && <Breadcrumb activePath={activePath} />
363+
}
364+
navigation={
365+
activeType !== 'page' &&
366+
themeContext.pagination && (
367+
<NavLinks
368+
flatDirectories={flatDocsDirectories}
369+
currentIndex={activeIndex}
370+
/>
371+
)
372+
}
373+
>
374+
{children}
375+
</Body>
376+
</div>
377+
)
378+
} satisfies NextraMDXContent
225379
}
226380

227381
export function getComponents({
@@ -232,7 +386,8 @@ export function getComponents({
232386
components?: DocsThemeConfig['components']
233387
}): Components {
234388
if (isRawLayout) {
235-
return { a: A }
389+
// @ts-expect-error
390+
return { a: A, wrapper: DEFAULT_COMPONENTS.wrapper }
236391
}
237392

238393
const context = { index: 0 }

0 commit comments

Comments
 (0)
Please sign in to comment.