From c1bb43ebda91fef8527ef0f05d7191e1241f8683 Mon Sep 17 00:00:00 2001 From: Djalma Araujo Date: Tue, 31 Oct 2023 23:33:36 -0300 Subject: [PATCH 01/17] Add Nav component with some Storybook configuration --- .storybook/preview.tsx | 3 + package-lock.json | 64 ++++++ package.json | 1 + src/system/Link/Link.stories.tsx | 2 +- src/system/Nav/Nav.stories.tsx | 68 ++++++ src/system/Nav/Nav.tsx | 66 ++++++ src/system/Nav/NavItem.tsx | 75 +++++++ src/system/Nav/index.tsx | 14 ++ src/system/Nav/style.css | 309 +++++++++++++++++++++++++++ src/system/Tabs/Tabs.stories.jsx | 2 +- src/system/Wizard/Wizard.stories.tsx | 2 +- src/system/index.js | 2 + 12 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 src/system/Nav/Nav.stories.tsx create mode 100644 src/system/Nav/Nav.tsx create mode 100644 src/system/Nav/NavItem.tsx create mode 100644 src/system/Nav/index.tsx create mode 100644 src/system/Nav/style.css 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..e145f46d --- /dev/null +++ b/src/system/Nav/Nav.stories.tsx @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import type { StoryObj } from '@storybook/react'; + +/** + * Internal dependencies + */ +import * as Nav from '.'; +import { NavItemProps } from './NavItem'; + +export default { + title: 'Navigation/Nav', + component: Nav.Root, + 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. + +### Usability guidance + +- Pick one of the available variants: \`primary\`, \`secondary\`, \`display\`. + +------- + +## Component Properties +`, + }, + }, + }, +}; + +type Story = StoryObj< typeof Nav >; + +const variants = [ 'primary', 'secondary', 'display' ] as NavItemProps[ 'variant' ][]; + +export const Default: Story = { + render: () => ( + <> + { variants.map( variant => ( + + + PHP + + WordPress + New Relic + + Disabled anchor + + + ) ) } + + ), +}; diff --git a/src/system/Nav/Nav.tsx b/src/system/Nav/Nav.tsx new file mode 100644 index 00000000..3c589d64 --- /dev/null +++ b/src/system/Nav/Nav.tsx @@ -0,0 +1,66 @@ +/** @jsxImportSource theme-ui */ +import React, { Ref, forwardRef } from 'react'; +import * as NavigationMenu from '@radix-ui/react-navigation-menu'; +import classNames from 'classnames'; + +import './style.css'; + +import { VIP_NAV } from '.'; +import { ThemeUIStyleObject } from 'theme-ui'; +import { NavItemProps } from './NavItem'; + +export interface NavProps extends NavigationMenu.NavigationMenuProps { + className?: string; + variant?: 'primary' | 'secondary' | 'display' | 'ghost'; + sx?: ThemeUIStyleObject; +} + +const Nav = forwardRef< HTMLElement, NavProps >( + ( + { className, children, orientation, variant = 'primary', sx = {} }: NavProps, + ref: Ref< HTMLElement > + ) => { + const childrenWithVariant = React.Children.map( + children as React.ReactElement< NavItemProps >, + child => { + if ( React.isValidElement( child ) ) { + return React.cloneElement( child, { variant } ); + } + + return child; + } + ); + + return ( + + + { childrenWithVariant } + + + ); + } +); + +export default Nav; diff --git a/src/system/Nav/NavItem.tsx b/src/system/Nav/NavItem.tsx new file mode 100644 index 00000000..fc9e7070 --- /dev/null +++ b/src/system/Nav/NavItem.tsx @@ -0,0 +1,75 @@ +/** @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 { NavProps } from './Nav'; + +interface NavItemTheme extends Theme { + outline?: Record< string, string >; +} +export interface NavItemProps extends NavigationMenu.NavigationMenuLinkProps { + className?: string; + disabled?: boolean | undefined; + sx?: ThemeUIStyleObject | undefined; + variant?: NavProps[ 'variant' ]; +} + +const styles = variant => ( { + backgroundColor: 'transparent', + mr: 0, + fontSize: 2, + borderRadius: 1, + p: 3, + px: 4, + display: 'inline-block', + color: 'link', + textDecoration: 'none', + '&[data-active]': { + color: `button.${ variant }.label.default`, + backgroundColor: `button.${ variant }.background.default`, + fontWeight: 'regular', + boxShadow: 'inset 0 -1px 0 0', + }, + '&[aria-disabled="true"]': { + opacity: 0.7, + color: 'texts.secondary', + cursor: 'not-allowed', + }, + ':hover': { fontWeight: 'regular', color: 'link' }, + '&:focus': ( theme: NavItemTheme ) => theme.outline, + '&:focus-visible': ( theme: NavItemTheme ) => theme.outline, +} ); + +const NavItem = forwardRef< HTMLAnchorElement, NavItemProps >( + ( + { className, children, href, active, disabled, variant = 'primary' }: NavItemProps, + ref: Ref< HTMLAnchorElement > + ) => { + return ( + + + { children } + + + ); + } +); + +export default NavItem; diff --git a/src/system/Nav/index.tsx b/src/system/Nav/index.tsx new file mode 100644 index 00000000..dac1d99b --- /dev/null +++ b/src/system/Nav/index.tsx @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import Nav from './Nav'; +import NavItem from './NavItem'; + +export const VIP_NAV = 'vip-nav-component'; + +const Root = Nav; +const Item = NavItem; + +export { Root, Item }; + +export default Nav; diff --git a/src/system/Nav/style.css b/src/system/Nav/style.css new file mode 100644 index 00000000..74707820 --- /dev/null +++ b/src/system/Nav/style.css @@ -0,0 +1,309 @@ +/* .vip-nav-component { + position: relative; + display: flex; + justify-content: center; + width: 100vw; + z-index: 1; +} + +.vip-nav-component-list { + display: flex; + justify-content: center; + list-style: none; + margin: 0; +} + +.vip-nav-component-item-trigger, +.vip-nav-component-item-link { + padding: 8px 12px; + outline: none; + user-select: none; + font-weight: 500; + line-height: 1; + border-radius: 4px; + font-size: 15px; + color: var( --violet-11 ); +} +.vip-nav-component-item-trigger:focus, +.vip-nav-component-item-link:focus { + box-shadow: 0 0 0 2px red; +} +.vip-nav-component-item-trigger:hover, +.vip-nav-component-item-link:hover { + background-color: var( --violet-3 ); +} */ +/* +.vip-nav-component-item-trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 2px; +} + +.vip-nav-component-item-link { + display: block; + text-decoration: none; + font-size: 15px; + line-height: 1; +} + +.NavigationMenuContent { + position: absolute; + top: 0; + left: 0; + width: 100%; + animation-duration: 250ms; + animation-timing-function: ease; +} +.NavigationMenuContent[data-motion='from-start'] { + animation-name: enterFromLeft; +} +.NavigationMenuContent[data-motion='from-end'] { + animation-name: enterFromRight; +} +.NavigationMenuContent[data-motion='to-start'] { + animation-name: exitToLeft; +} +.NavigationMenuContent[data-motion='to-end'] { + animation-name: exitToRight; +} +@media only screen and ( min-width: 600px ) { + .NavigationMenuContent { + width: auto; + } +} + +.NavigationMenuIndicator { + display: flex; + align-items: flex-end; + justify-content: center; + height: 10px; + top: 100%; + overflow: hidden; + z-index: 1; + transition: width, transform 250ms ease; +} +.NavigationMenuIndicator[data-state='visible'] { + animation: fadeIn 200ms ease; +} +.NavigationMenuIndicator[data-state='hidden'] { + animation: fadeOut 200ms ease; +} */ + +.NavigationMenuViewport { + position: relative; + transform-origin: top center; + margin-top: 10px; + width: 100%; + overflow: hidden; + box-shadow: hsl( 206 22% 7% / 35% ) 0px 10px 38px -10px, + hsl( 206 22% 7% / 20% ) 0px 10px 20px -15px; + height: var( --radix-navigation-menu-viewport-height ); + transition: width, height, 300ms ease; +} +.NavigationMenuViewport[data-state='open'] { + animation: scaleIn 200ms ease; +} +.NavigationMenuViewport[data-state='closed'] { + animation: scaleOut 200ms ease; +} +@media only screen and ( min-width: 600px ) { + .NavigationMenuViewport { + width: var( --radix-navigation-menu-viewport-width ); + } +} +/* +.List { + display: grid; + padding: 22px; + margin: 0; + column-gap: 10px; + list-style: none; +} +@media only screen and ( min-width: 600px ) { + .List.one { + width: 500px; + grid-template-columns: 0.75fr 1fr; + } + .List.two { + width: 600px; + grid-auto-flow: column; + grid-template-rows: repeat( 3, 1fr ); + } +} + +.ListItemLink { + display: block; + outline: none; + text-decoration: none; + user-select: none; + padding: 12px; + border-radius: 6px; + font-size: 15px; + line-height: 1; +} +.ListItemLink:focus { + box-shadow: 0 0 0 2px red; +} +.ListItemLink:hover { + background-color: var( --mauve-3 ); +} + +.ListItemHeading { + font-weight: 500; + line-height: 1.2; + margin-bottom: 5px; + color: var( --violet-12 ); +} + +.ListItemText { + color: var( --mauve-11 ); + line-height: 1.4; + font-weight: initial; +} + +.Callout { + display: flex; + justify-content: flex-end; + flex-direction: column; + width: 100%; + height: 100%; + background: linear-gradient( 135deg, var( --purple-9 ) 0%, var( --indigo-9 ) 100% ); + border-radius: 6px; + padding: 25px; + text-decoration: none; + outline: none; + user-select: none; +} +.Callout:focus { + box-shadow: 0 0 0 2px red; +} + +.CalloutHeading { + color: white; + font-size: 18px; + font-weight: 500; + line-height: 1.2; + margin-top: 16px; + margin-bottom: 7px; +} + +.CalloutText { + color: var( --mauve-4 ); + font-size: 14px; + line-height: 1.3; +} + +.ViewportPosition { + position: absolute; + display: flex; + justify-content: center; + width: 100%; + top: 100%; + left: 0; + perspective: 2000px; +} + +.CaretDown { + position: relative; + color: var( --violet-10 ); + top: 1px; + transition: transform 250ms ease; +} +[data-state='open'] > .CaretDown { + transform: rotate( -180deg ); +} + +.Arrow { + position: relative; + top: 70%; + background-color: white; + width: 10px; + height: 10px; + transform: rotate( 45deg ); + border-top-left-radius: 2px; +} + +@keyframes enterFromRight { + from { + opacity: 0; + transform: translateX( 200px ); + } + to { + opacity: 1; + transform: translateX( 0 ); + } +} + +@keyframes enterFromLeft { + from { + opacity: 0; + transform: translateX( -200px ); + } + to { + opacity: 1; + transform: translateX( 0 ); + } +} + +@keyframes exitToRight { + from { + opacity: 1; + transform: translateX( 0 ); + } + to { + opacity: 0; + transform: translateX( 200px ); + } +} + +@keyframes exitToLeft { + from { + opacity: 1; + transform: translateX( 0 ); + } + to { + opacity: 0; + transform: translateX( -200px ); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: rotateX( -30deg ) scale( 0.9 ); + } + to { + opacity: 1; + transform: rotateX( 0deg ) scale( 1 ); + } +} + +@keyframes scaleOut { + from { + opacity: 1; + transform: rotateX( 0deg ) scale( 1 ); + } + to { + opacity: 0; + transform: rotateX( -10deg ) scale( 0.95 ); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} */ 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..72e242e3 100644 --- a/src/system/index.js +++ b/src/system/index.js @@ -7,6 +7,7 @@ import { Box } from './Box'; import { Button, ButtonSubmit, ButtonVariant } from './Button'; import { Card } from './Card'; import { Code } from './Code'; +import * as Nav from './Nav'; import { Dialog, DialogButton, @@ -93,6 +94,7 @@ export { Progress, Text, Tabs, + Nav, TabsTrigger, TabsContent, TabsList, From 003a510f5531a7836b9ae2a0d0e3e008cc250350 Mon Sep 17 00:00:00 2001 From: Djalma Araujo Date: Wed, 1 Nov 2023 09:55:08 -0300 Subject: [PATCH 02/17] Adjust textDecoration and hover styles --- src/system/Nav/NavItem.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/system/Nav/NavItem.tsx b/src/system/Nav/NavItem.tsx index fc9e7070..6d611f44 100644 --- a/src/system/Nav/NavItem.tsx +++ b/src/system/Nav/NavItem.tsx @@ -45,7 +45,12 @@ const styles = variant => ( { color: 'texts.secondary', cursor: 'not-allowed', }, - ':hover': { fontWeight: 'regular', color: 'link' }, + ':hover': { + fontWeight: 'regular', + color: `button.${ variant }.label.hover`, + backgroundColor: `button.${ variant }.background.hover`, + textDecoration: 'none', + }, '&:focus': ( theme: NavItemTheme ) => theme.outline, '&:focus-visible': ( theme: NavItemTheme ) => theme.outline, } ); From 0d7df4af95aeaa2a404407598e4c164427072b09 Mon Sep 17 00:00:00 2001 From: Djalma Araujo Date: Wed, 1 Nov 2023 14:04:44 -0300 Subject: [PATCH 03/17] Add Nav styles and tests --- src/system/Nav/Nav.stories.tsx | 52 ++++-- src/system/Nav/Nav.test.tsx | 51 ++++++ src/system/Nav/Nav.tsx | 16 +- src/system/Nav/NavItem.tsx | 174 ++++++++++++++----- src/system/Nav/index.tsx | 5 +- src/system/Nav/style.css | 309 --------------------------------- 6 files changed, 236 insertions(+), 371 deletions(-) create mode 100644 src/system/Nav/Nav.test.tsx delete mode 100644 src/system/Nav/style.css diff --git a/src/system/Nav/Nav.stories.tsx b/src/system/Nav/Nav.stories.tsx index e145f46d..bf4420d8 100644 --- a/src/system/Nav/Nav.stories.tsx +++ b/src/system/Nav/Nav.stories.tsx @@ -46,23 +46,53 @@ A navigation menu is a list of links used to navigate a website. It is usually p type Story = StoryObj< typeof Nav >; -const variants = [ 'primary', 'secondary', 'display' ] as NavItemProps[ 'variant' ][]; +const variants = [ + 'primary', + 'secondary', + 'display', + 'link', + 'tabs', +] as NavItemProps[ 'variant' ][]; export const Default: Story = { render: () => ( <> { variants.map( variant => ( - - - PHP - - WordPress - New Relic - - Disabled anchor - - + <> +

+ Variant: { variant } +

+ + PHP + WordPress + + New Relic + + + Not accessible + + + ) ) } ), }; + +export const SubMenus: Story = { + render: () => ( + + Home + + + Sports + + Juices + + + ), +}; diff --git a/src/system/Nav/Nav.test.tsx b/src/system/Nav/Nav.test.tsx new file mode 100644 index 00000000..2609248a --- /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 * as Nav from './'; + +import { theme } from '../'; + +const renderWithTheme = children => + render( { children } ); + +const renderComponent = () => + renderWithTheme( + + PHP + WordPress + + New Relic + + + Not accessible + + + ); + +describe( '