diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 74a9985e..8b5aa66c 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -21,6 +21,9 @@ export const parameters = {
>
),
+ canvas: {
+ sourceState: 'shown',
+ },
},
options: {
storySort: {
diff --git a/package-lock.json b/package-lock.json
index 967c8603..9ae39e7a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@radix-ui/react-checkbox": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.1-rc.6",
+ "@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tabs": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
@@ -4719,6 +4720,69 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.4.tgz",
+ "integrity": "sha512-Cc+seCS3PmWmjI51ufGG7zp1cAAIRqHVw7C9LOA2TZ+R4hG6rDvHcTqIsEEFLmZO3zNVH72jOOE7kKNy8W+RtA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-collection": "1.0.3",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-direction": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-id": "1.0.1",
+ "@radix-ui/react-presence": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1",
+ "@radix-ui/react-use-previous": "1.0.1",
+ "@radix-ui/react-visually-hidden": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
+ "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-escape-keydown": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.0.1-rc.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.0.1-rc.7.tgz",
diff --git a/package.json b/package.json
index c1ba583d..3a2cb8b3 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"@radix-ui/react-checkbox": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^1.0.1-rc.6",
+ "@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tabs": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
diff --git a/src/system/Link/Link.stories.tsx b/src/system/Link/Link.stories.tsx
index d553cb36..b8622a4f 100644
--- a/src/system/Link/Link.stories.tsx
+++ b/src/system/Link/Link.stories.tsx
@@ -9,8 +9,8 @@ import type { StoryObj } from '@storybook/react';
import { Link } from '..';
export default {
+ title: 'Navigation/Link',
component: Link,
- title: 'Link',
};
type Story = StoryObj< typeof Link >;
diff --git a/src/system/Nav/Nav.stories.tsx b/src/system/Nav/Nav.stories.tsx
new file mode 100644
index 00000000..24ce9096
--- /dev/null
+++ b/src/system/Nav/Nav.stories.tsx
@@ -0,0 +1,117 @@
+/**
+ * External dependencies
+ */
+import type { StoryObj } from '@storybook/react';
+
+/**
+ * Internal dependencies
+ */
+import { Nav, NavItem } from '../../system';
+
+export default {
+ title: 'Navigation/Nav',
+ component: Nav,
+ parameters: {
+ docs: {
+ description: {
+ component: `
+A navigation menu is a list of links used to navigate a website. It is usually placed in a prominent position at the top of a site, or anywhere that needs a linked-navigation.
+
+## Guidance
+
+### When to use the Nav component
+
+- To link internal or external links in a menu format.
+- To link to pages that are not part of the main navigation.
+
+### When to consider something else
+
+- If you have content inside the same page that will not affect the page Route/URL, use [Tabs](/docs/tabs--docs) component instead.
+- If you are planning to have buttons in your navigation, use another navigation solution, for example: [Dropdown](/docs/dropdown--docs) component instead.
+
+## Accessibility Considerations guidance
+
+This component is based on the Radix Navigation Menu primitive, so it contains all the accessibility features from the primitive.
+
+- Adheres to the [navigation role requirements.](https://www.w3.org/TR/wai-aria-1.2/#navigation)
+- Keyboard Interactions: https://www.radix-ui.com/primitives/docs/components/navigation-menu#keyboard-interactions
+
+### Usability guidance
+
+Pick one of the available variants: Primary or Tabs. You can use the components directly from the \`Nav\` component:
+
+~~~jsx filename="index.jsx"
+import { Nav, NavItem } from '@automattic/components';
+
+ or
+ or
+~~~
+
+### Usage with Next.js framwork
+
+~~~jsx filename="index.jsx"
+import Link from 'next/link';
+
+
+
+
+ Your page name
+
+
+
+~~~
+
+-------
+
+## Component Properties
+`,
+ },
+ },
+ },
+};
+
+type Story = StoryObj< typeof Nav >;
+
+export const Default: Story = {
+ render: () => (
+ <>
+
+ Variant: Primary
+
+
+
+ PHP
+
+ WordPress
+ New Relic
+
+ Not accessible
+
+
+ >
+ ),
+};
+
+export const Tab: Story = {
+ render: () => (
+ <>
+
+ Variant: Tab
+
+
+
+ PHP
+
+ WordPress
+ New Relic
+
+ Not accessible
+
+
+ >
+ ),
+};
diff --git a/src/system/Nav/Nav.test.tsx b/src/system/Nav/Nav.test.tsx
new file mode 100644
index 00000000..7f92efc8
--- /dev/null
+++ b/src/system/Nav/Nav.test.tsx
@@ -0,0 +1,51 @@
+/** @jsxImportSource theme-ui */
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+// @ts-nocheck
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import { axe } from 'jest-axe';
+import { ThemeUIProvider } from 'theme-ui';
+
+/**
+ * Internal dependencies
+ */
+import { Nav, NavItem } from './';
+
+import { theme } from '../';
+
+const renderWithTheme = children =>
+ render( { children } );
+
+const renderComponent = () =>
+ renderWithTheme(
+
+ PHP
+ WordPress
+
+ New Relic
+
+
+ Not accessible
+
+
+ );
+
+describe( '', () => {
+ it( 'renders the Nav component with default value visible', async () => {
+ const { container } = renderComponent();
+
+ // Should find the nav label
+ expect( screen.getByLabelText( 'Main' ) ).toBeInTheDocument();
+
+ // Should find all links
+ expect( screen.queryByText( 'PHP' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'WordPress' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'New Relic' ) ).toBeInTheDocument();
+ expect( screen.queryByText( 'Not accessible' ) ).toHaveAttribute( 'aria-disabled', 'true' );
+
+ // Check for accessibility issues
+ expect( await axe( container ) ).toHaveNoViolations();
+ } );
+} );
diff --git a/src/system/Nav/Nav.tsx b/src/system/Nav/Nav.tsx
new file mode 100644
index 00000000..681f0419
--- /dev/null
+++ b/src/system/Nav/Nav.tsx
@@ -0,0 +1,62 @@
+/** @jsxImportSource theme-ui */
+import React, { Ref, forwardRef } from 'react';
+import * as NavigationMenu from '@radix-ui/react-navigation-menu';
+import classNames from 'classnames';
+
+import { VIP_NAV } from '.';
+import { ThemeUIStyleObject } from 'theme-ui';
+
+export type NavVariant = 'primary' | 'tabs';
+
+export interface NavProps extends NavigationMenu.NavigationMenuProps {
+ className?: string;
+ variant?: NavVariant;
+ sx?: ThemeUIStyleObject;
+ label: string;
+}
+
+const Nav = forwardRef< HTMLElement, NavProps >(
+ (
+ { className, children, orientation, variant = 'primary', sx = {}, label }: NavProps,
+ ref: Ref< HTMLElement >
+ ) => (
+
+
+ { children }
+
+
+ )
+);
+
+export const NavPrimary = forwardRef< HTMLElement, NavProps >(
+ ( props: NavProps, ref: Ref< HTMLElement > ) =>
+);
+
+export const NavTab = forwardRef< HTMLElement, NavProps >(
+ ( props: NavProps, ref: Ref< HTMLElement > ) =>
+);
diff --git a/src/system/Nav/NavItem.tsx b/src/system/Nav/NavItem.tsx
new file mode 100644
index 00000000..27ec0ee8
--- /dev/null
+++ b/src/system/Nav/NavItem.tsx
@@ -0,0 +1,152 @@
+/** @jsxImportSource theme-ui */
+
+/**
+ * External dependencies
+ */
+import React, { Ref, forwardRef } from 'react';
+import * as NavigationMenu from '@radix-ui/react-navigation-menu';
+import classNames from 'classnames';
+
+/**
+ * Internal dependencies
+ */
+import { VIP_NAV } from '.';
+import { Theme, ThemeUIStyleObject } from 'theme-ui';
+import { NavVariant } from './Nav';
+
+interface NavItemTheme extends Theme {
+ outline?: Record< string, string >;
+}
+
+export interface NavItemBaseProps extends NavigationMenu.NavigationMenuItemProps {
+ className?: string;
+ sx?: ThemeUIStyleObject;
+}
+
+const NavItemBase = forwardRef< HTMLLIElement, NavItemBaseProps >(
+ ( { children, sx = {}, className, ...rest }: NavItemBaseProps, ref: Ref< HTMLLIElement > ) => (
+
+ { children }
+
+ )
+);
+
+const NavLink = forwardRef< HTMLAnchorElement, NavItemProps >(
+ (
+ { children, href, active, disabled, variant, ...props }: NavItemProps,
+ ref: Ref< HTMLAnchorElement >
+ ) => (
+
+ { children }
+
+ )
+);
+
+export interface NavItemProps extends NavigationMenu.NavigationMenuLinkProps {
+ className?: string;
+ disabled?: boolean;
+ variant?: NavVariant;
+}
+
+const styles = variant => {
+ const defaultVariantStyles: ThemeUIStyleObject =
+ variant === 'tabs'
+ ? {
+ px: 0,
+ mr: 2,
+ color: 'heading',
+ '&[data-active]': {
+ color: 'link',
+ fontWeight: 'normal',
+ boxShadow: 'inset 0 -1px 0 0, 0 1px 0 0',
+ },
+ '&[aria-disabled="true"]': {
+ color: 'muted',
+ },
+ ':hover': { fontWeight: 'regular', color: 'heading' },
+ }
+ : {
+ variant: `buttons.${ variant === 'link' ? 'text' : 'tertiary' }`,
+ borderRadius: 1,
+ '&[data-active]': {
+ variant: `buttons.${ variant === 'link' ? 'display' : variant }`,
+ },
+ '&[aria-disabled="true"]': {
+ opacity: 0.7,
+ color: 'texts.secondary',
+ cursor: 'not-allowed',
+ },
+ ':hover': {
+ backgroundColor: `button.${ variant }.background.hover`,
+ textDecoration: 'none',
+ },
+ };
+
+ return {
+ alignItems: 'center',
+ display: 'inline-flex',
+ fontSize: 2,
+ justifyContent: 'center',
+ lineHeight: 'inherit',
+ minHeight: '36px',
+ px: 3,
+ py: 0,
+ textDecoration: 'none',
+ verticalAlign: 'middle',
+ ...defaultVariantStyles,
+ '&:focus': ( theme: NavItemTheme ) => theme.outline,
+ '&:focus-visible': ( theme: NavItemTheme ) => theme.outline,
+ };
+};
+
+const NavItem = forwardRef< HTMLAnchorElement, NavItemProps >(
+ (
+ { className, children, href, active, disabled, variant = 'primary', ...props }: NavItemProps,
+ ref: Ref< HTMLAnchorElement >
+ ) => (
+
+
+ { children }
+
+
+ )
+);
+
+export const ItemPrimary = forwardRef< HTMLAnchorElement, NavItemProps >(
+ ( props: NavItemProps, ref: Ref< HTMLAnchorElement > ) => (
+
+ )
+);
+
+export const ItemTab = forwardRef< HTMLAnchorElement, NavItemProps >(
+ ( props: NavItemProps, ref: Ref< HTMLAnchorElement > ) => (
+
+ )
+);
diff --git a/src/system/Nav/index.tsx b/src/system/Nav/index.tsx
new file mode 100644
index 00000000..08ed9e46
--- /dev/null
+++ b/src/system/Nav/index.tsx
@@ -0,0 +1,18 @@
+/**
+ * Internal dependencies
+ */
+import { NavPrimary as Primary, NavTab as Tab } from './Nav';
+
+import { ItemPrimary, ItemTab } from './NavItem';
+
+export const VIP_NAV = 'vip-nav-component';
+
+export const Nav = {
+ Primary,
+ Tab,
+};
+
+export const NavItem = {
+ Primary: ItemPrimary,
+ Tab: ItemTab,
+};
diff --git a/src/system/Tabs/Tabs.stories.jsx b/src/system/Tabs/Tabs.stories.jsx
index 21163175..d23ce6c5 100644
--- a/src/system/Tabs/Tabs.stories.jsx
+++ b/src/system/Tabs/Tabs.stories.jsx
@@ -11,7 +11,7 @@ import React from 'react';
import { Tabs, TabsTrigger, TabsList, TabsContent, Text, Link, Button } from '..';
export default {
- title: 'Tabs',
+ title: 'Navigation/Tabs',
component: Tabs,
};
diff --git a/src/system/Wizard/Wizard.stories.tsx b/src/system/Wizard/Wizard.stories.tsx
index 4cbabebb..6d94c1fc 100644
--- a/src/system/Wizard/Wizard.stories.tsx
+++ b/src/system/Wizard/Wizard.stories.tsx
@@ -12,7 +12,7 @@ import { Wizard, Box, Label, Input, Button, Checkbox, Flex } from '..';
import { WizardStepProps } from './WizardStep';
export default {
- title: 'Wizard',
+ title: 'Navigation/Wizard',
component: Wizard,
};
diff --git a/src/system/index.js b/src/system/index.js
index 5a39fa24..003488ca 100644
--- a/src/system/index.js
+++ b/src/system/index.js
@@ -7,6 +7,8 @@ import { Box } from './Box';
import { Button, ButtonSubmit, ButtonVariant } from './Button';
import { Card } from './Card';
import { Code } from './Code';
+import { Nav, NavItem } from './Nav';
+
import {
Dialog,
DialogButton,
@@ -93,6 +95,8 @@ export {
Progress,
Text,
Tabs,
+ Nav,
+ NavItem,
TabsTrigger,
TabsContent,
TabsList,