Skip to content

Revamp mobile navigation #3282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sweet-hornets-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gitbook': minor
---

Revamp mobile navigation
109 changes: 82 additions & 27 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/gitbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@gitbook/react-math": "workspace:*",
"@gitbook/react-openapi": "workspace:*",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.12",
"@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.0.7",
Expand Down
20 changes: 15 additions & 5 deletions packages/gitbook/src/components/Header/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useState } from 'react';
import { type ClassValue, tcls } from '@/lib/tailwind';

import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import { Slot } from '@radix-ui/react-slot';

import { Link, type LinkInsightsProps } from '../primitives';

Expand All @@ -25,13 +26,21 @@ export function DropdownMenu(props: {
children: React.ReactNode;
/** Custom styles */
className?: ClassValue;
/** Open the dropdown on hover */
/** Open the dropdown on hover
* @default false
*/
openOnHover?: boolean;
/** Whether to render the dropdown menu in a portal
* @default true
*/
withPortal?: boolean;
}) {
const { button, children, className, openOnHover = false } = props;
const { button, children, className, openOnHover = false, withPortal = true } = props;
const [hovered, setHovered] = useState(false);
const [clicked, setClicked] = useState(false);

const Portal = withPortal ? RadixDropdownMenu.Portal : Slot;

return (
<RadixDropdownMenu.Root
modal={false}
Expand All @@ -48,15 +57,16 @@ export function DropdownMenu(props: {
{button}
</RadixDropdownMenu.Trigger>

<RadixDropdownMenu.Portal>
<Portal>
<RadixDropdownMenu.Content
data-testid="dropdown-menu"
hideWhenDetached
collisionPadding={8}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
align="start"
className="z-40 animate-present pt-2"
sideOffset={8}
className="z-40 animate-present"
>
<div
className={tcls(
Expand All @@ -67,7 +77,7 @@ export function DropdownMenu(props: {
{children}
</div>
</RadixDropdownMenu.Content>
</RadixDropdownMenu.Portal>
</Portal>
</RadixDropdownMenu.Root>
);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/gitbook/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { HeaderLink } from './HeaderLink';
import { HeaderLinkMore } from './HeaderLinkMore';
import { HeaderLinks } from './HeaderLinks';
import { HeaderLogo } from './HeaderLogo';
import { HeaderMobileMenu } from './HeaderMobileMenu';
import { HeaderMobileMenuButton } from './HeaderMobileMenuButton';
import { SpacesDropdown } from './SpacesDropdown';

/**
Expand Down Expand Up @@ -76,7 +76,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo
'min-w-0 shrink items-center justify-start gap-2 lg:gap-4'
)}
>
<HeaderMobileMenu
<HeaderMobileMenuButton
className={tcls(
'lg:hidden',
'-ml-2',
Expand Down
57 changes: 0 additions & 57 deletions packages/gitbook/src/components/Header/HeaderMobileMenu.tsx

This file was deleted.

35 changes: 35 additions & 0 deletions packages/gitbook/src/components/Header/HeaderMobileMenuButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use client';

import { Icon } from '@gitbook/icons';

import { useMobileMenuSheet } from '@/components/MobileMenu/useMobileMenuSheet';
import { tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind';

/**
* Button to show/hide the table of content on mobile.
*/
export function HeaderMobileMenuButton(
props: Partial<React.ButtonHTMLAttributes<HTMLButtonElement>>
) {
const language = useLanguage();
const { open, setOpen } = useMobileMenuSheet();

const toggleNavigation = () => {
setOpen(!open);
};

return (
<button
{...props}
aria-label={tString(language, 'table_of_contents_button_label')}
onClick={toggleNavigation}
className={tcls(
'flex flex-row items-center rounded straight-corners:rounded-sm px-2 py-1',
props.className
)}
>
<Icon icon="bars" className="size-4 text-inherit" />
</button>
);
}
4 changes: 3 additions & 1 deletion packages/gitbook/src/components/Header/SpacesDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ export function SpacesDropdown(props: {
siteSpace: SiteSpace;
siteSpaces: SiteSpace[];
className?: string;
withPortal?: boolean;
}) {
const { context, siteSpace, siteSpaces, className } = props;
const { context, siteSpace, siteSpaces, className, withPortal } = props;

return (
<DropdownMenu
className={tcls(
'group-hover/dropdown:invisible', // Prevent hover from opening the dropdown, as it's annoying in this context
'group-focus-within/dropdown:group-hover/dropdown:visible' // When the dropdown is already open, it should remain visible when hovered
)}
withPortal={withPortal}
button={
<div
data-testid="space-dropdown-button"
Expand Down
26 changes: 26 additions & 0 deletions packages/gitbook/src/components/MobileMenu/MobileMenuScript.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions packages/gitbook/src/components/MobileMenu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useMobileMenuSheet';
export * from './MobileMenuScript';
12 changes: 12 additions & 0 deletions packages/gitbook/src/components/MobileMenu/useMobileMenuSheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { create } from 'zustand';

/**
* Hooks to manage the mobile menu sheet state.
*/
export const useMobileMenuSheet = create<{
open: boolean;
setOpen: (open: boolean) => void;
}>((set) => ({
open: false,
setOpen: (open) => set({ open }),
}));
99 changes: 56 additions & 43 deletions packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +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 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';
Expand Down Expand Up @@ -81,7 +81,6 @@ export function SpaceLayout(props: {
)}
>
<TableOfContents
context={context}
header={
withTopHeader ? null : (
<div
Expand All @@ -99,47 +98,61 @@ 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'
>
<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
/** Needed to avoid the dropdown being rendered in the wrong place when the mobile menu is open. */
withPortal={false}
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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export function PageGroupItem(props: {
'[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',
'[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
'[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint'
'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary',
'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint'
)}
>
<TOCPageIcon page={page} />
Expand Down
Loading