Skip to content

Commit

Permalink
feat: TET-854 add header component (#157)
Browse files Browse the repository at this point in the history
* feat: TET-854 fix input width in SearchInput component

* feat: TET-854 fix search width

* feat: TET-854 improve search styling

* feat: TET-854 add Header component

* feat: TET-854 fix build
  • Loading branch information
karolinaszarek authored Sep 16, 2024
1 parent 06a584e commit 5c5b46c
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 4 deletions.
13 changes: 13 additions & 0 deletions src/components/Header/Header.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { HeaderConfig, HeaderType } from './Header.styles';

import { ActionProp } from '@/types';

export type HeaderProps = {
type?: HeaderType;
counter?: number;
bottomBar?: boolean;
title: string;
description?: string;
custom?: HeaderConfig;
action?: ActionProp;
};
64 changes: 64 additions & 0 deletions src/components/Header/Header.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Header } from './Header';

import { HeaderDocs } from '@/docs-components/HeaderDocs';
import { TetDocs } from '@/docs-components/TetDocs';

const meta = {
title: 'Header',
component: Header,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'A collection of other components that forms the header of a page, used to indicate some subpage or group of content such as a table or listing.',
},
page: () => (
<TetDocs docs="https://docs.tetrisly.com/components/in-progress/header">
<HeaderDocs />
</TetDocs>
),
},
},
} satisfies Meta<typeof Header>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Complex: Story = {
args: {
title: 'Table title',
counter: 2,
bottomBar: true,
description: 'Description',
type: 'complex',
action: [
{
label: 'Add new',
},
{
label: 'Export csv',
},
],
},
};

export const Compact: Story = {
args: {
title: 'Table title',
counter: 0,
bottomBar: true,
description: 'Description',
type: 'compact',
action: [
{
label: 'Add new',
},
{
label: 'Export csv',
},
],
},
};
93 changes: 93 additions & 0 deletions src/components/Header/Header.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { BaseProps } from '@/types/BaseProps';

export type HeaderType = 'complex' | 'compact';

export type HeaderConfig = {
table: { type: Record<HeaderType, BaseProps> } & BaseProps;
titleAndDescriptionContainer: BaseProps;
titleContainer: BaseProps;
description: BaseProps;
counter: BaseProps;
actionContainer: BaseProps;
search: { type: Record<HeaderType, BaseProps> } & BaseProps;
bottomBar: { type: Record<HeaderType, BaseProps> } & BaseProps;
} & BaseProps;

export const defaultConfig = {
display: 'flex',
flexDirection: 'column',
minWidth: 'fit-content',
w: '100%',
h: 'fit-content',
backgroundColor: '$color-background-default',
table: {
w: '100%',
display: 'flex',
padding: '$space-component-padding-large $space-component-padding-2xLarge',
type: {
complex: {
h: '88px',
},
compact: {
h: '80px',
},
},
},
titleAndDescriptionContainer: {
w: '100%',
text: '$typo-header-xLarge',
display: 'flex',
flexDirection: 'column',
},
counter: {
w: '$size-2xSmall',
h: '$size-2xSmall',
},
titleContainer: {
display: 'flex',
alignItems: 'center',
gap: '$space-component-gap-small',
},
description: {
text: '$typo-body-medium',
color: '$color-content-secondary',
gap: '$space-component-gap-small',
},
actionContainer: {
display: 'flex',
gap: '$space-component-gap-large',
alignItems: 'center',
},
bottomBar: {
borderTopWidth: '$border-width-small',
borderStyle: '$border-style-solid',
borderColor: '$color-border-default',
display: 'flex',
type: {
complex: {
padding:
'$space-component-padding-large $space-component-padding-2xLarge',
gap: '$space-component-gap-small',
h: '$size-2xLarge',
},
compact: {
padding:
'$space-component-padding-small $space-component-padding-large',
h: '$size-large',
},
},
},
search: {
w: '105px',
type: {
complex: {
marginLeft: 'auto',
},
compact: {},
},
},
} satisfies HeaderConfig;

export const headerStyles = {
defaultConfig,
};
80 changes: 80 additions & 0 deletions src/components/Header/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Header } from './Header';
import { render } from '../../tests/render';

import { customPropTester } from '@/tests/customPropTester';

const getHeader = (jsx: JSX.Element) => {
const { getByTestId, queryByTestId } = render(jsx);

return {
actionContainer: queryByTestId('header-action-container'),
bottomBar: queryByTestId('header-bottom-bar'),
counter: queryByTestId('header-counter'),
description: queryByTestId('header-description'),
header: getByTestId('header'),
search: queryByTestId('header-search'),
title: getByTestId('header-title'),
};
};

describe('Header', () => {
customPropTester(<Header type="complex" title="" />, {
containerId: 'header',
props: {},
innerElements: {},
});

it('should render the header', () => {
const { header } = getHeader(<Header type="complex" title="" />);
expect(header).toBeInTheDocument();
});

it('should render the correct title', () => {
const { title } = getHeader(<Header title="Title" />);
expect(title).toHaveTextContent('Title');
});

it('should render the correct title', () => {
const { title } = getHeader(<Header title="Title" />);
expect(title).toHaveTextContent('Title');
});

it('should render the correct description', () => {
const { description } = getHeader(
<Header description="Description" title="" />,
);
expect(description).toHaveTextContent('Description');
});

it('should not render the description', () => {
const { description } = getHeader(<Header title="" />);
expect(description).toBeNull();
});

it('should render the counter', () => {
const { counter } = getHeader(<Header counter={0} title="" />);
expect(counter).toBeInTheDocument();
});

it('should render the bottom bar', () => {
const { bottomBar } = getHeader(<Header bottomBar title="" />);
expect(bottomBar).toBeInTheDocument();
});

it('should not render the bottom bar', () => {
const { bottomBar } = getHeader(<Header title="" />);
expect(bottomBar).toBeNull();
});

it('should render the search', () => {
const { search } = getHeader(<Header bottomBar title="" />);
expect(search).toBeInTheDocument();
});

it('should render the action container', () => {
const { actionContainer } = getHeader(
<Header action={[{ label: 'Label' }, { label: 'Label' }]} title="" />,
);
expect(actionContainer).toBeInTheDocument();
});
});
103 changes: 103 additions & 0 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { MarginProps } from '@xstyled/styled-components';
import { FC, useMemo } from 'react';

import { HeaderProps } from './Header.props';
import { stylesBuilder } from './stylesBuilder';
import { Button } from '../Button';
import { Counter } from '../Counter';
import { CounterConfig } from '../Counter/Counter.styles';
import { InlineSearchInput } from '../InlineSearchInput';
import { SelectablePill } from '../SelectablePill';

import { useAction } from '@/hooks';
import { tet } from '@/tetrisly';

export const Header: FC<HeaderProps & MarginProps> = ({
bottomBar,
counter,
description,
title,
type = 'complex',
action,
custom,
...restProps
}) => {
const styles = useMemo(() => stylesBuilder(type, custom), [custom, type]);

const [firstAction, secondAction] = useAction(action);

const isComplexType = type === 'complex';
const customSearchStyle = { innerElements: { input: { w: '100%' } } };

return (
<tet.div {...styles.container} data-testid="header" {...restProps}>
<tet.div {...styles.table} data-testid="header-table">
<tet.div
{...styles.titleAndDescriptionContainer}
data-testid="header-title-and-description-container"
>
<tet.div
{...styles.titleContainer}
data-testid="header-title-container"
>
<tet.span data-testid="header-title">{title}</tet.span>
{!(counter === undefined) && (
<Counter
data-testid="header-counter"
number={counter}
custom={styles.counter as CounterConfig}
/>
)}
</tet.div>

{!!description && (
<tet.div {...styles.description} data-testid="header-description">
{description}
</tet.div>
)}
</tet.div>
{firstAction && (
<tet.div
{...styles.actionContainer}
data-testid="header-action-container"
>
{/* should it always be like this - first button is primary and second secondary or user decides about it? */}
{secondAction && (
<Button
variant="default"
appearance="secondary"
size={isComplexType ? 'medium' : 'small'}
{...secondAction}
/>
)}
<Button
variant="default"
appearance="primary"
size={isComplexType ? 'medium' : 'small'}
{...firstAction}
/>
</tet.div>
)}
</tet.div>
{!!bottomBar && (
<tet.div {...styles.bottomBar} data-testId="header-bottom-bar">
{type === 'complex' && (
<>
{/* not sure how it should work, if and how user passes a data about inside components */}
<SelectablePill text="Name" />
<SelectablePill text="E-mail" />
<SelectablePill text="Date added" />
<Button label="Filters" size="small" />
</>
)}
<InlineSearchInput
{...styles.search}
placeholder="Search..."
custom={customSearchStyle}
data-testId="header-search"
/>
</tet.div>
)}
</tet.div>
);
};
3 changes: 3 additions & 0 deletions src/components/Header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Header } from './Header';
export type { HeaderProps } from './Header.props';
export { headerStyles } from './Header.styles';
Loading

0 comments on commit 5c5b46c

Please sign in to comment.