Skip to content

Commit

Permalink
chore(resource-adm): copy LeftNavigationBar component to resourceadm (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mgunnerud authored Nov 11, 2024
1 parent 9aba26e commit aeec096
Show file tree
Hide file tree
Showing 19 changed files with 892 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.icon {
font-size: 1.8rem;
color: var(--fds-semantic-text-neutral-default);
}

.backButton {
border-bottom: 2px solid var(--fds-semantic-border-divider-default);
}

.backButton:hover {
background-color: var(--fds-semantic-surface-neutral-subtle-hover);
}

.buttonText {
color: var(--fds-semantic-text-neutral-default);
margin-left: 10px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import type { GoBackButtonProps } from './GoBackButton';
import { GoBackButton } from './GoBackButton';
import { MemoryRouter } from 'react-router-dom';

const mockBackButtonText: string = 'Go back';

describe('GoBackButton', () => {
afterEach(jest.clearAllMocks);

const defaultProps: GoBackButtonProps = {
className: '.navigationElement',
to: '/back',
text: mockBackButtonText,
};

it('calls the "onClickBackButton" function when the button is clicked', () => {
render(
<MemoryRouter initialEntries={['/']}>
<GoBackButton {...defaultProps} />
</MemoryRouter>,
);

const backButton = screen.getByRole('link', { name: mockBackButtonText });
expect(backButton).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ReactNode } from 'react';
import React from 'react';
import classes from './GoBackButton.module.css';
import cn from 'classnames';
import { ArrowLeftIcon } from '@studio/icons';
import { Paragraph } from '@digdir/designsystemet-react';
import { NavLink } from 'react-router-dom';

export type GoBackButtonProps = {
/**
* Classname for navigation element
*/
className?: string;
/**
* Text on the back button
*/
text: string;
/**
* Where to navigate to
*/
to: string;
};

/**
* @component
* Displays the back button on top of the LeftNavigationBar
*
* @example
* <GoBackButton
* className={classes.navigationElement}
* text={backButtonText}
* to={someUrl}
* />
*
* @property {string}[className] - Classname for navigation element
* @property {string}[text] - Text on the back button
* @property {string}[to] - Where to navigate to
*
* @returns {ReactNode} - The rendered component
*/
export const GoBackButton = ({ className, text, to }: GoBackButtonProps): ReactNode => {
return (
<NavLink className={cn(className, classes.backButton)} to={to}>
<ArrowLeftIcon className={classes.icon} />
<Paragraph asChild size='small' variant='short' className={classes.buttonText}>
<span>{text}</span>
</Paragraph>
</NavLink>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GoBackButton } from './GoBackButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.navigationBar {
background-color: var(--fds-semantic-background-subtle);
width: 250px;
height: 100%;
left: 0;
border-right: 1px solid var(--fds-semantic-border-divider-default);
border-top: none;
}

.navigationElements {
display: flex;
flex-direction: column;
}

.navigationElement {
display: flex;
align-items: center;
padding-left: 15px;
padding-block: 20px;
border: none;
border-bottom: 1px solid var(--fds-semantic-border-divider-default);
background-color: var(--fds-semantic-background-subtle);
cursor: pointer;
}

.navigationElement:hover {
background-color: var(--fds-semantic-background-default);
z-index: 1;
}

.navigationElement:focus-visible {
background-color: var(--fds-semantic-background-default);
z-index: 2;
border-bottom: none;

outline: var(--focus-border-width) solid var(--focus-border-outline-color);
outline-offset: var(--focus-border-width);
box-shadow:
8px 0 0 0 var(--fds-semantic-border-action-active) inset,
0 0 0 var(--focus-border-width) var(--focus-border-inner-color);
}

.navigationElement:focus:not(:focus-visible) {
outline: none;
box-shadow: none;
background-color: var(--fds-semantic-background-subtle);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React from 'react';
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { LeftNavigationBarProps } from './LeftNavigationBar';
import { LeftNavigationBar } from './LeftNavigationBar';
import type { LeftNavigationTab, TabAction } from './Tab/LeftNavigationTab';
import { TestFlaskIcon } from '@studio/icons';
import { MemoryRouter } from 'react-router-dom';
import { textMock } from '@studio/testing/mocks/i18nMock';

const mockOnClick = jest.fn();

const mockTo: string = '/test';

const mockLinkAction1: TabAction = {
type: 'link',
to: mockTo,
onClick: mockOnClick,
};

const mockLinkAction2: TabAction = {
type: 'link',
to: mockTo,
};

const mockButtonAction: TabAction = {
type: 'button',
onClick: mockOnClick,
};

const mockTabId1: string = 'tab1';
const mockTabId2: string = 'tab2';
const mockTabId3: string = 'tab3';

const mockTabs: LeftNavigationTab[] = [
{
icon: <TestFlaskIcon />,
tabName: `test.test_${mockTabId1}`,
tabId: mockTabId1,
action: mockLinkAction1,
isActiveTab: true,
},
{
icon: <TestFlaskIcon />,
tabName: `test.test_${mockTabId2}`,
tabId: mockTabId2,
action: mockButtonAction,
isActiveTab: false,
},
{
icon: <TestFlaskIcon />,
tabName: `test.test_${mockTabId3}`,
tabId: mockTabId3,
action: mockLinkAction2,
isActiveTab: false,
},
];

const mockBackButtonText: string = 'Go back';
const mockBackButtonHref: string = '/back';

describe('LeftNavigationBar', () => {
afterEach(jest.clearAllMocks);

const defaultProps: LeftNavigationBarProps = {
tabs: mockTabs,
upperTab: 'backButton',
backLink: mockBackButtonHref,
backLinkText: mockBackButtonText,
selectedTab: mockTabId1,
};

it('calls the onClick function when a tab is clicked and action type is button', async () => {
const user = userEvent.setup();
render(defaultProps);

const nextTab = mockTabs[1];

const tab2 = screen.getByRole('tab', { name: textMock(nextTab.tabName) });
await user.click(tab2);
expect(nextTab.action.onClick).toHaveBeenCalledTimes(1);
});

it('calls the onClick function when a tab is clicked and action type is link and onClick is present', async () => {
const user = userEvent.setup();
render(defaultProps);

const nextTab = mockTabs[1];
const tab2 = screen.getByRole('tab', { name: textMock(nextTab.tabName) });
await user.click(tab2);

const newNextTab = mockTabs[0];
const tab1 = screen.getByRole('tab', { name: textMock(newNextTab.tabName) });
await user.click(tab1);

expect(newNextTab.action.onClick).toHaveBeenCalledTimes(1);
});

it('does not call the onClick function when a tab is clicked and action type is link and onClick is not present', async () => {
const user = userEvent.setup();
render(defaultProps);

const nextTab = mockTabs[2];

const tab3 = screen.getByRole('tab', { name: textMock(nextTab.tabName) });
await user.click(tab3);
expect(mockOnClick).not.toHaveBeenCalled();
});

it('does not call the onClick function when the active tab is clicked', async () => {
const user = userEvent.setup();
render(defaultProps);

const currentTab = mockTabs[0];

const tab1 = screen.getByRole('tab', { name: textMock(currentTab.tabName) });
await user.click(tab1);
expect(currentTab.action.onClick).toHaveBeenCalledTimes(0);
});

it('displays back button when "upperTab" is backButton and "backButtonHref" and "backButtonText" is present', () => {
render(defaultProps);

const backButton = screen.getByRole('link', { name: mockBackButtonText });
expect(backButton).toBeInTheDocument();
});

it('does not display the back button when "upperTab" is backButton and "backButtonHref" or "backButtonText" is not present', () => {
render({ tabs: mockTabs, selectedTab: mockTabId1 });

const backButton = screen.queryByRole('link', { name: mockBackButtonText });
expect(backButton).not.toBeInTheDocument();
});

it('handles tab navigation correctly', async () => {
const user = userEvent.setup();
render({ tabs: mockTabs, selectedTab: mockTabId1 });

await user.tab();
expect(getTabItem(mockTabId1)).toHaveFocus();
await user.keyboard('{arrowdown}');
expect(getTabItem(mockTabId2)).toHaveFocus();
await user.keyboard('{arrowdown}');
expect(getTabItem(mockTabId3)).toHaveFocus();
await user.keyboard('{arrowdown}');
expect(getTabItem(mockTabId1)).toHaveFocus();
await user.keyboard('{arrowup}');
expect(getTabItem(mockTabId3)).toHaveFocus();
await user.keyboard('{arrowup}');
expect(getTabItem(mockTabId2)).toHaveFocus();
});

it('selects a tab when pressing "enter"', async () => {
const user = userEvent.setup();
render({ tabs: mockTabs, selectedTab: mockTabId1 });

await user.tab();
expect(getTabItem(mockTabId1)).toHaveFocus();
await user.keyboard('{arrowdown}');
expect(getTabItem(mockTabId2)).toHaveFocus();

await user.keyboard('{enter}');
expect(mockTabs[1].action.onClick).toHaveBeenCalledTimes(1);
});
});

const getTabItem = (tabId: string) => {
const tabName: string = `test.test_${tabId}`;
return screen.getByRole('tab', { name: textMock(tabName) });
};

const render = (props: LeftNavigationBarProps) => {
return rtlRender(
<MemoryRouter initialEntries={['/']}>
<LeftNavigationBar {...props} />
</MemoryRouter>,
);
};
Loading

0 comments on commit aeec096

Please sign in to comment.