Skip to content

Commit e2141d3

Browse files
committed
Migrate the Navbar to Base UI
1 parent 787f57b commit e2141d3

File tree

3 files changed

+119
-134
lines changed

3 files changed

+119
-134
lines changed

src/components/navbar/navbar.tsx

Lines changed: 114 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { MenuItem, Menu, MenuButton, MenuItems } from "@headlessui/react"
1+
import { NavigationMenu } from "@base-ui-components/react/navigation-menu"
2+
23
import clsx from "clsx"
34
// eslint-disable-next-line no-restricted-imports -- since we don't need newWindow prop
45
import NextLink from "next/link"
56
import { Button } from "nextra/components"
67
import { useFSRoute } from "nextra/hooks"
78
import type * as normalizePages from "nextra/normalize-pages"
8-
import { Fragment, useState, type ReactElement, type ReactNode } from "react"
9+
import {
10+
useCallback,
11+
useEffect,
12+
type ReactElement,
13+
type ReactNode,
14+
} from "react"
915
import { useMenu, useThemeConfig } from "nextra-theme-docs"
1016
import { Anchor } from "@/app/conf/_design-system/anchor"
1117
import { renderComponent } from "@/components/utils"
@@ -18,77 +24,53 @@ export interface NavBarProps {
1824
items: Item[]
1925
}
2026

21-
const classes = {
22-
link: "typography-menu flex items-center text-neu-900 px-3 py-1 nextra-focus [text-box:trim-both_cap_alphabetic] leading-none hover:underline underline-offset-2",
23-
}
27+
const linkClasses =
28+
"typography-menu flex items-center text-neu-900 px-3 py-1 nextra-focus [text-box:trim-both_cap_alphabetic] leading-none hover:underline underline-offset-2"
2429

2530
function NavbarMenu({
2631
menu,
2732
children,
28-
onSubmenuOpen,
2933
}: {
3034
menu: normalizePages.MenuItem
3135
children: ReactNode
32-
onSubmenuOpen: (open: boolean) => void
3336
}): ReactElement {
3437
const routes = Object.fromEntries(
3538
(menu.children || []).map(route => [route.name, route]),
3639
)
3740
return (
38-
<Menu>
39-
<MenuButton as={Fragment}>
40-
{({ focus, open }) => {
41-
// I'm sorry, I know this is so cursed.
42-
// I need to migrate out of HeadlessUI to something with change handlers.
43-
onSubmenuOpen(open)
44-
45-
return (
46-
<button
47-
onClick={() => onSubmenuOpen(open)}
48-
className={clsx(
49-
classes.link,
50-
"flex items-center gap-1.5 whitespace-nowrap max-md:hidden",
51-
focus && "nextra-focusable",
52-
)}
53-
>
54-
{children}
55-
</button>
56-
)
57-
}}
58-
</MenuButton>
59-
<MenuItems
60-
transition
61-
modal={false}
62-
className={({ open }) =>
63-
// eslint-disable-next-line tailwindcss/no-custom-classname
64-
clsx(
65-
"gql-navbar-menu-items",
66-
"motion-reduce:transition-none",
67-
"focus-visible:outline-none",
68-
open ? "opacity-100" : "opacity-0",
69-
"nextra-scrollbar overflow-visible transition-opacity",
70-
"z-20 rounded-md py-1 text-sm",
71-
// headlessui adds max-height as style, use !important to override
72-
"!max-h-[min(calc(100vh-5rem),256px)]",
73-
)
74-
}
75-
anchor={{ to: "top start", gap: 21, padding: 16, offset: -8 }}
41+
<NavigationMenu.Item className="max-md:hidden">
42+
<NavigationMenu.Trigger
43+
className={clsx(
44+
linkClasses,
45+
"focus-visible:nextra-focusable flex items-center gap-1.5 whitespace-nowrap data-[popup-open]:underline max-md:hidden",
46+
)}
7647
>
48+
{children}
49+
</NavigationMenu.Trigger>
50+
<NavigationMenu.Content className="flex flex-col py-1 text-sm">
7751
{Object.entries(menu.items || {}).map(([key, item]) => (
78-
<MenuItem key={key}>
79-
<Anchor
80-
href={item.href || routes[key]?.route}
81-
className="block py-1.5 pl-2 pr-9"
82-
target={item.newWindow ? "_blank" : undefined}
83-
>
84-
<span className="typography-menu px-3 py-1 underline-offset-2 [[data-active]>&]:underline">
85-
{item.title}
86-
</span>
87-
</Anchor>
88-
</MenuItem>
52+
<NavigationMenu.Link
53+
key={key}
54+
href={item.href || routes[key]?.route}
55+
target={item.newWindow ? "_blank" : undefined}
56+
className="block py-3.5 pl-2 pr-9"
57+
closeOnClick
58+
render={(linkProps, state) => (
59+
<Anchor {...linkProps}>
60+
<span
61+
className={clsx(
62+
"typography-menu px-3 py-1 underline-offset-2 hover:underline focus-visible:underline",
63+
state.active && "underline",
64+
)}
65+
>
66+
{item.title}
67+
</span>
68+
</Anchor>
69+
)}
70+
/>
8971
))}
90-
</MenuItems>
91-
</Menu>
72+
</NavigationMenu.Content>
73+
</NavigationMenu.Item>
9274
)
9375
}
9476

@@ -97,7 +79,21 @@ export function Navbar({ items }: NavBarProps): ReactElement {
9779

9880
const activeRoute = useFSRoute()
9981
const { menu, setMenu } = useMenu()
100-
const [submenuOpen, setSubmenuOpen] = useState(false)
82+
const handleNavigationMenuChange = useCallback((value: unknown) => {
83+
if (typeof document === "undefined") {
84+
return
85+
}
86+
document.body.style.overflow = value != null ? "hidden" : "auto"
87+
}, [])
88+
89+
useEffect(() => {
90+
if (typeof document === "undefined") {
91+
return
92+
}
93+
return () => {
94+
document.body.style.overflow = "auto"
95+
}
96+
}, [])
10197

10298
return (
10399
<div
@@ -125,61 +121,69 @@ export function Navbar({ items }: NavBarProps): ReactElement {
125121
</div>
126122
)}
127123
<div className="flex-1" />
128-
<div className="-mx-2 flex overflow-x-auto px-2 py-1.5 lg:gap-2 xl:absolute xl:left-1/2 xl:-translate-x-1/2">
129-
{items.map(pageOrMenu => {
130-
if (pageOrMenu.display === "hidden") return null
124+
<NavigationMenu.Root
125+
// onValueChange={handleNavigationMenuChange}
126+
className="-mx-2 flex overflow-x-auto px-2 py-1.5 xl:absolute xl:left-1/2 xl:-translate-x-1/2"
127+
render={props => <div {...props} />}
128+
>
129+
<NavigationMenu.List className="flex w-full items-center gap-2">
130+
{items.map(pageOrMenu => {
131+
if (pageOrMenu.display === "hidden") return null
131132

132-
if (pageOrMenu.type === "menu") {
133-
const menu = pageOrMenu as normalizePages.MenuItem
134-
return (
135-
<NavbarMenu
136-
key={menu.title}
137-
menu={menu}
138-
onSubmenuOpen={open => {
139-
if (typeof window !== "undefined") {
140-
if (open) {
141-
document.body.style.overflow = "hidden"
142-
} else {
143-
document.body.style.overflow = "auto"
144-
}
145-
}
146-
setSubmenuOpen(open)
147-
}}
148-
>
149-
{menu.title}
150-
</NavbarMenu>
151-
)
152-
}
153-
const page = pageOrMenu as normalizePages.PageItem
154-
let href = page.href || page.route || "#"
133+
if (pageOrMenu.type === "menu") {
134+
const menu = pageOrMenu as normalizePages.MenuItem
135+
return (
136+
<NavbarMenu key={menu.title} menu={menu}>
137+
{menu.title}
138+
</NavbarMenu>
139+
)
140+
}
141+
const page = pageOrMenu as normalizePages.PageItem
142+
let href = page.href || page.route || "#"
155143

156-
// If it's a directory
157-
if (page.children) {
158-
href =
159-
(page.withIndexPage ? page.route : page.firstChildRoute) || href
160-
}
144+
// If it's a directory
145+
if (page.children) {
146+
href =
147+
(page.withIndexPage ? page.route : page.firstChildRoute) ||
148+
href
149+
}
161150

162-
const isActive =
163-
page.route === activeRoute ||
164-
activeRoute.startsWith(page.route + "/")
151+
const isActive =
152+
page.route === activeRoute ||
153+
activeRoute.startsWith(page.route + "/")
165154

166-
return (
167-
<Anchor
168-
href={href}
169-
key={href}
170-
className={clsx(
171-
classes.link,
172-
"whitespace-nowrap max-md:hidden",
173-
isActive && !page.newWindow && "underline",
174-
)}
175-
target={page.newWindow ? "_blank" : undefined}
176-
aria-current={!page.newWindow && isActive}
177-
>
178-
{page.title}
179-
</Anchor>
180-
)
181-
})}
182-
</div>
155+
return (
156+
<NavigationMenu.Item key={href} className="max-md:hidden">
157+
<Anchor
158+
href={href}
159+
className={clsx(
160+
linkClasses,
161+
"whitespace-nowrap max-md:hidden",
162+
isActive && !page.newWindow && "underline",
163+
)}
164+
target={page.newWindow ? "_blank" : undefined}
165+
aria-current={!page.newWindow && isActive}
166+
>
167+
{page.title}
168+
</Anchor>
169+
</NavigationMenu.Item>
170+
)
171+
})}
172+
</NavigationMenu.List>
173+
<NavigationMenu.Portal keepMounted>
174+
<NavigationMenu.Backdrop className="fixed inset-0 top-[calc(var(--nextra-navbar-height)+1px)] !block bg-[rgb(var(--nextra-bg),.4)] opacity-100 backdrop-blur-[6.4px] transition-opacity data-[closed]:pointer-events-none data-[closed]:opacity-0" />
175+
<NavigationMenu.Positioner
176+
side="bottom"
177+
align="start"
178+
sideOffset={21}
179+
alignOffset={-8}
180+
>
181+
<NavigationMenu.Popup className="data-[closed]:animate-fade-out data-[open]:animate-fade-in">
182+
<NavigationMenu.Viewport className="nextra-scrollbar !max-h-[min(calc(100vh-5rem),256px)] overflow-visible transition-opacity focus-visible:outline-none motion-reduce:transition-none" />
183+
</NavigationMenu.Popup>
184+
</NavigationMenu.Positioner>
185+
</NavigationMenu.Portal>
186+
</NavigationMenu.Root>
183187

184188
{process.env.NEXTRA_SEARCH &&
185189
renderComponent(themeConfig.search.component, {
@@ -218,7 +222,6 @@ export function Navbar({ items }: NavBarProps): ReactElement {
218222
)}
219223
</Button>
220224
</nav>
221-
<SubmenuBackdrop className={submenuOpen ? "opacity-100" : "opacity-0"} />
222225
</div>
223226
)
224227
}
@@ -266,14 +269,3 @@ export function NavbarPlaceholder({
266269
/>
267270
)
268271
}
269-
270-
function SubmenuBackdrop({ className }: { className: string }) {
271-
return (
272-
<div
273-
className={clsx(
274-
"pointer-events-none fixed inset-0 top-[calc(var(--nextra-navbar-height)+1px)] bg-[rgb(var(--nextra-bg),.4)] backdrop-blur-[6.4px] transition-opacity",
275-
className,
276-
)}
277-
/>
278-
)
279-
}

src/globals.css

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,6 @@ footer {
6464
}
6565
}
6666

67-
div[role="menu"][data-headlessui-state] {
68-
@apply left-0 right-auto;
69-
}
70-
71-
div[id^="headlessui-menu-items"] {
72-
@apply rounded-none;
73-
74-
> a {
75-
@apply py-3.5;
76-
}
77-
}
78-
7967
/* should be fixed in Nextra 4 */
8068
._max-w-\[90rem\] {
8169
@apply container;

tailwind.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ const config: Config = {
8181
"show-overflow":
8282
"show-overflow var(--animation-duration, 12s) var(--animation-delay, 1s) var(--animation-direction, forwards) ease infinite",
8383
"fade-in": "fade-in var(--animation-duration, 200ms) ease-out forwards",
84+
"fade-out":
85+
"fade-out var(--animation-duration, 200ms) ease-out forwards",
8486
},
8587
keyframes: {
8688
scroll: {
@@ -108,6 +110,9 @@ const config: Config = {
108110
from: { opacity: "0" },
109111
to: { opacity: "1" },
110112
},
113+
"fade-out": {
114+
to: { opacity: "0" },
115+
},
111116
},
112117
},
113118
},

0 commit comments

Comments
 (0)