Skip to content

Commit

Permalink
feat(EmptyState): add EmptyState component (#1491)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoannaSikora authored Jan 23, 2025
1 parent 61dbc8e commit bf5c0f0
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 0 deletions.
42 changes: 42 additions & 0 deletions packages/react-components/src/components/EmptyState/EmptyState.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Canvas, Controls, Meta, Title } from '@storybook/blocks';

import * as EmptyStateStories from './EmptyState.stories';

<Meta of={EmptyStateStories} />

<Title>EmptyState</Title>

[Intro](#Intro) | [Component API](#ComponentAPI) | [Content Spec](#ContentSpec)

## Intro <a id="Intro" />

The Empty State component is used to display a placeholder when there is no content to show in a container or page.
It helps communicate to users why content is missing and what actions they can take,
providing a better user experience than showing a blank space.

<Canvas of={EmptyStateStories.Default} sourceState="none" />

#### Example implementation

```jsx
<EmptyState
icon={InfoIcon}
title="No data"
description="There is no data to display"
actions={<Button kind="primary">Go to settings</Button>}
/>
```

## Component API <a id="ComponentAPI" />

<Controls of={EmptyStateStories.Default} sort="requiredFirst" />

## Content Spec <a id="ContentSpec" />

<a
className="sb-unstyled"
href="https://www.figma.com/design/9FDwjR8lYvincseDkKypC4/%5BDS%5D-Component-Documentations?node-id=24836-28755&t=XiUCfb1mKh8KKhcQ-0"
target="_blank"
>
Go to Figma documentation
</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
$base-class: 'empty-state';

.#{$base-class} {
display: flex;
align-items: center;

&--centered {
justify-content: center;
width: 100%;
height: 100%;
}

&--inline {
flex-direction: row;
justify-content: space-between;
}

&--full {
flex-direction: column;
justify-content: center;
padding: var(--spacing-16) var(--spacing-6);

@media (width <= 600px) {
padding: var(--spacing-8) var(--spacing-6);
}
}

&__image {
margin-bottom: var(--spacing-6);
border-radius: var(--radius-4);
width: auto;
max-width: min(100%, 600px);
height: auto;
max-height: min(100%, 300px);
object-fit: contain;
}

&__icon {
&--full {
margin-bottom: var(--spacing-2);
}
}

&__description {
max-width: 600px;
text-align: center;
}

&__content-inline {
display: flex;
gap: var(--spacing-2);
align-items: center;
}

&__title {
margin: 0;
margin-bottom: var(--spacing-2);
max-width: 600px;
text-align: center;
}

&__actions {
display: flex;
gap: var(--spacing-2);
margin-top: var(--spacing-4);

&--inline {
margin-top: 0;
}

@media (width <= 600px) {
flex-direction: column;
margin-top: var(--spacing-2);

&--inline {
margin-top: 0;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Info as InfoIcon } from '@livechat/design-system-icons';

import { render } from 'test-utils';

import { Button } from '../Button';
import { Icon } from '../Icon';

import { EmptyState } from './EmptyState';
import { IEmptyStateProps } from './types';

const renderComponent = (props: IEmptyStateProps) => {
return render(<EmptyState {...props} />);
};

describe('<EmptyState> component', () => {
it('should render with image', () => {
const { getByAltText } = renderComponent({
title: 'Test title',
description: 'Test description',
image: 'test-image.jpg',
});

const image = getByAltText('Test title');

expect(image).toBeVisible();
expect(image).toHaveAttribute('src', 'test-image.jpg');
});

it('should render actions when provided', () => {
const { getByText } = renderComponent({
title: 'Test title',
description: 'Test description',
actions: <Button>Test action</Button>,
});

expect(getByText('Test action')).toBeVisible();
});

it('should render with icon', () => {
const { getByTestId } = renderComponent({
title: 'Test title',
description: 'Test description',
icon: <Icon source={InfoIcon} />,
});

const icon = getByTestId('icon');

expect(icon).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
.empty-state-story {
&--centered {
height: 100vh;
}

&__custom-content {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
align-items: center;

&__row {
display: flex;
gap: var(--spacing-2);

@media (width <= 600px) {
flex-direction: column;
}
}

&__box {
border-radius: 8px;
background-color: var(--surface-secondary-default);
padding: var(--spacing-6);
width: 220px;
}

@media (width <= 600px) {
flex-direction: column;
}
}

&__icon {
svg {
width: 40px;
height: 40px;
color: var(--content-basic-disabled);
}
}

&__action-menu {
width: 200px;

&__item {
width: 400px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
Info as InfoIcon,
FacebookColored as FacebookIcon,
GoogleLightModeColored as GoogleIcon,
MoreHoriz,
} from '@livechat/design-system-icons';
import { Meta, StoryObj } from '@storybook/react';

import { ActionMenu } from '../ActionMenu';
import { Button } from '../Button';
import { Icon } from '../Icon';
import { ListItem } from '../ListItem';
import { Text } from '../Typography';

import { EmptyState } from './EmptyState';

import styles from './EmptyState.stories.module.scss';

export default {
title: 'Components/EmptyState',
component: EmptyState,
} as Meta<typeof EmptyState>;

type Story = StoryObj<typeof EmptyState>;

export const Default: Story = {
args: {
image: 'https://placehold.co/600x300',
title: 'No data',
description: 'There is no data to display',
actions: (
<>
<Button kind="primary">Primary action</Button>
<Button kind="secondary">Secondary action</Button>
<Button icon={<Icon source={InfoIcon} />} kind="secondary">
Tell me more
</Button>
<Button kind="link">Link action</Button>
</>
),
},
};

export const Inline: Story = {
decorators: [
(Story) => (
<div className={styles['empty-state-story__action-menu']}>
<ActionMenu
options={[
{
key: '1',
element: (
<div className={styles['empty-state-story__action-menu__item']}>
<ListItem>
<Story />
</ListItem>
</div>
),
},
]}
triggerRenderer={
<Button icon={<Icon source={MoreHoriz} kind="primary" />} />
}
openedOnInit
></ActionMenu>
</div>
),
],
args: {
icon: <Icon source={InfoIcon} />,
title: 'No data',
type: 'inline',
actions: <Button kind="link">Plain action</Button>,
},
parameters: {
controls: {
exclude: ['description', 'image'],
},
},
};

export const WithIcon: Story = {
args: {
icon: (
<Icon className={styles['empty-state-story__icon']} source={InfoIcon} />
),
title: 'No data',
description: 'There is no data to display',
actions: (
<>
<Button kind="primary">Primary action</Button>
<Button kind="secondary">Secondary action</Button>
</>
),
},
};

export const Centered: Story = {
args: {
centered: true,
icon: (
<Icon className={styles['empty-state-story__icon']} source={InfoIcon} />
),
title: 'No data',
description: 'There is no data to display',
actions: (
<>
<Button kind="primary">Primary action</Button>
<Button kind="secondary">Secondary action</Button>
</>
),
},
decorators: [
(Story) => (
<div className={styles['empty-state-story--centered']}>
<Story />
</div>
),
],
};

export const SmallImage: Story = {
args: {
image: 'https://placehold.co/250x200',
title: 'All tickets solved',
description: 'Follow the instruction to start working with tickets',
},
};

export const VeryBigImage: Story = {
args: {
image: 'https://placehold.co/800x300',
title: 'All tickets solved',
description: 'Follow the instruction to start working with tickets',
},
};

export const WithCustomContentAndNoIllustration: Story = {
args: {
title: 'Title up to 50 characters',
description:
'A description with a maximum of 100 characters. That usually means only one or two sentences.',
actions: (
<div className={styles['empty-state-story__custom-content']}>
<div className={styles['empty-state-story__custom-content__row']}>
<div className={styles['empty-state-story__custom-content__box']}>
<Icon size="xlarge" source={FacebookIcon} />
<Text bold>Facebook Messenger</Text>
</div>
<div className={styles['empty-state-story__custom-content__box']}>
<Icon size="xlarge" source={GoogleIcon} />
<Text bold>Google Messages</Text>
</div>
</div>
<Button kind="link">See all channels</Button>
</div>
),
},
};

export const WithoutActionsOnlyDescription: Story = {
args: {
icon: (
<Icon className={styles['empty-state-story__icon']} source={InfoIcon} />
),
title: 'No data',
description: (
<Text style={{ margin: 0 }}>
There is no data to display{' '}
<Button kind="link"> start by chatting with yourself</Button>
</Text>
),
},
};
Loading

0 comments on commit bf5c0f0

Please sign in to comment.