mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Render TOCScrollContent only once
This commit is contained in:
@@ -5,15 +5,13 @@ import React from 'react';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { Header, HeaderLogo } from '@/components/Header';
|
||||
import { SearchButton, SearchModal } from '@/components/Search';
|
||||
import { TableOfContents } from '@/components/TableOfContents';
|
||||
import { TOCScrollContent, TableOfContents } from '@/components/TableOfContents';
|
||||
import { CONTAINER_STYLE } from '@/components/layout';
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { t } from '@/intl/translate';
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { MobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { TOCScrollContent } from '@/components/TableOfContents/TOCScrollContent';
|
||||
import type { VisitorAuthClaims } from '@/lib/adaptive';
|
||||
import { GITBOOK_API_PUBLIC_URL, GITBOOK_APP_URL } from '@v2/lib/env';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
@@ -68,27 +66,6 @@ export function SpaceLayout(props: {
|
||||
>
|
||||
<Announcement context={context} />
|
||||
<Header withTopHeader={withTopHeader} context={context} />
|
||||
<MobileMenuSheet>
|
||||
<TOCScrollContent
|
||||
innerHeader={
|
||||
isMultiVariants ? (
|
||||
<SpacesDropdown
|
||||
withPortal={false}
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'page-no-toc:hidden',
|
||||
'site-header-none:page-no-toc:flex',
|
||||
'h-8'
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
context={context}
|
||||
/>
|
||||
</MobileMenuSheet>
|
||||
<div className="scroll-nojump">
|
||||
<div
|
||||
className={tcls(
|
||||
@@ -104,7 +81,6 @@ export function SpaceLayout(props: {
|
||||
)}
|
||||
>
|
||||
<TableOfContents
|
||||
context={context}
|
||||
header={
|
||||
withTopHeader ? null : (
|
||||
<div
|
||||
@@ -122,48 +98,59 @@ export function SpaceLayout(props: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
innerHeader={
|
||||
// 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.
|
||||
<>
|
||||
{!withTopHeader && (
|
||||
<div className={tcls('hidden', 'lg:block')}>
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchButton>
|
||||
<span className={tcls('flex-1')}>
|
||||
{t(
|
||||
getSpaceLanguage(customization),
|
||||
customization.aiSearch.enabled
|
||||
? 'search_or_ask'
|
||||
: 'search'
|
||||
)}
|
||||
...
|
||||
</span>
|
||||
</SearchButton>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
)}
|
||||
{!withTopHeader && withSections && sections && (
|
||||
<SiteSectionList
|
||||
className={tcls('hidden', 'lg:block')}
|
||||
sections={encodeClientSiteSections(context, sections)}
|
||||
/>
|
||||
)}
|
||||
{isMultiVariants && (
|
||||
<SpacesDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'page-no-toc:hidden',
|
||||
'site-header-none:page-no-toc:flex',
|
||||
'mb-2'
|
||||
>
|
||||
<TOCScrollContent
|
||||
context={context}
|
||||
innerHeader={
|
||||
!withTopHeader || isMultiVariants ? (
|
||||
// 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.
|
||||
<>
|
||||
{!withTopHeader && (
|
||||
<div className={tcls('hidden', 'lg:block')}>
|
||||
<React.Suspense fallback={null}>
|
||||
<SearchButton>
|
||||
<span className={tcls('flex-1')}>
|
||||
{t(
|
||||
getSpaceLanguage(customization),
|
||||
customization.aiSearch.enabled
|
||||
? 'search_or_ask'
|
||||
: 'search'
|
||||
)}
|
||||
...
|
||||
</span>
|
||||
</SearchButton>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!withTopHeader && withSections && sections && (
|
||||
<SiteSectionList
|
||||
className={tcls('hidden', 'lg:block')}
|
||||
sections={encodeClientSiteSections(
|
||||
context,
|
||||
sections
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isMultiVariants && (
|
||||
<SpacesDropdown
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
className={tcls(
|
||||
'w-full',
|
||||
'page-no-toc:hidden',
|
||||
'site-header-none:page-no-toc:flex',
|
||||
'mb-2',
|
||||
// Set the height to match the close button of the mobile menu sheet
|
||||
'max-lg:h-8'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</TableOfContents>
|
||||
<div className="flex min-w-0 flex-1 flex-col">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,8 +40,8 @@ export function TOCScrollContent(props: {
|
||||
<TOCScrollContainer // The scrollview inside the sidebar
|
||||
className={tcls(
|
||||
'flex flex-grow flex-col p-2',
|
||||
innerHeader ? 'mt-0 lg:mt-2' : 'mt-8',
|
||||
customization.trademark.enabled && 'pb-20',
|
||||
innerHeader && 'mt-0 lg:mt-2',
|
||||
customization.trademark.enabled && 'pb-[4.5rem]',
|
||||
'gutter-stable overflow-y-auto',
|
||||
'[&::-webkit-scrollbar]:bg-transparent',
|
||||
'[&::-webkit-scrollbar-thumb]:bg-transparent',
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import type { GitBookSiteContext } from '@v2/lib/context';
|
||||
'use client';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import { TOCScrollContent } from '@/components/TableOfContents/TOCScrollContent';
|
||||
import { MobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { TableOfContentsScript } from './TableOfContentsScript';
|
||||
|
||||
export function TableOfContents(props: {
|
||||
context: GitBookSiteContext;
|
||||
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
|
||||
innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { innerHeader, context, header } = props;
|
||||
const { header, children } = props;
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// If the screen is mobile, we use the mobile menu sheet to display the table of contents.
|
||||
if (isMobile) {
|
||||
return <MobileMenuSheet>{children}</MobileMenuSheet>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,7 +72,7 @@ export function TableOfContents(props: {
|
||||
)}
|
||||
>
|
||||
{header && header}
|
||||
<TOCScrollContent context={context} innerHeader={innerHeader} />
|
||||
{children}
|
||||
</aside>
|
||||
<TableOfContentsScript />
|
||||
</>
|
||||
|
||||
@@ -2,3 +2,4 @@ export { TableOfContents } from './TableOfContents';
|
||||
export { PagesList } from './PagesList';
|
||||
export { TOCScrollContainer } from './TOCScroller';
|
||||
export { Trademark } from './Trademark';
|
||||
export { TOCScrollContent } from './TOCScrollContent';
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
/**
|
||||
* Hook to check if the screen is mobile
|
||||
* @default breakpoint => 1024
|
||||
*/
|
||||
export function useIsMobile(breakpoint = 1024) {
|
||||
const store = getMediaQueryStore(breakpoint);
|
||||
|
||||
return useSyncExternalStore(
|
||||
(cb) => {
|
||||
store.subscribers.add(cb);
|
||||
return () => store.subscribers.delete(cb);
|
||||
},
|
||||
() => store.isMatch,
|
||||
() => false
|
||||
);
|
||||
}
|
||||
|
||||
type MediaQueryStore = {
|
||||
/** Latest match result */
|
||||
isMatch: boolean;
|
||||
/** The native MediaQueryList object */
|
||||
mediaQueryList: MediaQueryList;
|
||||
/** React subscribers that need re-rendering on change */
|
||||
subscribers: Set<() => void>;
|
||||
};
|
||||
|
||||
const mediaQueryStores: Record<string, MediaQueryStore> = {};
|
||||
|
||||
/**
|
||||
* getMediaQueryStore("(max-width: 1024px)")
|
||||
* Returns a singleton store for that query,
|
||||
* creating it (and its listener) the first time.
|
||||
*/
|
||||
function getMediaQueryStore(breakpoint: number): MediaQueryStore {
|
||||
// If the store already exists, return it
|
||||
if (mediaQueryStores[breakpoint]) return mediaQueryStores[breakpoint];
|
||||
|
||||
const queryString = `(max-width: ${breakpoint - 0.1}px)`;
|
||||
const mqList =
|
||||
typeof window !== 'undefined' ? window.matchMedia(queryString) : ({} as MediaQueryList);
|
||||
|
||||
const store: MediaQueryStore = {
|
||||
isMatch: typeof window !== 'undefined' ? mqList.matches : false,
|
||||
mediaQueryList: mqList,
|
||||
subscribers: new Set(),
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
store.isMatch = mqList.matches;
|
||||
store.subscribers.forEach((cb) => cb());
|
||||
};
|
||||
|
||||
if (mqList.addEventListener) mqList.addEventListener('change', update);
|
||||
// For Safari < 14
|
||||
else if (mqList.addListener) mqList.addListener(update);
|
||||
|
||||
mediaQueryStores[breakpoint] = store;
|
||||
return store;
|
||||
}
|
||||
Reference in New Issue
Block a user