Skip to content

Commit

Permalink
OPHJOD-276: Add navigation bar
Browse files Browse the repository at this point in the history
  • Loading branch information
sauanto committed Mar 25, 2024
1 parent 841ccb4 commit 1dec1b9
Show file tree
Hide file tree
Showing 7 changed files with 508 additions and 47 deletions.
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" {...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/vitest';

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" {...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();
});
});
58 changes: 58 additions & 0 deletions lib/components/NavigationBar/NavigationBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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
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

0 comments on commit 1dec1b9

Please sign in to comment.