Skip to content
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

feature(navigation-menu): added option for navigation menu content to make content dynamic #6217

Open
wants to merge 2 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
30 changes: 30 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4209,6 +4209,21 @@ export const Index: Record<string, any> = {
source: "",
meta: undefined,
},
"navigation-menu-options-demo": {
name: "navigation-menu-options-demo",
description: "",
type: "registry:example",
registryDependencies: ["navigation-menu"],
files: [{
path: "registry/new-york/examples/navigation-menu-options-demo.tsx",
type: "registry:example",
target: ""
}],
categories: undefined,
component: React.lazy(() => import("@/registry/new-york/examples/navigation-menu-options-demo.tsx")),
source: "",
meta: undefined,
},
"pagination-demo": {
name: "pagination-demo",
description: "",
Expand Down Expand Up @@ -9464,6 +9479,21 @@ export const Index: Record<string, any> = {
source: "",
meta: undefined,
},
"navigation-menu-options-demo": {
name: "navigation-menu-options-demo",
description: "",
type: "registry:example",
registryDependencies: ["navigation-menu"],
files: [{
path: "registry/default/examples/navigation-menu-options-demo.tsx",
type: "registry:example",
target: ""
}],
categories: undefined,
component: React.lazy(() => import("@/registry/default/examples/navigation-menu-options-demo.tsx")),
source: "",
meta: undefined,
},
"pagination-demo": {
name: "pagination-demo",
description: "",
Expand Down
4 changes: 3 additions & 1 deletion apps/www/components/component-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
description?: string
hideCode?: boolean
type?: "block" | "component" | "example"
componentProps?: Record<string, string>
}

export function ComponentPreview({
Expand All @@ -39,6 +40,7 @@ export function ComponentPreview({
align = "center",
description,
hideCode = false,
componentProps,
...props
}: ComponentPreviewProps) {
const [config] = useConfig()
Expand All @@ -62,7 +64,7 @@ export function ComponentPreview({
)
}

return <Component />
return <Component {...componentProps} />
}, [name, config.style])

const codeString = React.useMemo(() => {
Expand Down
29 changes: 29 additions & 0 deletions apps/www/content/docs/components/navigation-menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,33 @@ import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"
</NavigationMenuItem>
```

### Content options

`align`: `"start"` | `"center"` | `"end"`
assign align type for content position
`contentWidth`: `number[]`
assign width for every NavigationMenuContent


`contentOptions={{ align: "start" }}`

<ComponentPreview
name="navigation-menu-options-demo"
componentProps={{ align: "start" }}
/>

`contentOptions={{ align: "center" }}`

<ComponentPreview
name="navigation-menu-options-demo"
componentProps={{ align: "center" }}
/>

`contentOptions={{ align: "end" }}`

<ComponentPreview
name="navigation-menu-options-demo"
componentProps={{ align: "end" }}
/>

See also the [Radix UI documentation](https://www.radix-ui.com/docs/primitives/components/navigation-menu#with-client-side-routing) for handling client side routing.
74 changes: 74 additions & 0 deletions apps/www/hooks/use-content-position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from "react";

const aligns = {
start: "start",
center: "center",
end: "end",
} as const;

export interface ContentOptions {
align: keyof typeof aligns;
contentWidth: number[];
}

const alignFns: Record<
keyof typeof aligns,
(computedWidth: number, contentWidth: number, buttonWidth: number) => number
> = {
start: (computedWidth, contentWidth, _) => {
return computedWidth - contentWidth * 0;
},
center: (computedWidth, contentWidth, buttonWidth) => {
return computedWidth - contentWidth / 2 + buttonWidth / 2;
},
end: (computedWidth, contentWidth, buttonWidth) => {
return computedWidth - contentWidth + buttonWidth;
},
};

const useContentPosition = (options?: ContentOptions) => {
const ref = React.useRef<HTMLLIElement | null>(null);
const [position, setPosition] = React.useState<{
x: number;
}>({ x: 0 });

React.useLayoutEffect(() => {
const container = ref.current;
if (!container || !options) return;

const handleMouseEnter = (ev: MouseEvent) => {
const { clientX, clientY } = ev;
const element = document.elementFromPoint(clientX, clientY);
if (!(element instanceof HTMLButtonElement)) {
return;
}
container.childNodes.forEach((_, idx) => {
const _elem = container.childNodes[idx].firstChild
if (_elem === element) {
const containerCoords = container.getBoundingClientRect();
const elemCoords = element.getBoundingClientRect();
const computedWidth = elemCoords.left - containerCoords.left;
const X = alignFns[options.align](
computedWidth,
options.contentWidth[idx],
elemCoords.width
);
setPosition({
x: X,
});
}
});
};

container.addEventListener("mousemove", handleMouseEnter);
return () => {
if (container) {
container.removeEventListener("mousemove", handleMouseEnter);
}
};
}, []);

return [position, ref] as const;
};

export default useContentPosition;
15 changes: 15 additions & 0 deletions apps/www/public/r/styles/default/navigation-menu-options-demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "navigation-menu-options-demo",
"type": "registry:example",
"registryDependencies": [
"navigation-menu"
],
"files": [
{
"path": "examples/navigation-menu-options-demo.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport Link from \"next/link\"\n\nimport { cn } from \"@/lib/utils\"\nimport { type ContentOptions } from \"@/hooks/use-content-position\"\nimport { Icons } from \"@/components/icons\"\nimport {\n NavigationMenu,\n NavigationMenuContent,\n NavigationMenuItem,\n NavigationMenuLink,\n NavigationMenuList,\n NavigationMenuTrigger,\n navigationMenuTriggerStyle,\n} from \"@/registry/default/ui/navigation-menu\"\n\nconst components: { title: string; href: string; description: string }[] = [\n {\n title: \"Alert Dialog\",\n href: \"/docs/primitives/alert-dialog\",\n description:\n \"A modal dialog that interrupts the user with important content and expects a response.\",\n },\n {\n title: \"Hover Card\",\n href: \"/docs/primitives/hover-card\",\n description:\n \"For sighted users to preview content available behind a link.\",\n },\n {\n title: \"Progress\",\n href: \"/docs/primitives/progress\",\n description:\n \"Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.\",\n },\n {\n title: \"Scroll-area\",\n href: \"/docs/primitives/scroll-area\",\n description: \"Visually or semantically separates content.\",\n },\n {\n title: \"Tabs\",\n href: \"/docs/primitives/tabs\",\n description:\n \"A set of layered sections of content—known as tab panels—that are displayed one at a time.\",\n },\n {\n title: \"Tooltip\",\n href: \"/docs/primitives/tooltip\",\n description:\n \"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.\",\n },\n]\n\nexport default function NavigationMenuOptionsDemo({\n align,\n}: {\n align: ContentOptions[\"align\"]\n}) {\n return (\n <NavigationMenu\n contentOptions={{ align: align || \"start\", contentWidth: 500 }}\n >\n <NavigationMenuList>\n <NavigationMenuItem>\n <NavigationMenuTrigger>Getting started</NavigationMenuTrigger>\n <NavigationMenuContent>\n <ul className=\"grid gap-3 p-4 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]\">\n <li className=\"row-span-3\">\n <NavigationMenuLink asChild>\n <a\n className=\"flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md\"\n href=\"/\"\n >\n <Icons.logo className=\"h-6 w-6\" />\n <div className=\"mb-2 mt-4 text-lg font-medium\">\n shadcn/ui\n </div>\n <p className=\"text-sm leading-tight text-muted-foreground\">\n Beautifully designed components built with Radix UI and\n Tailwind CSS.\n </p>\n </a>\n </NavigationMenuLink>\n </li>\n <ListItem href=\"/docs\" title=\"Introduction\">\n Re-usable components built using Radix UI and Tailwind CSS.\n </ListItem>\n <ListItem href=\"/docs/installation\" title=\"Installation\">\n How to install dependencies and structure your app.\n </ListItem>\n <ListItem href=\"/docs/primitives/typography\" title=\"Typography\">\n Styles for headings, paragraphs, lists...etc\n </ListItem>\n </ul>\n </NavigationMenuContent>\n </NavigationMenuItem>\n <NavigationMenuItem>\n <NavigationMenuTrigger>Components</NavigationMenuTrigger>\n <NavigationMenuContent>\n <ul className=\"grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] \">\n {components.map((component) => (\n <ListItem\n key={component.title}\n title={component.title}\n href={component.href}\n >\n {component.description}\n </ListItem>\n ))}\n </ul>\n </NavigationMenuContent>\n </NavigationMenuItem>\n <NavigationMenuItem>\n <Link href=\"/docs\" legacyBehavior passHref>\n <NavigationMenuLink className={navigationMenuTriggerStyle()}>\n Documentation\n </NavigationMenuLink>\n </Link>\n </NavigationMenuItem>\n </NavigationMenuList>\n </NavigationMenu>\n )\n}\n\nconst ListItem = React.forwardRef<\n React.ElementRef<\"a\">,\n React.ComponentPropsWithoutRef<\"a\">\n>(({ className, title, children, ...props }, ref) => {\n return (\n <li>\n <NavigationMenuLink asChild>\n <a\n ref={ref}\n className={cn(\n \"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n className\n )}\n {...props}\n >\n <div className=\"text-sm font-medium leading-none\">{title}</div>\n <p className=\"line-clamp-2 text-sm leading-snug text-muted-foreground\">\n {children}\n </p>\n </a>\n </NavigationMenuLink>\n </li>\n )\n})\nListItem.displayName = \"ListItem\"\n",
"type": "registry:example",
"target": ""
}
]
}
Loading