Fix MobileMenu

This commit is contained in:
Nolann Biron
2025-06-13 09:29:25 +02:00
parent a2a852eb17
commit 92ae60fe87
7 changed files with 87 additions and 259 deletions
@@ -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}
/>
);
}