Skip to content

Commit

Permalink
feat: Implementing StudioPageHeader component (#13559)
Browse files Browse the repository at this point in the history
  • Loading branch information
wrt95 authored Sep 18, 2024
1 parent 111b4c9 commit 1fa42ed
Show file tree
Hide file tree
Showing 38 changed files with 801 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Canvas, Meta } from '@storybook/blocks';
import { Heading, Paragraph } from '@digdir/designsystemet-react';
import * as StudioPageHeaderStories from './StudioPageHeader.stories';

<Meta of={StudioPageHeaderStories} />

<Heading level={1} size='small'>
StudioPageHeader
</Heading>
<Paragraph>
StudioPageHeader is used to display the header banner at the top of a page. If you want to add
buttons in the header, use the `<StudioPageHeaderButton />` component. This component is created
to match the colours of the page header.
</Paragraph>

<Canvas of={StudioPageHeaderStories.Preview} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.studioPageHeader  {
/*
To avoid the header to go above the modal, and stay below
the comboboxes on "Designer" page, this is the z-index that
satisfies both.
*/
z-index: 2000;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { StudioPageHeader, StudioPageHeaderButton } from './index';
import { StudioParagraph } from '../StudioParagraph';

const PreviewComponent = (args): React.ReactElement => (
<StudioPageHeader {...args}>
<StudioPageHeader.Main>
<StudioPageHeader.Left title='Left' showTitle={false} />
<StudioPageHeader.Center>
<StudioPageHeaderButton
color={args.variant === 'regular' ? 'light' : 'dark'}
variant={args.variant}
>
Button
</StudioPageHeaderButton>
</StudioPageHeader.Center>
<StudioPageHeader.Right>
<StudioParagraph size='sm' style={{ color: 'white', paddingLeft: '20px' }}>
Right
</StudioParagraph>
</StudioPageHeader.Right>
</StudioPageHeader.Main>
<StudioPageHeader.Sub>
<div style={{ padding: '10px' }}>
<StudioParagraph size='sm' style={{ color: 'white' }}>
<StudioPageHeaderButton
color={args.variant === 'regular' ? 'dark' : 'light'}
variant={args.variant}
>
Subheader Button
</StudioPageHeaderButton>
</StudioParagraph>
</div>
</StudioPageHeader.Sub>
</StudioPageHeader>
);

type Story = StoryFn<typeof StudioPageHeader>;

const meta: Meta = {
title: 'StudioPageHeader',
component: PreviewComponent,
argTypes: {
variant: {
control: 'radio',
options: ['regular', 'preview'],
},
},
};
export const Preview: Story = (args): React.ReactElement => <PreviewComponent {...args} />;

Preview.args = {
variant: 'regular',
};

export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { StudioPageHeader, type StudioPageHeaderProps } from './StudioPageHeader';

describe('StudioPageHeader', () => {
it('should render the children passed to it', () => {
const childText = 'Test Child';
renderStudioPageHeader({ children: <div>{childText}</div> });

expect(screen.getByText(childText)).toBeInTheDocument();
});

test('the root container should have role banner', () => {
renderStudioPageHeader({ children: <div>Test Child</div> });

const banner = screen.getByRole('banner');
expect(banner).toBeInTheDocument();
});
});

const renderStudioPageHeader = (props: Partial<StudioPageHeaderProps> = {}) => {
const { children, variant = 'regular' } = props;

return render(
<StudioPageHeader variant={variant}>{children ?? <div>Default Child</div>}</StudioPageHeader>,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, { type ReactNode } from 'react';
import classes from './StudioPageHeader.module.css';
import { type StudioPageHeaderVariant } from './types/StudioPageHeaderVariant';
import { StudioPageHeaderContextProvider } from './context';

export type StudioPageHeaderProps = {
children: ReactNode;
variant?: StudioPageHeaderVariant;
};

export const StudioPageHeader = ({
children,
variant = 'regular',
}: StudioPageHeaderProps): React.ReactElement => {
return (
<StudioPageHeaderContextProvider variant={variant}>
<div role='banner' className={classes.studioPageHeader}>
{children}
</div>
</StudioPageHeaderContextProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.button {
--studio-button-regular-dark: #393b51;
--studio-button-regular-medium: #3e4f62;
--studio-button-regular-light: #657281;

--studio-button-preview-dark: #0c6536;
--studio-button-preview-medium: #118849;
--studio-button-preview-light: #67b38a;

color: var(--fds-semantic-background-default);
}

.regularDark {
background-color: var(--studio-button-regular-dark);
}
.regularDark:hover {
background-color: var(--studio-button-regular-medium);
}

.regularLight {
background-color: var(--studio-button-regular-medium);
}

.regularLight:hover {
background-color: var(--studio-button-regular-light);
}

.previewDark {
background-color: var(--studio-button-preview-dark);
}
.previewDark:hover {
background-color: var(--studio-button-preview-medium);
}

.previewLight {
background-color: var(--studio-button-preview-medium);
}
.previewLight:hover {
background-color: var(--studio-button-preview-light);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { StudioPageHeaderButton, type StudioPageHeaderButtonProps } from './StudioPageHeaderButton';
import userEvent from '@testing-library/user-event';

describe('StudioPageHeaderButton', () => {
it('should apply the correct class based on variant and color - regular dark', () => {
renderStudioPageHeaderButton({ variant: 'regular', color: 'dark' });

const button = screen.getByRole('button', { name: buttonText });
expect(button).toHaveClass('regularDark');
});

it('should apply the correct class based on variant and color - regular light', () => {
renderStudioPageHeaderButton({ variant: 'regular', color: 'light' });

const button = screen.getByRole('button', { name: buttonText });
expect(button).toHaveClass('regularLight');
});

it('should apply the correct class based on variant and color - preview dark', () => {
renderStudioPageHeaderButton({ variant: 'preview', color: 'dark' });

const button = screen.getByRole('button', { name: buttonText });
expect(button).toHaveClass('previewDark');
});

it('should apply the correct class based on variant and color - preview light', () => {
renderStudioPageHeaderButton({ variant: 'preview', color: 'light' });

const button = screen.getByRole('button', { name: buttonText });
expect(button).toHaveClass('previewLight');
});

it('should forward ref to the button element', () => {
const ref = React.createRef<HTMLButtonElement>();
renderStudioPageHeaderButton({ color: 'dark' }, ref);

expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});

it('should call the onClick function when the button is clicked', async () => {
const user = userEvent.setup();

const mockOnClick = jest.fn();
renderStudioPageHeaderButton({ onClick: mockOnClick });

const button = screen.getByRole('button', { name: buttonText });
await user.click(button);

expect(mockOnClick).toHaveBeenCalledTimes(1);
});
});

const buttonText: string = 'Button';

const defaultProps: StudioPageHeaderButtonProps = {
color: 'dark',
variant: 'regular',
};

const renderStudioPageHeaderButton = (
props: Partial<StudioPageHeaderButtonProps> = {},
ref?: React.Ref<HTMLButtonElement>,
) => {
return render(
<StudioPageHeaderButton ref={ref} {...defaultProps} {...props}>
{buttonText}
</StudioPageHeaderButton>,
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { forwardRef, type ReactElement } from 'react';
import classes from './StudioPageHeaderButton.module.css';
import { StudioButton, type StudioButtonProps } from '../../StudioButton';
import { type StudioPageHeaderColor } from '../types/StudioPageHeaderColor';
import cn from 'classnames';
import { type StudioPageHeaderVariant } from '../types/StudioPageHeaderVariant';

export type StudioPageHeaderButtonProps = {
color: StudioPageHeaderColor;
variant: StudioPageHeaderVariant;
} & Omit<StudioButtonProps, 'color' | 'variant'>;

export const StudioPageHeaderButton = forwardRef<HTMLButtonElement, StudioPageHeaderButtonProps>(
({ color, variant, ...rest }, ref): ReactElement => {
const getClassName = (): string => {
if (variant === 'regular' && color === 'dark') return classes.regularDark;
if (variant === 'regular' && color === 'light') return classes.regularLight;
if (variant === 'preview' && color === 'dark') return classes.previewDark;
if (variant === 'preview' && color === 'light') return classes.previewLight;
};

return <StudioButton ref={ref} className={cn(classes.button, getClassName())} {...rest} />;
},
);

StudioPageHeaderButton.displayName = 'StudioPageHeaderButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StudioPageHeaderButton } from './StudioPageHeaderButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { StudioPageHeaderCenter, type StudioPageHeaderCenterProps } from './StudioPageHeaderCenter';

describe('StudioPageHeaderCenter', () => {
it('should render the children passed as prop', () => {
const childText = 'Test Child';
renderStudioPageHeaderCenter({ children: <span>{childText}</span> });

expect(screen.getByText(childText)).toBeInTheDocument();
});

it('should render multiple children elements', () => {
renderStudioPageHeaderCenter({
children: (
<>
<span>Child 1</span>
<span>Child 2</span>
</>
),
});

expect(screen.getByText('Child 1')).toBeInTheDocument();
expect(screen.getByText('Child 2')).toBeInTheDocument();
});
});

const renderStudioPageHeaderCenter = (props: StudioPageHeaderCenterProps) => {
return render(<StudioPageHeaderCenter {...props} />);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { type ReactNode, type ReactElement } from 'react';

export type StudioPageHeaderCenterProps = {
children: ReactNode;
};

export const StudioPageHeaderCenter = ({ children }: StudioPageHeaderCenterProps): ReactElement => {
return <div>{children}</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { StudioPageHeaderCenter } from './StudioPageHeaderCenter';
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { type ReactElement } from 'react';

export const DigdirLogo = (): ReactElement => {
return (
<svg role='img' width='30' height='30' fill='none' xmlns='http://www.w3.org/2000/svg'>
<title>Altinn logo</title>
<path
d='M14.9512 0H0.590024C0.25961 0 0 0.259054 0 0.588759V28.5312C0 28.8609 0.25961 29.12 0.590024 29.12H14.9512C22.8457 28.9434 29.1826 22.4906 29.1826 14.5541C29.1826 6.62942 22.8339 0.176628 14.9512 0ZM16.3437 24.7632C9.23977 25.9289 3.20973 19.9 4.36618 12.8114C5.0742 8.51345 8.54354 5.05155 12.8389 4.35681C19.9546 3.20285 25.9846 9.21996 24.8282 16.3204C24.1202 20.6066 20.6508 24.0685 16.3437 24.7632Z'
fill='white'
/>
</svg>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.wrapper {
display: flex;
align-items: center;
gap: var(--fds-spacing-1);
overflow: hidden;
padding: var(--fds-spacing-2);
}

.titleText {
color: var(--fds-semantic-background-default);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
Loading

0 comments on commit 1fa42ed

Please sign in to comment.