Skip to content
Merged
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
8 changes: 8 additions & 0 deletions packages/propel/src/components/navigation-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,24 @@ export {
type NavigationMenuArrowProps,
NavigationMenuContent,
type NavigationMenuContentProps,
NavigationMenuContentList,
type NavigationMenuContentListProps,
NavigationMenuIcon,
type NavigationMenuIconProps,
NavigationMenuItem,
type NavigationMenuItemProps,
NavigationMenuLink,
type NavigationMenuLinkProps,
NavigationMenuLinkDescription,
type NavigationMenuLinkDescriptionProps,
NavigationMenuLinkTitle,
type NavigationMenuLinkTitleProps,
NavigationMenuList,
type NavigationMenuListProps,
NavigationMenuTrigger,
type NavigationMenuTriggerProps,
NavigationMenuTriggerLabel,
type NavigationMenuTriggerLabelProps,
NavigationMenuViewport,
type NavigationMenuViewportProps,
} from "../../ui/navigation-menu";
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { expect, waitFor } from "storybook/test";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuContentList,
NavigationMenuIcon,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuLinkDescription,
NavigationMenuLinkTitle,
NavigationMenuList,
NavigationMenuPanel,
NavigationMenuTrigger,
NavigationMenuTriggerLabel,
NavigationMenuViewport,
} from "./index";

Expand All @@ -24,8 +28,14 @@ const meta = {
subcomponents: {
NavigationMenuList,
NavigationMenuItem,
NavigationMenuTrigger,
NavigationMenuTriggerLabel,
NavigationMenuIcon,
NavigationMenuContent,
NavigationMenuContentList,
NavigationMenuLink,
NavigationMenuLinkTitle,
NavigationMenuLinkDescription,
NavigationMenuPanel,
},
parameters: {
Expand Down Expand Up @@ -80,7 +90,31 @@ const RESOURCE_LINKS = [
// navigates the page, tears down the iframe, and fails unrelated stories.
const cancelNavigation = (event: React.MouseEvent) => event.preventDefault();

/** Two dropdown items plus a bare top-level link, opening into the shared `NavigationMenuPanel`. */
/** A rich content link pairing a title with a description, wrapped in its list item. */
function ContentLink({ href, title, description }: (typeof PRODUCT_LINKS)[number]) {
return (
<li>
<NavigationMenuLink variant="card" render={<a href={href} onClick={cancelNavigation} />}>
<NavigationMenuLinkTitle>{title}</NavigationMenuLinkTitle>
<NavigationMenuLinkDescription>{description}</NavigationMenuLinkDescription>
</NavigationMenuLink>
</li>
);
}

/** A trigger row that pairs the label with the rotating disclosure caret. */
function TriggerRow({ children }: { children: React.ReactNode }) {
return (
<NavigationMenuTrigger>
<NavigationMenuTriggerLabel>{children}</NavigationMenuTriggerLabel>
<NavigationMenuIcon>
<ChevronDown aria-hidden />
</NavigationMenuIcon>
</NavigationMenuTrigger>
);
}

/** Two menu items plus a bare top-level link, opening into the shared `NavigationMenuPanel`. */
export const Default: Story = {
render: () => (
<NavigationMenu>
Expand All @@ -90,12 +124,7 @@ export const Default: Story = {
<NavigationMenuContent>
<ul className="grid w-md grid-cols-2 gap-1 p-2">
{PRODUCT_LINKS.map((item) => (
<li key={item.href}>
<NavigationMenuLink render={<a href={item.href} onClick={cancelNavigation} />}>
<span className="block text-14 font-medium text-primary">{item.title}</span>
<span className="block text-12 text-tertiary">{item.description}</span>
</NavigationMenuLink>
</li>
<ContentLink key={item.href} {...item} />
))}
</ul>
</NavigationMenuContent>
Expand All @@ -104,21 +133,19 @@ export const Default: Story = {
<NavigationMenuItem>
<TriggerRow>Resources</TriggerRow>
<NavigationMenuContent>
<ul className="flex w-72 flex-col gap-1 p-2">
<NavigationMenuContentList>
{RESOURCE_LINKS.map((item) => (
<li key={item.href}>
<NavigationMenuLink render={<a href={item.href} onClick={cancelNavigation} />}>
<span className="block text-14 font-medium text-primary">{item.title}</span>
<span className="block text-12 text-tertiary">{item.description}</span>
</NavigationMenuLink>
</li>
<ContentLink key={item.href} {...item} />
))}
</ul>
</NavigationMenuContentList>
</NavigationMenuContent>
</NavigationMenuItem>

<NavigationMenuItem>
<NavigationMenuLink render={<a href="#pricing" onClick={cancelNavigation} />}>
<NavigationMenuLink
variant="item"
render={<a href="#pricing" onClick={cancelNavigation} />}
>
Pricing
</NavigationMenuLink>
</NavigationMenuItem>
Expand Down Expand Up @@ -160,15 +187,18 @@ export const OpenContent: Story = {
<NavigationMenuItem>
<TriggerRow>Product</TriggerRow>
<NavigationMenuContent>
<ul className="flex w-72 flex-col gap-1 p-2">
<NavigationMenuContentList>
{PRODUCT_LINKS.map((item) => (
<li key={item.href}>
<NavigationMenuLink render={<a href={item.href} onClick={cancelNavigation} />}>
{item.title}
<NavigationMenuLink
variant="card"
render={<a href={item.href} onClick={cancelNavigation} />}
>
<NavigationMenuLinkTitle>{item.title}</NavigationMenuLinkTitle>
</NavigationMenuLink>
</li>
))}
</ul>
</NavigationMenuContentList>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
Expand All @@ -190,15 +220,3 @@ export const OpenContent: Story = {
});
},
};

/** A trigger row that pairs the label with the rotating disclosure caret. */
function TriggerRow({ children }: { children: React.ReactNode }) {
return (
<NavigationMenuTrigger>
{children}
<NavigationMenuIcon>
<ChevronDown aria-hidden className="size-3.5" />
</NavigationMenuIcon>
</NavigationMenuTrigger>
);
}
16 changes: 16 additions & 0 deletions packages/propel/src/ui/navigation-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,21 @@ export {
type NavigationMenuBackdropProps,
} from "./navigation-menu-backdrop";
export { NavigationMenuContent, type NavigationMenuContentProps } from "./navigation-menu-content";
export {
NavigationMenuContentList,
type NavigationMenuContentListProps,
} from "./navigation-menu-content-list";
export { NavigationMenuIcon, type NavigationMenuIconProps } from "./navigation-menu-icon";
export { NavigationMenuItem, type NavigationMenuItemProps } from "./navigation-menu-item";
export { NavigationMenuLink, type NavigationMenuLinkProps } from "./navigation-menu-link";
export {
NavigationMenuLinkDescription,
type NavigationMenuLinkDescriptionProps,
} from "./navigation-menu-link-description";
export {
NavigationMenuLinkTitle,
type NavigationMenuLinkTitleProps,
} from "./navigation-menu-link-title";
export { NavigationMenuList, type NavigationMenuListProps } from "./navigation-menu-list";
export { NavigationMenuPopup, type NavigationMenuPopupProps } from "./navigation-menu-popup";
export { NavigationMenuPortal, type NavigationMenuPortalProps } from "./navigation-menu-portal";
Expand All @@ -16,6 +28,10 @@ export {
type NavigationMenuPositionerProps,
} from "./navigation-menu-positioner";
export { NavigationMenuTrigger, type NavigationMenuTriggerProps } from "./navigation-menu-trigger";
export {
NavigationMenuTriggerLabel,
type NavigationMenuTriggerLabelProps,
} from "./navigation-menu-trigger-label";
export {
NavigationMenuViewport,
type NavigationMenuViewportProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type * as React from "react";

import { navigationMenuContentListVariants } from "./variants";

export type NavigationMenuContentListProps = Omit<
React.ComponentPropsWithoutRef<"ul">,
"className" | "style"
>;

/**
* The vertical list of links inside a `NavigationMenuContent` panel. Stacks its items with a
* consistent gap; the surrounding `NavigationMenuPopup` owns the padding. Renders a `<ul>`, so each
* child link belongs in its own `<li>`.
*/
export function NavigationMenuContentList(props: NavigationMenuContentListProps) {
return <ul className={navigationMenuContentListVariants()} {...props} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type * as React from "react";

import { navigationMenuLinkDescriptionVariants } from "./variants";

export type NavigationMenuLinkDescriptionProps = Omit<
React.ComponentPropsWithoutRef<"span">,
"className" | "style"
>;

/**
* The optional secondary line of a `variant="card"` `NavigationMenuLink`: a muted description shown
* below the `NavigationMenuLinkTitle`.
*/
export function NavigationMenuLinkDescription(props: NavigationMenuLinkDescriptionProps) {
return <span className={navigationMenuLinkDescriptionVariants()} {...props} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type * as React from "react";

import { navigationMenuLinkTitleVariants } from "./variants";

export type NavigationMenuLinkTitleProps = Omit<
React.ComponentPropsWithoutRef<"span">,
"className" | "style"
>;

/**
* The primary line of a `variant="card"` `NavigationMenuLink`: the navigable label. Pairs with an
* optional `NavigationMenuLinkDescription` below it.
*/
export function NavigationMenuLinkTitle(props: NavigationMenuLinkTitleProps) {
return <span className={navigationMenuLinkTitleVariants()} {...props} />;
}
14 changes: 9 additions & 5 deletions packages/propel/src/ui/navigation-menu/navigation-menu-link.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { NavigationMenu as BaseNavigationMenu } from "@base-ui/react/navigation-menu";
import type { VariantProps } from "class-variance-authority";

import { navigationMenuLinkVariants } from "./variants";

export type NavigationMenuLinkProps = Omit<BaseNavigationMenu.Link.Props, "className" | "style">;
export type NavigationMenuLinkProps = Omit<BaseNavigationMenu.Link.Props, "className" | "style"> &
Required<VariantProps<typeof navigationMenuLinkVariants>>;

/**
* A navigable link, either as a top-level item or inside `Content`. Shares the nav-item chrome with
* `Trigger`. Maps 1:1 to `NavigationMenu.Link`.
* A navigable link. `variant="item"` is a top-level pill that shares the nav-item chrome with
* `Trigger`; `variant="card"` is a stacked entry inside `Content`, pairing a
* `NavigationMenuLinkTitle` with an optional `NavigationMenuLinkDescription`. Maps 1:1 to
* `NavigationMenu.Link`.
*/
export function NavigationMenuLink(props: NavigationMenuLinkProps) {
return <BaseNavigationMenu.Link className={navigationMenuLinkVariants()} {...props} />;
export function NavigationMenuLink({ variant, ...props }: NavigationMenuLinkProps) {
return <BaseNavigationMenu.Link className={navigationMenuLinkVariants({ variant })} {...props} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type * as React from "react";

import { navigationMenuTriggerLabelVariants } from "./variants";

export type NavigationMenuTriggerLabelProps = Omit<
React.ComponentPropsWithoutRef<"span">,
"className" | "style"
>;

/**
* The trigger's text label, sitting beside the disclosure `NavigationMenuIcon`. Splitting the label
* into its own part keeps `NavigationMenuTrigger` a single styled element that composes parts
* rather than baking in raw text.
*/
export function NavigationMenuTriggerLabel(props: NavigationMenuTriggerLabelProps) {
return <span className={navigationMenuTriggerLabelVariants()} {...props} />;
}
Loading