mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Fix MobileMenu
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useMobileMenuSheet } from '@/components/MobileMenu/useMobileMenuSheet';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
/**
|
||||
* Button to show/hide the table of content on mobile.
|
||||
@@ -15,18 +13,12 @@ export function HeaderMobileMenuButton(
|
||||
props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>
|
||||
) {
|
||||
const language = useLanguage();
|
||||
const pathname = usePathname();
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
const toggleNavigation = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Close the navigation when navigating to a page
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useMobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function MobileMenuScript() {
|
||||
const pathname = usePathname();
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Close the navigation when navigating to a page
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// If the menu is open, we add a class to the body to prevent scrolling
|
||||
if (open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/primitives/Sheet';
|
||||
import { useMobileMenuSheet } from './useMobileMenuSheet';
|
||||
|
||||
export function MobileMenuSheet(props: { children: React.ReactNode }) {
|
||||
const { children } = props;
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent
|
||||
aria-label="Mobile menu"
|
||||
overlayClassName="lg:hidden"
|
||||
className="pb-2 lg:hidden"
|
||||
>
|
||||
{/** Needed for screen readers */}
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Table of contents</SheetTitle>
|
||||
<SheetDescription>A list of all the pages in the site.</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{children}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './MobileMenuSheet';
|
||||
export * from './useMobileMenuSheet';
|
||||
export * from './MobileMenuScript';
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import { MobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { MobileMenuScript, useMobileMenuSheet } from '@/components/MobileMenu';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import type React from 'react';
|
||||
import { TableOfContentsScript } from './TableOfContentsScript';
|
||||
|
||||
export function TableOfContents(props: {
|
||||
@@ -12,30 +11,59 @@ export function TableOfContents(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
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>;
|
||||
}
|
||||
const { open, setOpen } = useMobileMenuSheet();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-40 bg-tint-12/4 backdrop-blur-lg data-[state=closed]:pointer-events-none data-[state=closed]:invisible data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn data-[state=open]:animate-in lg:hidden dark:bg-tint-1/6"
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<aside // Sidebar container, responsible for setting the right dimensions and position for the sidebar.
|
||||
data-testid="table-of-contents"
|
||||
id="table-of-contents"
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className={tcls(
|
||||
'group',
|
||||
|
||||
'flex',
|
||||
'flex-col',
|
||||
|
||||
'max-lg:fixed',
|
||||
'max-lg:z-50',
|
||||
|
||||
'max-lg:transition',
|
||||
'max-lg:ease-in-out',
|
||||
'max-lg:duration-500',
|
||||
|
||||
'max-lg:rounded-xl',
|
||||
'max-lg:circular-corners:rounded-2xl',
|
||||
'max-lg:straight-corners:rounded-none',
|
||||
'max-lg:bg-tint-base',
|
||||
'max-lg:sidebar-filled:bg-tint-subtle',
|
||||
'max-lg:theme-muted:bg-tint-subtle',
|
||||
'max-lg:[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'max-lg:[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'max-lg:[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
|
||||
'max-lg:w-10/12',
|
||||
'max-lg:shadow-lg',
|
||||
'max-lg:depth-flat:shadow-none',
|
||||
'max-lg:inset-1.5',
|
||||
'max-lg:transition-all',
|
||||
'max-lg:duration-300',
|
||||
'max-lg:max-w-sm',
|
||||
'max-lg:data-[state=open]:left-1.5',
|
||||
'max-lg:data-[state=closed]:-left-full',
|
||||
|
||||
'text-sm',
|
||||
|
||||
'grow-0',
|
||||
'shrink-0',
|
||||
'basis-full',
|
||||
'lg:basis-72',
|
||||
'page-no-toc:lg:basis-56',
|
||||
|
||||
'relative',
|
||||
'z-[1]',
|
||||
'lg:sticky',
|
||||
'lg:mr-12',
|
||||
|
||||
@@ -56,25 +84,37 @@ export function TableOfContents(props: {
|
||||
'[html[style*="--toc-top-offset"]_&]:lg:!top-[var(--toc-top-offset)]',
|
||||
'[html[style*="--toc-height"]_&]:lg:!h-[var(--toc-height)]',
|
||||
|
||||
'pt-6',
|
||||
'pb-4',
|
||||
'lg:pt-6',
|
||||
'lg:pb-4',
|
||||
'sidebar-filled:lg:pr-6',
|
||||
'page-no-toc:lg:pr-0',
|
||||
|
||||
'hidden',
|
||||
'lg:flex',
|
||||
'page-no-toc:lg:hidden',
|
||||
'page-no-toc:xl:flex',
|
||||
'site-header-none:page-no-toc:lg:flex',
|
||||
'flex-col',
|
||||
'gap-4',
|
||||
'border-tint-subtle'
|
||||
)}
|
||||
>
|
||||
{header && header}
|
||||
|
||||
{open ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="close"
|
||||
iconOnly
|
||||
autoFocus={false}
|
||||
className="absolute top-2 right-2 z-50 bg-transparent text-tint opacity-8 shadow-none ring-transparent"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{children}
|
||||
</aside>
|
||||
<TableOfContentsScript />
|
||||
<MobileMenuScript />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ export function Trademark(props: {
|
||||
|
||||
'left-2',
|
||||
'right-2',
|
||||
'bottom-0',
|
||||
'bottom-2',
|
||||
'lg:bottom-0',
|
||||
'lg:left-0',
|
||||
|
||||
'pt-2',
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog';
|
||||
import type * as React from 'react';
|
||||
|
||||
import { Button } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* Root component for the Sheet dialog.
|
||||
* Wraps the Radix UI Dialog.Root component and provides the context for the sheet dialog.
|
||||
*
|
||||
* @example
|
||||
* <Sheet>
|
||||
* <SheetTrigger>Open</SheetTrigger>
|
||||
* <SheetContent>
|
||||
* <SheetHeader>
|
||||
* <SheetTitle>Title</SheetTitle>
|
||||
* <SheetDescription>Description</SheetDescription>
|
||||
* </SheetHeader>
|
||||
* </SheetContent>
|
||||
* </Sheet>
|
||||
*/
|
||||
export function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger component that opens the sheet dialog.
|
||||
* Typically used as a button or other interactive element to open the sheet.
|
||||
*/
|
||||
export function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close component that closes the sheet dialog.
|
||||
* Can be used to create custom close buttons or triggers.
|
||||
*/
|
||||
export function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Portal component that renders the sheet content outside the DOM hierarchy.
|
||||
* Ensures proper stacking context and accessibility.
|
||||
*/
|
||||
export function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay component that creates a semi-transparent backdrop behind the sheet.
|
||||
* Includes blur effect and animation states for opening/closing.
|
||||
*/
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={tcls(
|
||||
'fixed inset-0 z-30 bg-tint-12/4 backdrop-blur-lg data-[state=closed]:animate-fadeIn data-[state=closed]:animate-out data-[state=open]:animate-fadeOut data-[state=open]:animate-in dark:bg-tint-1/6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface SheetContentProps extends React.ComponentProps<typeof SheetPrimitive.Content> {
|
||||
/**
|
||||
* The class name for the overlay component.
|
||||
*/
|
||||
overlayClassName?: string;
|
||||
/**
|
||||
* The side the sheet slides in from.
|
||||
* @default 'left'
|
||||
*/
|
||||
side?: 'right' | 'left';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main content component for the sheet dialog.
|
||||
* Handles positioning, animations, and styling of the sheet content.
|
||||
*/
|
||||
export function SheetContent(props: SheetContentProps) {
|
||||
const { overlayClassName, side = 'left', className, children, ...rest } = props;
|
||||
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay className={overlayClassName} />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={tcls(
|
||||
'fixed',
|
||||
'z-30',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'shadow-lg',
|
||||
'depth-flat:shadow-none',
|
||||
|
||||
'transition ease-in-out',
|
||||
'data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
|
||||
'border',
|
||||
'border-tint-subtle',
|
||||
|
||||
'inset-x-1.5',
|
||||
'inset-y-1.5',
|
||||
|
||||
'w-10/12',
|
||||
'sm:max-w-sm',
|
||||
|
||||
'bg-tint-base',
|
||||
'sidebar-filled:bg-tint-subtle',
|
||||
'theme-muted:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'[html.sidebar-filled.theme-muted_&]:bg-tint-base',
|
||||
'[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base',
|
||||
|
||||
'rounded-xl',
|
||||
'circular-corners:rounded-2xl',
|
||||
'straight-corners:rounded-none',
|
||||
side === 'right' &&
|
||||
'right-1.5 data-[state=closed]:animate-exitToRight data-[state=open]:animate-enterFromRight',
|
||||
side === 'left' &&
|
||||
'left-1.5 data-[state=closed]:animate-exitToLeft data-[state=open]:animate-enterFromLeft',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
<SheetClose asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon="close"
|
||||
iconOnly
|
||||
autoFocus={false}
|
||||
className="absolute top-2 right-2 z-50 bg-transparent text-tint opacity-8 shadow-none ring-transparent"
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Header component for the sheet dialog.
|
||||
* Provides consistent padding and layout for the sheet header area.
|
||||
*/
|
||||
export function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={tcls('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Title component for the sheet dialog.
|
||||
* Renders the main heading of the sheet with consistent styling.
|
||||
*/
|
||||
export function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={tcls('font-semibold text-tint-default', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Description component for the sheet dialog.
|
||||
* Renders supplementary text or description with consistent styling.
|
||||
*/
|
||||
export function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={tcls('text-sm text-tint-weak', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user