UI: Disable hover to open navbar menu by default, can be enabled via nav.enableHoverToOpen

This commit is contained in:
Fuma Nama
2025-01-14 19:01:20 +08:00
parent 6f5d5d03dc
commit 036f8e1226
5 changed files with 130 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
Disable hover to open navbar menu by default, can be enabled via `nav.enableHoverToOpen`
@@ -1,3 +1,4 @@
'use client';
import * as React from 'react';
import * as Primitive from '@radix-ui/react-navigation-menu';
import { cn } from '@/utils/cn';
@@ -6,7 +7,20 @@ const NavigationMenu = Primitive.Root;
const NavigationMenuList = Primitive.List;
const NavigationMenuItem = Primitive.Item;
const NavigationMenuItem = React.forwardRef<
React.ComponentRef<typeof Primitive.NavigationMenuItem>,
React.ComponentPropsWithoutRef<typeof Primitive.NavigationMenuItem>
>(({ className, children, ...props }, ref) => (
<Primitive.NavigationMenuItem
ref={ref}
className={cn('list-none', className)}
{...props}
>
{children}
</Primitive.NavigationMenuItem>
));
NavigationMenuItem.displayName = Primitive.NavigationMenuItem.displayName;
const NavigationMenuTrigger = React.forwardRef<
React.ComponentRef<typeof Primitive.Trigger>,
+33 -28
View File
@@ -1,20 +1,15 @@
import type { HTMLAttributes } from 'react';
import { replaceOrDefault } from '@/layouts/shared';
import { type NavOptions, replaceOrDefault } from '@/layouts/shared';
import { cn } from '@/utils/cn';
import { getLinks, type BaseLayoutProps } from './shared';
import { NavProvider, Title } from '@/components/layout/nav';
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuList,
NavigationMenuTrigger,
} from '@/components/ui/navigation-menu';
import { NavigationMenuList } from '@/components/ui/navigation-menu';
import {
Navbar,
NavbarLink,
NavbarMenu,
NavbarMenuContent,
NavbarMenuItem,
NavbarMenuLink,
NavbarMenuTrigger,
} from '@/layouts/home/navbar';
import { type LinkItemType } from '@/layouts/links';
@@ -28,12 +23,27 @@ import {
LanguageToggleText,
} from '@/components/layout/language-toggle';
import { ChevronDown, Languages } from 'lucide-react';
import { buttonVariants } from '@/components/ui/button';
import { SearchOnly } from '@/contexts/search';
import Link from 'fumadocs-core/link';
import { MenuLinkItem } from '@/layouts/home/menu';
import {
Menu,
MenuContent,
MenuLinkItem,
MenuTrigger,
} from '@/layouts/home/menu';
export type HomeLayoutProps = BaseLayoutProps & HTMLAttributes<HTMLElement>;
export interface HomeLayoutProps
extends BaseLayoutProps,
HTMLAttributes<HTMLElement> {
nav?: Partial<
NavOptions & {
/**
* Open mobile menu when hovering the trigger
*/
enableHoverToOpen?: boolean;
}
>;
}
export function HomeLayout(props: HomeLayoutProps) {
const {
@@ -68,7 +78,7 @@ function Header({
i18n = false,
finalLinks,
disableThemeSwitch,
}: BaseLayoutProps & {
}: HomeLayoutProps & {
finalLinks: LinkItemType[];
}) {
const navItems = finalLinks.filter((item) =>
@@ -106,22 +116,17 @@ function Header({
<NavbarLinkItem
key={i}
item={item}
className="-me-1.5 list-none max-lg:hidden"
className="-me-1.5 max-lg:hidden"
/>
))}
<NavigationMenuItem className="list-none lg:hidden">
<NavigationMenuTrigger
className={cn(
buttonVariants({
size: 'icon',
color: 'ghost',
}),
'group -me-2',
)}
<Menu className="lg:hidden">
<MenuTrigger
className="group -me-2"
enableHover={nav.enableHoverToOpen}
>
<ChevronDown className="size-3 transition-transform duration-300 group-data-[state=open]:rotate-180" />
</NavigationMenuTrigger>
<NavigationMenuContent className="flex flex-col p-4 sm:flex-row sm:items-center sm:justify-end">
</MenuTrigger>
<MenuContent className="sm:flex-row sm:items-center sm:justify-end">
{menuItems
.filter((item) => !isSecondary(item))
.map((item, i) => (
@@ -141,8 +146,8 @@ function Header({
) : null}
{!disableThemeSwitch ? <ThemeToggle /> : null}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
</MenuContent>
</Menu>
</div>
</Navbar>
);
@@ -164,7 +169,7 @@ function NavbarLinkItem({
const { banner, footer, ...rest } = child.menu ?? {};
return (
<NavbarMenuItem key={j} href={child.url} {...rest}>
<NavbarMenuLink key={j} href={child.url} {...rest}>
{banner ??
(child.icon ? (
<div className="w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4">
@@ -178,7 +183,7 @@ function NavbarLinkItem({
</p>
) : null}
{footer}
</NavbarMenuItem>
</NavbarMenuLink>
);
});
+59 -10
View File
@@ -1,10 +1,16 @@
'use client';
import { BaseLinkItem, type LinkItemType } from '@/layouts/links';
import { cn } from '@/utils/cn';
import { NavigationMenuLink } from '@/components/ui/navigation-menu';
import {
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuTrigger,
} from '@/components/ui/navigation-menu';
import Link from 'fumadocs-core/link';
import { cva } from 'class-variance-authority';
import { buttonVariants } from '@/components/ui/button';
import type { ComponentPropsWithoutRef } from 'react';
const menuItemVariants = cva('', {
variants: {
@@ -35,22 +41,23 @@ export function MenuLinkItem({
if (item.type === 'custom')
return <div className={cn('grid', props.className)}>{item.children}</div>;
if (item.type === 'menu')
if (item.type === 'menu') {
const header = (
<>
{item.icon}
{item.text}
</>
);
return (
<div className={cn('mb-4 flex flex-col', props.className)}>
<p className="mb-1 text-sm text-fd-muted-foreground">
{item.url ? (
<NavigationMenuLink asChild>
<Link href={item.url}>
{item.icon}
{item.text}
</Link>
<Link href={item.url}>{header}</Link>
</NavigationMenuLink>
) : (
<>
{item.icon}
{item.text}
</>
header
)}
</p>
{item.items.map((child, i) => (
@@ -58,6 +65,7 @@ export function MenuLinkItem({
))}
</div>
);
}
return (
<NavigationMenuLink asChild>
@@ -75,3 +83,44 @@ export function MenuLinkItem({
</NavigationMenuLink>
);
}
export const Menu = NavigationMenuItem;
export function MenuTrigger({
enableHover = false,
...props
}: ComponentPropsWithoutRef<typeof NavigationMenuTrigger> & {
/**
* Enable hover to trigger
*/
enableHover?: boolean;
}) {
return (
<NavigationMenuTrigger
{...props}
onPointerMove={enableHover ? undefined : (e) => e.preventDefault()}
className={cn(
buttonVariants({
size: 'icon',
color: 'ghost',
}),
props.className,
)}
>
{props.children}
</NavigationMenuTrigger>
);
}
export function MenuContent(
props: ComponentPropsWithoutRef<typeof NavigationMenuContent>,
) {
return (
<NavigationMenuContent
{...props}
className={cn('flex flex-col p-4', props.className)}
>
{props.children}
</NavigationMenuContent>
);
}
+18 -16
View File
@@ -76,7 +76,23 @@ export function NavbarMenuTrigger(props: NavigationMenuTriggerProps) {
);
}
const linkVariants = cva(undefined, {
export function NavbarMenuLink(props: LinkProps) {
return (
<NavigationMenuLink asChild>
<Link
{...props}
className={cn(
'flex flex-col gap-2 rounded-lg border bg-fd-card p-3 transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground',
props.className,
)}
>
{props.children}
</Link>
</NavigationMenuLink>
);
}
const linkVariants = cva('', {
variants: {
variant: {
main: navItemVariants(),
@@ -101,7 +117,7 @@ export function NavbarLink({
...props
}: ComponentProps<typeof BaseLinkItem> & VariantProps<typeof linkVariants>) {
return (
<NavigationMenuItem className="list-none">
<NavigationMenuItem>
<NavigationMenuLink asChild>
<BaseLinkItem
{...props}
@@ -114,17 +130,3 @@ export function NavbarLink({
</NavigationMenuItem>
);
}
export function NavbarMenuItem(props: LinkProps) {
return (
<NavigationMenuLink asChild>
<Link
{...props}
className={cn(
'flex flex-col gap-2 rounded-lg border bg-fd-card p-3 transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground',
props.className,
)}
/>
</NavigationMenuLink>
);
}