Skip to content
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
192 changes: 99 additions & 93 deletions lib/components/Sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Theme } from '@/domain/theme';
import { Typography } from '../Typography/Typography';
import {
Footer,
Label,
Logo,
Navigation,
NavigationGroup,
Expand Down Expand Up @@ -84,105 +85,110 @@ const meta = {
],
} satisfies Meta<typeof SidebarPrimitive>;

const renderSidebarContent = (
theme: Theme,
onThemeChange: (next: Theme) => void,
) => (
<>
<Logo>
<a className="flex items-center gap-2">
<img
className="flex-1 shrink-0 hidden group-data-[mode=expanded]/sidebar:block"
src="./logo-kubefirst.svg"
alt="Company logo"
/>
<img
className="block h-10 w-12 group-data-[mode=expanded]/sidebar:hidden"
src="./ray.svg"
alt="Company logo"
/>
<Typography
variant="labelSmall"
className="group-data-[mode=expanded]/sidebar:left-[35%] -bottom-5! group-data-[mode=expanded]/sidebar:bottom-0! text-[#ABADC6] lowercase"
>
v1.11.1
</Typography>
</a>
</Logo>

<Navigation className="mt-4 group-data-[mode=expanded]/sidebar:mt-0">
<NavigationGroup>
<NavigationOption>
<a className="flex items-center gap-2">
<ScatterPlotIcon className="w-6 h-6" />
<Label>Clusters</Label>
</a>
</NavigationOption>

<NavigationOption
role="button"
onClick={() => onThemeChange('kubefirst')}
isActive={theme === 'kubefirst'}
>
<PhotoLibraryIcon className="w-6 h-6" />
<Label>Environments</Label>
</NavigationOption>
</NavigationGroup>

<NavigationGroup title="Admin settings" titleClassName="uppercase">
<NavigationOption
role="button"
onClick={() => onThemeChange('light')}
isActive={theme === 'light'}
>
<ReceiptLongIcon className="w-6 h-6" />
<Label>Plans & Billing</Label>
</NavigationOption>

<NavigationOption>
<a className="flex items-center gap-2">
<CloudIcon className="w-6 h-6" />
<Label>Cloud accounts</Label>
</a>
</NavigationOption>
</NavigationGroup>
</Navigation>

<Footer>
<span className="text-[#81e2b4] flex items-center gap-2 justify-center font-semibold cursor-pointer">
<Star className="w-5 h-5" />
<Label>Upgrade to Business</Label>
</span>
</Footer>
</>
);

export const Sidebar = {
render: function SidebarStory() {
const [theme, setTheme] = useState<Theme>('kubefirst');

return (
<SidebarPrimitive theme={theme}>
<Logo>
<a className="flex items-center gap-2">
<img
className="flex-1 shrink-0 hidden md:block"
src="./logo-kubefirst.svg"
alt="Company logo"
/>
<img
className="block h-10 w-12 md:hidden"
src="./ray.svg"
alt="Company logo"
/>
<Typography
variant="labelSmall"
className="md:left-[35%] !bottom-[-20px] md:!bottom-0 text-[#ABADC6] lowercase"
>
v1.11.1
</Typography>
</a>
</Logo>

<Navigation className="mt-4 md:mt-0">
<NavigationGroup>
<NavigationOption>
<a className="flex items-center gap-2 md:mt-0">
<ScatterPlotIcon className="w-6 h-6" />{' '}
<Typography
variant="body1"
className="hidden md:block text-inherit"
>
Clusters
</Typography>
</a>
</NavigationOption>

<NavigationOption
role="button"
onClick={() => setTheme('kubefirst')}
isActive={theme === 'kubefirst'}
>
<PhotoLibraryIcon className="w-6 h-6" />{' '}
<Typography
variant="body1"
className="hidden md:block text-inherit"
>
Environments
</Typography>
</NavigationOption>
</NavigationGroup>

<NavigationGroup
title="Admin settings"
titleClassName="uppercase hidden md:block "
>
<NavigationOption
role="button"
onClick={() => setTheme('light')}
isActive={theme === 'light'}
>
<ReceiptLongIcon className="w-6 h-6" />{' '}
<Typography
variant="body1"
className="hidden md:block text-inherit"
>
Plans & Billing
</Typography>
</NavigationOption>

<NavigationOption>
<a className="flex items-center gap-2">
<CloudIcon className="w-6 h-6" />{' '}
<Typography
variant="body1"
className="hidden md:block text-inherit"
>
Cloud accounts
</Typography>
</a>
</NavigationOption>
</NavigationGroup>
</Navigation>

<Footer>
<span className="text-[#81e2b4] flex items-center gap-2 justify-center font-semibold cursor-pointer">
<Star className="w-5 h-5" />{' '}
<Typography
variant="body1"
className="hidden md:block text-inherit"
>
Upgrade to Business
</Typography>
</span>
</Footer>
{renderSidebarContent(theme, setTheme)}
</SidebarPrimitive>
);
},
} satisfies Story;

export const CollapsedMode = {
render: function CollapsedStory() {
const [theme, setTheme] = useState<Theme>('kubefirst');

return (
<SidebarPrimitive theme={theme} mode="collapsed">
{renderSidebarContent(theme, setTheme)}
</SidebarPrimitive>
);
},
} satisfies Story;

export const DrawerMode = {
render: function DrawerStory() {
const [theme, setTheme] = useState<Theme>('kubefirst');

return (
<SidebarPrimitive theme={theme} mode="drawer">
{renderSidebarContent(theme, setTheme)}
</SidebarPrimitive>
);
},
Expand Down
107 changes: 107 additions & 0 deletions lib/components/Sidebar/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ReactNode } from 'react';

import {
Footer,
Label,
Logo,
Navigation,
NavigationGroup,
Expand All @@ -19,6 +20,7 @@ describe('Sidebar', () => {
const defaultProps = {
minWith: 100,
maxWith: 400,
mode: 'expanded' as const,
...props,
} satisfies Props;

Expand Down Expand Up @@ -116,4 +118,109 @@ describe('Sidebar', () => {
expect(link).toHaveAttribute('href', mockUrl);
expect(mockOnClick).toHaveBeenCalledTimes(1);
});

describe('responsive modes', () => {
const renderWithGroups = (
mode: 'expanded' | 'collapsed' | 'drawer',
extraProps: Partial<Props> = {},
) =>
render(
<Sidebar mode={mode} {...extraProps}>
<Logo>Logo</Logo>
<Navigation>
<NavigationGroup title="Main">
<NavigationOption>
<Label>Clusters</Label>
</NavigationOption>
</NavigationGroup>

<NavigationGroup title="Admin">
<NavigationOption>
<Label>Billing</Label>
</NavigationOption>
</NavigationGroup>
</Navigation>
<Footer>
<Label>Upgrade</Label>
</Footer>
</Sidebar>,
);

it('shows labels and group titles in expanded mode', () => {
renderWithGroups('expanded');

expect(screen.getByText('Clusters')).toBeInTheDocument();
expect(screen.getByText('Billing')).toBeInTheDocument();
expect(screen.getByText('Upgrade')).toBeInTheDocument();
expect(screen.getByText('Main')).toBeInTheDocument();
expect(screen.getByText('Admin')).toBeInTheDocument();
});

it('hides labels and group titles in collapsed mode without expandOnHover', () => {
renderWithGroups('collapsed', { expandOnHover: false });

expect(screen.queryByText('Clusters')).not.toBeInTheDocument();
expect(screen.queryByText('Billing')).not.toBeInTheDocument();
expect(screen.queryByText('Upgrade')).not.toBeInTheDocument();
expect(screen.queryByText('Main')).not.toBeInTheDocument();
expect(screen.queryByText('Admin')).not.toBeInTheDocument();
});

it('keeps labels in the DOM but visually clipped in collapsed mode with expandOnHover', () => {
renderWithGroups('collapsed');

expect(screen.getByText('Clusters')).toHaveClass('max-w-0');
expect(screen.getByText('Billing')).toHaveClass('max-w-0');
expect(screen.queryByText('Main')).not.toBeInTheDocument();
expect(screen.queryByText('Admin')).not.toBeInTheDocument();
});

it('auto-inserts a separator between groups in collapsed mode', () => {
const { container } = renderWithGroups('collapsed');

const nav = container.querySelector('nav');
const groups = nav?.querySelectorAll('ul') ?? [];
const separators = nav?.querySelectorAll(':scope > div') ?? [];

expect(groups.length).toBe(2);
expect(separators.length).toBe(1);
});

it('does not insert separators in expanded mode', () => {
const { container } = renderWithGroups('expanded');

const nav = container.querySelector('nav');
const separators = nav?.querySelectorAll(':scope > div') ?? [];

expect(separators.length).toBe(0);
});

it('renders a hamburger trigger and hides the aside in drawer mode', async () => {
renderWithGroups('drawer');

const trigger = screen.getByRole('button', {
name: /open navigation/i,
});

expect(trigger).toBeInTheDocument();
expect(document.querySelector('aside')).not.toBeInTheDocument();
});

it('opens the drawer with expanded content when the hamburger is clicked', async () => {
renderWithGroups('drawer');

const user = userEvent.setup();
const trigger = screen.getByRole('button', {
name: /open navigation/i,
});

expect(screen.queryByText('Clusters')).not.toBeInTheDocument();

await user.click(trigger);

expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Clusters')).toBeInTheDocument();
expect(screen.getByText('Main')).toBeInTheDocument();
});
});
});
3 changes: 3 additions & 0 deletions lib/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FC } from 'react';

import {
Footer,
Label,
Logo,
Navigation,
NavigationGroup,
Expand Down Expand Up @@ -42,6 +43,7 @@ const Sidebar: FC<SidebarProps> & SidebarChildrenProps = (props) => (
Sidebar.displayName = 'KonstructSidebar';

Sidebar.Footer = Footer;
Sidebar.Label = Label;
Sidebar.Logo = Logo;
Sidebar.Navigation = Navigation;
Sidebar.NavigationGroup = NavigationGroup;
Expand All @@ -50,6 +52,7 @@ Sidebar.NavigationSeparator = NavigationSeparator;

export {
Footer,
Label,
Logo,
Navigation,
NavigationGroup,
Expand Down
Loading
Loading