Skip to content

Commit eef6d2f

Browse files
committed
Render TOCScrollContent only once
1 parent 7b6dff7 commit eef6d2f

File tree

5 files changed

+131
-75
lines changed

5 files changed

+131
-75
lines changed

packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx

Lines changed: 54 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ import React from 'react';
55
import { Footer } from '@/components/Footer';
66
import { Header, HeaderLogo } from '@/components/Header';
77
import { SearchButton, SearchModal } from '@/components/Search';
8-
import { TableOfContents } from '@/components/TableOfContents';
8+
import { TOCScrollContent, TableOfContents } from '@/components/TableOfContents';
99
import { CONTAINER_STYLE } from '@/components/layout';
1010
import { getSpaceLanguage } from '@/intl/server';
1111
import { t } from '@/intl/translate';
12+
import type { VisitorAuthClaims } from '@/lib/adaptive';
1213
import { tcls } from '@/lib/tailwind';
1314

14-
import { MobileMenuSheet } from '@/components/MobileMenu';
15-
import { TOCScrollContent } from '@/components/TableOfContents/TOCScrollContent';
16-
import type { VisitorAuthClaims } from '@/lib/adaptive';
1715
import { GITBOOK_API_PUBLIC_URL, GITBOOK_APP_URL } from '@v2/lib/env';
1816
import { Announcement } from '../Announcement';
1917
import { SpacesDropdown } from '../Header/SpacesDropdown';
@@ -68,27 +66,6 @@ export function SpaceLayout(props: {
6866
>
6967
<Announcement context={context} />
7068
<Header withTopHeader={withTopHeader} context={context} />
71-
<MobileMenuSheet>
72-
<TOCScrollContent
73-
innerHeader={
74-
isMultiVariants ? (
75-
<SpacesDropdown
76-
withPortal={false}
77-
context={context}
78-
siteSpace={siteSpace}
79-
siteSpaces={siteSpaces}
80-
className={tcls(
81-
'w-full',
82-
'page-no-toc:hidden',
83-
'site-header-none:page-no-toc:flex',
84-
'h-8'
85-
)}
86-
/>
87-
) : null
88-
}
89-
context={context}
90-
/>
91-
</MobileMenuSheet>
9269
<div className="scroll-nojump">
9370
<div
9471
className={tcls(
@@ -104,7 +81,6 @@ export function SpaceLayout(props: {
10481
)}
10582
>
10683
<TableOfContents
107-
context={context}
10884
header={
10985
withTopHeader ? null : (
11086
<div
@@ -122,48 +98,59 @@ export function SpaceLayout(props: {
12298
</div>
12399
)
124100
}
125-
innerHeader={
126-
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
127-
<>
128-
{!withTopHeader && (
129-
<div className={tcls('hidden', 'lg:block')}>
130-
<React.Suspense fallback={null}>
131-
<SearchButton>
132-
<span className={tcls('flex-1')}>
133-
{t(
134-
getSpaceLanguage(customization),
135-
customization.aiSearch.enabled
136-
? 'search_or_ask'
137-
: 'search'
138-
)}
139-
...
140-
</span>
141-
</SearchButton>
142-
</React.Suspense>
143-
</div>
144-
)}
145-
{!withTopHeader && withSections && sections && (
146-
<SiteSectionList
147-
className={tcls('hidden', 'lg:block')}
148-
sections={encodeClientSiteSections(context, sections)}
149-
/>
150-
)}
151-
{isMultiVariants && (
152-
<SpacesDropdown
153-
context={context}
154-
siteSpace={siteSpace}
155-
siteSpaces={siteSpaces}
156-
className={tcls(
157-
'w-full',
158-
'page-no-toc:hidden',
159-
'site-header-none:page-no-toc:flex',
160-
'mb-2'
101+
>
102+
<TOCScrollContent
103+
context={context}
104+
innerHeader={
105+
!withTopHeader || isMultiVariants ? (
106+
// displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
107+
<>
108+
{!withTopHeader && (
109+
<div className={tcls('hidden', 'lg:block')}>
110+
<React.Suspense fallback={null}>
111+
<SearchButton>
112+
<span className={tcls('flex-1')}>
113+
{t(
114+
getSpaceLanguage(customization),
115+
customization.aiSearch.enabled
116+
? 'search_or_ask'
117+
: 'search'
118+
)}
119+
...
120+
</span>
121+
</SearchButton>
122+
</React.Suspense>
123+
</div>
161124
)}
162-
/>
163-
)}
164-
</>
165-
}
166-
/>
125+
{!withTopHeader && withSections && sections && (
126+
<SiteSectionList
127+
className={tcls('hidden', 'lg:block')}
128+
sections={encodeClientSiteSections(
129+
context,
130+
sections
131+
)}
132+
/>
133+
)}
134+
{isMultiVariants && (
135+
<SpacesDropdown
136+
context={context}
137+
siteSpace={siteSpace}
138+
siteSpaces={siteSpaces}
139+
className={tcls(
140+
'w-full',
141+
'page-no-toc:hidden',
142+
'site-header-none:page-no-toc:flex',
143+
'mb-2',
144+
// Set the height to match the close button of the mobile menu sheet
145+
'max-lg:h-8'
146+
)}
147+
/>
148+
)}
149+
</>
150+
) : null
151+
}
152+
/>
153+
</TableOfContents>
167154
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
168155
</div>
169156
</div>

packages/gitbook/src/components/TableOfContents/TOCScrollContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export function TOCScrollContent(props: {
4040
<TOCScrollContainer // The scrollview inside the sidebar
4141
className={tcls(
4242
'flex flex-grow flex-col p-2',
43-
innerHeader ? 'mt-0 lg:mt-2' : 'mt-8',
44-
customization.trademark.enabled && 'pb-20',
43+
innerHeader && 'mt-0 lg:mt-2',
44+
customization.trademark.enabled && 'pb-[4.5rem]',
4545
'gutter-stable overflow-y-auto',
4646
'[&::-webkit-scrollbar]:bg-transparent',
4747
'[&::-webkit-scrollbar-thumb]:bg-transparent',

packages/gitbook/src/components/TableOfContents/TableOfContents.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import type { GitBookSiteContext } from '@v2/lib/context';
1+
'use client';
2+
23
import type React from 'react';
34

4-
import { TOCScrollContent } from '@/components/TableOfContents/TOCScrollContent';
5+
import { MobileMenuSheet } from '@/components/MobileMenu';
6+
import { useIsMobile } from '@/hooks/useIsMobile';
57
import { tcls } from '@/lib/tailwind';
68
import { TableOfContentsScript } from './TableOfContentsScript';
79

810
export function TableOfContents(props: {
9-
context: GitBookSiteContext;
1011
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
11-
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
12+
children: React.ReactNode;
1213
}) {
13-
const { innerHeader, context, header } = props;
14+
const { header, children } = props;
15+
const isMobile = useIsMobile();
16+
17+
// If the screen is mobile, we use the mobile menu sheet to display the table of contents.
18+
if (isMobile) {
19+
return <MobileMenuSheet>{children}</MobileMenuSheet>;
20+
}
1421

1522
return (
1623
<>
@@ -65,7 +72,7 @@ export function TableOfContents(props: {
6572
)}
6673
>
6774
{header && header}
68-
<TOCScrollContent context={context} innerHeader={innerHeader} />
75+
{children}
6976
</aside>
7077
<TableOfContentsScript />
7178
</>

packages/gitbook/src/components/TableOfContents/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export { TableOfContents } from './TableOfContents';
22
export { PagesList } from './PagesList';
33
export { TOCScrollContainer } from './TOCScroller';
44
export { Trademark } from './Trademark';
5+
export { TOCScrollContent } from './TOCScrollContent';
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useSyncExternalStore } from 'react';
2+
3+
/**
4+
* Hook to check if the screen is mobile
5+
* @default breakpoint => 1024
6+
*/
7+
export function useIsMobile(breakpoint = 1024) {
8+
const store = getMediaQueryStore(breakpoint);
9+
10+
return useSyncExternalStore(
11+
(cb) => {
12+
store.subscribers.add(cb);
13+
return () => store.subscribers.delete(cb);
14+
},
15+
() => store.isMatch,
16+
() => false
17+
);
18+
}
19+
20+
type MediaQueryStore = {
21+
/** Latest match result */
22+
isMatch: boolean;
23+
/** The native MediaQueryList object */
24+
mediaQueryList: MediaQueryList;
25+
/** React subscribers that need re-rendering on change */
26+
subscribers: Set<() => void>;
27+
};
28+
29+
const mediaQueryStores: Record<string, MediaQueryStore> = {};
30+
31+
/**
32+
* getMediaQueryStore("(max-width: 1024px)")
33+
* Returns a singleton store for that query,
34+
* creating it (and its listener) the first time.
35+
*/
36+
function getMediaQueryStore(breakpoint: number): MediaQueryStore {
37+
// If the store already exists, return it
38+
if (mediaQueryStores[breakpoint]) return mediaQueryStores[breakpoint];
39+
40+
const queryString = `(max-width: ${breakpoint - 0.1}px)`;
41+
const mqList =
42+
typeof window !== 'undefined' ? window.matchMedia(queryString) : ({} as MediaQueryList);
43+
44+
const store: MediaQueryStore = {
45+
isMatch: typeof window !== 'undefined' ? mqList.matches : false,
46+
mediaQueryList: mqList,
47+
subscribers: new Set(),
48+
};
49+
50+
const update = () => {
51+
store.isMatch = mqList.matches;
52+
store.subscribers.forEach((cb) => cb());
53+
};
54+
55+
if (mqList.addEventListener) mqList.addEventListener('change', update);
56+
// For Safari < 14
57+
else if (mqList.addListener) mqList.addListener(update);
58+
59+
mediaQueryStores[breakpoint] = store;
60+
return store;
61+
}

0 commit comments

Comments
 (0)