Skip to content

Commit

Permalink
Create a PrimaryNavigation component
Browse files Browse the repository at this point in the history
This component wraps Radix UI's NavigationMenu and provide's a
replacement for the current header navigation in Squareone.

With NavigationMenu we'll be able to give individual menu items
submenus, and not necessarily have to treat the user menu as a special
case.

This work styles the basic PrimaryNavigation component and demonstrates
it with a storybook story.

More work is need to make the submenu appear directly below its trigger.
  • Loading branch information
jonathansick committed Jan 31, 2025
1 parent d025f83 commit dd3ce88
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, screen } from '@storybook/testing-library';
import { expect } from '@storybook/jest';

import { ChevronDown } from 'react-feather';

import { PrimaryNavigation } from './PrimaryNavigation';

const meta: Meta<typeof PrimaryNavigation> = {
title: 'Components/PrimaryNavigation',
component: PrimaryNavigation,
parameters: {
layout: 'centered',
backgrounds: {
default: 'dark',
values: [{ name: 'dark', value: '#1f2121' }],
},
},
};

export default meta;
type Story = StoryObj<typeof PrimaryNavigation>;

export const Default: Story = {
args: {},
render: (args) => (
<PrimaryNavigation {...args}>
<PrimaryNavigation.Item>
<PrimaryNavigation.TriggerLink href="#">
Portal
</PrimaryNavigation.TriggerLink>
</PrimaryNavigation.Item>

<PrimaryNavigation.Item>
<PrimaryNavigation.TriggerLink href="/nb">
Notebooks
</PrimaryNavigation.TriggerLink>
</PrimaryNavigation.Item>

<PrimaryNavigation.Item>
<PrimaryNavigation.TriggerLink href="/docs">
Documentation
</PrimaryNavigation.TriggerLink>
</PrimaryNavigation.Item>

<PrimaryNavigation.Item>
<PrimaryNavigation.Trigger>
Account <ChevronDown />
</PrimaryNavigation.Trigger>
<PrimaryNavigation.SubMenuContent>
<PrimaryNavigation.MenuItem>
<PrimaryNavigation.Link href="#">Settings</PrimaryNavigation.Link>
</PrimaryNavigation.MenuItem>
<PrimaryNavigation.MenuItem>
<PrimaryNavigation.Link href="#">Logout</PrimaryNavigation.Link>
</PrimaryNavigation.MenuItem>
</PrimaryNavigation.SubMenuContent>
</PrimaryNavigation.Item>
</PrimaryNavigation>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import React from 'react';
import styled from 'styled-components';

import * as RadixNavigationMenu from '@radix-ui/react-navigation-menu';

export interface PrimaryNavigationProps {
children: React.ReactNode;
}

export const PrimaryNavigation = ({ children }: PrimaryNavigationProps) => {
return (
<MenuRoot>
<MenuList>{children}</MenuList>
<ViewportContainer>
<ContentViewport
onPointerEnter={(event) => event.preventDefault()}
onPointerLeave={(event) => event.preventDefault()}
/>
</ViewportContainer>
</MenuRoot>
);
};

const MenuRoot = styled(RadixNavigationMenu.Root)`
position: relative;
`;

const MenuList = styled(RadixNavigationMenu.List)`
list-style: none;
margin-bottom: 0;
padding: 0;
display: flex;
justify-self: end;
width: 100%;
font-size: 1.2rem;
`;

const MenuItem = styled(RadixNavigationMenu.Item)`
margin: 0 1em;
`;

const TriggerLink = styled(RadixNavigationMenu.Link)`
color: var(--rsd-component-header-nav-text-color);
border: none;
border-radius: 0.5rem;
padding: 2px 4px;
display: inline-block; // For consistency with MenuTrigger button
/* padding: 0; */
&:hover {
color: var(--rsd-component-header-nav-text-hover-color);
}
&:focus {
outline: 1px solid var(--rsd-color-primary-500);
}
`;

const MenuTrigger = styled(RadixNavigationMenu.Trigger)`
color: var(--rsd-component-header-nav-text-color);
padding: 2px 4px;
/* padding: 0; */
// Reset button styles
background-color: transparent;
border: none;
border-radius: 0.5rem;
&:focus {
outline: 1px solid var(--rsd-color-primary-500);
}
&:hover {
color: var(--rsd-component-header-nav-text-hover-color);
}
svg {
display: inline-block;
width: 1rem;
height: 1rem;
vertical-align: middle;
}
&[data-state='open'] {
svg {
transform: rotate(180deg);
}
}
`;

const SubMenuContent = styled(RadixNavigationMenu.Content)`
/* This unit for the padding is also the basis for the spacing and
* sizing of the menu items.
*/
--gafaelfawr-user-menu-padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
list-style: none;
font-size: 1rem;
background-color: var(--rsd-component-header-nav-menulist-background-color);
min-width: 12rem;
border-radius: 0.5rem;
padding: var(--gafaelfawr-user-menu-padding);
padding-right: 0; // to avoid double padding on the right side with MenuLink
box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
`;

const MenuContentItem = styled(RadixNavigationMenu.Item)`
display: flex;
`;

export const MenuLink = styled(RadixNavigationMenu.Link)`
/* The styling on the menu triggers is overriding this colour. Need to re-address. */
border-radius: 0.5rem;
padding: calc(var(--gafaelfawr-user-menu-padding) / 2)
var(--gafaelfawr-user-menu-padding);
margin: calc(var(--gafaelfawr-user-menu-padding) / -2);
margin-bottom: calc(var(--gafaelfawr-user-menu-padding) / 2);
width: 100%;
color: var(--rsd-component-header-nav-menulist-text-color);
&:last-of-type {
margin-bottom: 0;
}
outline: 1px solid transparent;
&:focus {
outline: 1px solid
var(--rsd-component-header-nav-menulist-selected-background-color);
}
&:hover {
background-color: var(
--rsd-component-header-nav-menulist-selected-background-color
);
color: white !important;
}
`;

const ViewportContainer = styled.div`
position: absolute;
display: flex;
justify-content: center;
width: 100%;
top: 100%;
left: 0;
perspective: 2000px;
`;

const ContentViewport = styled(RadixNavigationMenu.Viewport)`
position: relative;
transform-origin: top center;
margin-top: 10px;
width: 100%;
background-color: white;
border-radius: 6px;
overflow: hidden;
height: var(--radix-navigation-menu-viewport-height);
width: var(--radix-navigation-menu-viewport-width);
`;

// Associate child components with the parent for easier imports.
PrimaryNavigation.TriggerLink = TriggerLink;
PrimaryNavigation.Link = MenuLink;
PrimaryNavigation.Item = MenuItem;
PrimaryNavigation.MenuItem = MenuContentItem;
PrimaryNavigation.Trigger = MenuTrigger;
PrimaryNavigation.SubMenuContent = SubMenuContent;

export default PrimaryNavigation;
2 changes: 2 additions & 0 deletions packages/squared/src/components/PrimaryNavigation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './PrimaryNavigation';
export { default } from './PrimaryNavigation';
4 changes: 4 additions & 0 deletions packages/squared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export {
type GafaelfawrUserMenuProps,
} from './components/GafaelfawrUserMenu';
export { default as IconPill, type IconPillProps } from './components/IconPill';
export {
default as PrimaryNavigation,
type PrimaryNavigationProps,
} from './components/PrimaryNavigation';

/* Hooks */
export { default as useCurrentUrl } from './hooks/useCurrentUrl';
Expand Down

0 comments on commit dd3ce88

Please sign in to comment.