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

OPHJOD-276: Add navigation bar #8

Merged
merged 1 commit into from
Mar 26, 2024
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
22 changes: 0 additions & 22 deletions lib/components/Header/Header.stories.ts

This file was deleted.

25 changes: 0 additions & 25 deletions lib/components/Header/Header.tsx

This file was deleted.

168 changes: 168 additions & 0 deletions lib/components/NavigationBar/NavigationBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { Meta, StoryObj } from '@storybook/react';

import { NavigationBar, type NavigationBarProps } from './NavigationBar';

const meta = {
component: NavigationBar,
tags: ['autodocs'],
} satisfies Meta<typeof NavigationBar>;

export default meta;

type Story = StoryObj<typeof meta>;

const parameters = {
design: {
type: 'figma',
url: 'https://www.figma.com/file/TpFgprt8pjcFcrHlMuL8Ry/cx_jod_ui?node-id=42%3A3498',
},
layout: 'fullscreen',
viewport: {
defaultViewport: 'desktop',
},
};

const items: NavigationBarProps['items'] = [
{
text: 'Yksilöille',
active: true,
component: ({ children, ...rootProps }) => (
<a href="/yksiloille" {...rootProps}>
{children}
</a>
),
},
{
text: 'Ohjaajille',
active: false,
component: ({ children, ...rootProps }) => (
<a href="/ohjaajille" {...rootProps}>
{children}
</a>
),
},
{
text: 'Kouluttajille',
active: false,
component: ({ children, ...rootProps }) => (
<a href="/kouluttajille" {...rootProps}>
{children}
</a>
),
},
{
text: 'Viranomaisille',
active: false,
component: ({ children, ...rootProps }) => (
<a href="/viranomaisille" {...rootProps}>
{children}
</a>
),
},
];

const user: NavigationBarProps['user'] = {
name: 'Jane Doe',
src: 'https://i.pravatar.cc/200?img=24',
component: ({ children, ...rootProps }) => (
<a href="/profiili" aria-label="Profiili" {...rootProps}>
{children}
</a>
),
};

const args: NavigationBarProps = {
items,
user,
};

export const Default: Story = {
parameters: {
...parameters,
docs: {
description: {
story: 'This story demonstrates how the navigation bar looks with navigation items and an avatar.',
},
},
},
args,
};

export const Plain: Story = {
parameters: {
...parameters,
docs: {
description: {
story: 'This story demonstrates how the navigation bar looks without any items or an avatar.',
},
},
},
args: {},
};

export const Items: Story = {
parameters: {
...parameters,
docs: {
description: {
story: 'This story demonstrates how the navigation bar looks with navigation items.',
},
},
},
args: {
items,
},
};

export const Avatar: Story = {
parameters: {
...parameters,
docs: {
description: {
story: 'This story demonstrates how the navigation bar looks with an avatar.',
},
},
},
args: {
user,
},
};

export const Sticky: Story = {
decorators: [
(Story) => (
<div>
<header className="sticky top-0">
<Story />
</header>
<main>
{[...Array<undefined>(20)].map((_p, index) => (
<p key={index} className="p-4">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</p>
))}
</main>
</div>
),
],
parameters: {
...parameters,
docs: {
description: {
story: 'This story demonstrates how the navigation bar behaves when it is sticky.',
},
story: {
inline: false,
iframeHeight: 400,
},
source: {
code: '<header className="sticky top-0">\n <NavigationBar />\n</header>',
},
},
},
args,
};
120 changes: 120 additions & 0 deletions lib/components/NavigationBar/NavigationBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { afterEach, describe, it, expect } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom';

import { NavigationBar, NavigationBarLinkProps } from './NavigationBar';

afterEach(() => {
cleanup();
});

describe('NavigationBar', () => {
const items = [
{
text: 'Home',
active: true,
href: '/home',
},
{
text: 'About',
active: false,
href: '/about',
},
{
text: 'Contact',
active: false,
href: '/contact',
},
].map(({ text, active, href }) => ({
text,
active,
href,
component: ({ children, ...rootProps }: NavigationBarLinkProps) => (
<a href={href} {...rootProps}>
{children}
</a>
),
}));

const user = {
name: 'Jane Doe',
src: 'user.jpg',
component: ({ children, ...rootProps }: NavigationBarLinkProps) => (
<a href="/profile" aria-label="Profile" {...rootProps}>
{children}
</a>
),
};

it('renders navigation items and user', () => {
const { container } = render(<NavigationBar items={items} user={user} />);

// Assert snapshot
expect(container.firstChild).toMatchSnapshot();

// Assert navigation items
items.forEach((item) => {
const linkElement = screen.getByText(item.text);
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', item.href);
});

// Assert user
const userImage = screen.getByAltText(user.name);
expect(userImage).toBeInTheDocument();
expect(userImage).toHaveAttribute('src', user.src);
});

it('renders only navigation items', () => {
const { container } = render(<NavigationBar items={items} />);

// Assert snapshot
expect(container.firstChild).toMatchSnapshot();

// Assert navigation items
items.forEach((item) => {
const linkElement = screen.getByText(item.text);
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', item.href);
});

// Assert user is not rendered
const userImage = screen.queryByAltText(user.name);
expect(userImage).toBeNull();
});

it('renders only user', () => {
const { container } = render(<NavigationBar user={user} />);

// Assert snapshot
expect(container.firstChild).toMatchSnapshot();

// Assert user
const userImage = screen.getByAltText(user.name);
expect(userImage).toBeInTheDocument();
expect(userImage).toHaveAttribute('src', user.src);

// Assert navigation items are not rendered
items.forEach((item) => {
const linkElement = screen.queryByText(item.text);
expect(linkElement).toBeNull();
});
});

it('renders no navigation items and no user', () => {
const { container } = render(<NavigationBar />);

// Assert snapshot
expect(container.firstChild).toMatchSnapshot();

// Assert navigation items are not rendered
items.forEach((item) => {
const linkElement = screen.queryByText(item.text);
expect(linkElement).toBeNull();
});

// Assert user is not rendered
const userImage = screen.queryByAltText(user.name);
expect(userImage).toBeNull();
});
});
59 changes: 59 additions & 0 deletions lib/components/NavigationBar/NavigationBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export interface NavigationBarLinkProps {
className?: string;
children: React.ReactNode;
}

export type NavigationBarLink = React.ComponentType<NavigationBarLinkProps>;

export interface NavigationBarProps {
/** Navigation items */
items?: {
text: string;
active: boolean;
component: NavigationBarLink;
}[];
/** Navigation avatar */
user?: {
name: string;
src: string;
component?: NavigationBarLink;
};
}

/**
* This component is a navigation bar that displays a logo, navigation items, and an avatar.
*/
export const NavigationBar = ({ items, user }: NavigationBarProps) => (
<div className="min-w-min border-b-2 border-[#808080] bg-[#FFFFFFE5]">
<nav className="mx-auto flex h-[72px] items-center justify-between gap-4 p-4 font-semibold lg:container">
<div className="inline-flex select-none items-center gap-2 text-[24px] leading-[140%] text-[#888]">
<div className="h-8 w-8 bg-[#808080]"></div>JOD
</div>
{(items ?? user) && (
<ul className="inline-flex items-center gap-4">
{items?.map((item, index) => (
<li key={index}>
<item.component
aria-current={item.active ? 'location' : undefined}
className={`block rounded-lg px-4 py-2 ${item.active ? 'bg-[#697077] text-white' : 'hover:bg-[#edeff0] focus:bg-[#edeff0]'}`}
>
{item.text}
</item.component>
</li>
))}
{user && (
<li>
{user.component ? (
<user.component className="block h-[40px] w-[40px] rounded-full">
<img className="rounded-full" src={user.src} alt={user.name} />
</user.component>
) : (
<img className="block h-[40px] w-[40px] rounded-full" src={user.src} alt={user.name} />
)}
</li>
)}
</ul>
)}
</nav>
</div>
);
Loading