1- import { MenuItem , Menu , MenuButton , MenuItems } from "@headlessui/react"
1+ import { NavigationMenu } from "@base-ui-components/react/navigation-menu"
2+
23import clsx from "clsx"
34// eslint-disable-next-line no-restricted-imports -- since we don't need newWindow prop
45import NextLink from "next/link"
56import { Button } from "nextra/components"
67import { useFSRoute } from "nextra/hooks"
78import 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"
915import { useMenu , useThemeConfig } from "nextra-theme-docs"
1016import { Anchor } from "@/app/conf/_design-system/anchor"
1117import { 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
2530function 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- }
0 commit comments