-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: APP-2715 - Implement EmptyState Component (#77)
- Loading branch information
1 parent
b348478
commit 7d35e4b
Showing
9 changed files
with
357 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import type { IButtonBaseProps, IButtonElementProps } from '../../button/button.api'; | ||
import type { IIllustrationHumanProps, IIllustrationObjectProps } from '../../illustrations'; | ||
|
||
export interface IEmptyStateBaseProps { | ||
/** | ||
* Title of the empty state. | ||
*/ | ||
heading: string; | ||
/** | ||
* Description of the empty state. | ||
*/ | ||
description?: string; | ||
/** | ||
* Renders the state as horitontal when set to false. | ||
* @default true | ||
*/ | ||
isStacked?: boolean; | ||
/** | ||
* Primary button of the empty state. The primary button is only rendered on the stacked variant. | ||
*/ | ||
primaryButton?: IEmptyStateButton; | ||
/** | ||
* Secondary button of the empty state. | ||
*/ | ||
secondaryButton?: IEmptyStateButton; | ||
/** | ||
* Additional class names to be added to the empty state. | ||
*/ | ||
className?: string; | ||
} | ||
|
||
export type IEmptyStateButton = Omit<IButtonBaseProps, 'variant' | 'size' | 'children'> & | ||
IButtonElementProps & { | ||
/** | ||
* Button label to be rendered. | ||
*/ | ||
label: string; | ||
}; | ||
|
||
export interface IEmptyStateHumanIllustrationProps extends IEmptyStateBaseProps { | ||
/** | ||
* @see IIllustrationHumanProps | ||
*/ | ||
humanIllustration: IIllustrationHumanProps; | ||
objectIllustration?: never; | ||
} | ||
|
||
export interface IEmptyStateObjectIllustrationProps extends IEmptyStateBaseProps { | ||
/** | ||
* @see IIllustrationObjectProps | ||
*/ | ||
objectIllustration: IIllustrationObjectProps; | ||
humanIllustration?: never; | ||
} | ||
|
||
export type IEmptyStateProps = IEmptyStateHumanIllustrationProps | IEmptyStateObjectIllustrationProps; |
106 changes: 106 additions & 0 deletions
106
src/components/states/emptyState/emptyState.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { IconType } from '../../icon'; | ||
import { EmptyState } from './emptyState'; | ||
|
||
const meta: Meta<typeof EmptyState> = { | ||
title: 'components/States/EmptyState', | ||
component: EmptyState, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/ISSDryshtEpB7SUSdNqAcw/branch/jfKRr1V9evJUp1uBeyP3Zz/Aragon-ODS?type=design&node-id=10095%3A21633&mode=dev&t=FtMO7nBXAzYBFGaW-1', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof EmptyState>; | ||
|
||
/** | ||
* Default EmptyState component with minimum props. | ||
*/ | ||
export const Default: Story = { | ||
args: { | ||
heading: 'Heading', | ||
description: 'Description', | ||
objectIllustration: { object: 'LIGHTBULB' }, | ||
}, | ||
}; | ||
|
||
/** | ||
* Stacked EmptyState component with full props examples for Object Illustration. | ||
*/ | ||
export const StackedFullWithObject: Story = { | ||
args: { | ||
heading: 'Heading', | ||
description: 'Description', | ||
objectIllustration: { object: 'LIGHTBULB' }, | ||
primaryButton: { | ||
label: 'Label', | ||
iconLeft: IconType.ADD, | ||
iconRight: IconType.CHEVRON_RIGHT, | ||
onClick: () => alert('Primary Button Clicked'), | ||
}, | ||
secondaryButton: { | ||
label: 'Label', | ||
iconLeft: IconType.ADD, | ||
iconRight: IconType.CHEVRON_RIGHT, | ||
onClick: () => alert('Secondary Button Clicked'), | ||
}, | ||
}, | ||
}; | ||
/** | ||
* Non-Stacked EmptyState component with full props examples for Object Illustration. <br /> | ||
* **Warning:** Non-Stacked EmptyState with Human Illustration is not supported visually. | ||
* As displayed, use an object illustration instead for best layout. | ||
*/ | ||
export const NonStackedFullWithObject: Story = { | ||
args: { | ||
heading: 'Heading', | ||
description: 'Description', | ||
isStacked: false, | ||
objectIllustration: { object: 'LIGHTBULB' }, | ||
primaryButton: { | ||
label: 'Label', | ||
iconLeft: IconType.ADD, | ||
iconRight: IconType.CHEVRON_RIGHT, | ||
onClick: () => alert('Primary Button Clicked'), | ||
}, | ||
secondaryButton: { | ||
label: 'Label', | ||
iconLeft: IconType.ADD, | ||
iconRight: IconType.CHEVRON_RIGHT, | ||
onClick: () => alert('Secondary Button Clicked'), | ||
}, | ||
}, | ||
}; | ||
/** | ||
* Stacked EmptyState component with full props examples for Human Illustation. | ||
*/ | ||
export const StackedFullWithHuman: Story = { | ||
args: { | ||
heading: 'Heading', | ||
description: 'Description', | ||
humanIllustration: { | ||
body: 'VOTING', | ||
hairs: 'MIDDLE', | ||
accessory: 'EARRINGS_RHOMBUS', | ||
sunglasses: 'BIG_ROUNDED', | ||
expression: 'SMILE', | ||
}, | ||
primaryButton: { | ||
label: 'Label', | ||
iconLeft: IconType.ADD, | ||
iconRight: IconType.CHEVRON_RIGHT, | ||
onClick: () => alert('Primary Button Clicked'), | ||
}, | ||
secondaryButton: { | ||
label: 'Label', | ||
iconLeft: IconType.ADD, | ||
iconRight: IconType.CHEVRON_RIGHT, | ||
onClick: () => alert('Secondary Button Clicked'), | ||
}, | ||
}, | ||
}; | ||
|
||
export default meta; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { render, screen } from '@testing-library/react'; | ||
import { EmptyState } from './emptyState'; | ||
import type { IEmptyStateProps } from './emptyState.api'; | ||
|
||
describe('<EmptyState /> component', () => { | ||
const createTestComponent = (props?: Partial<IEmptyStateProps>) => { | ||
const commonProps = { | ||
heading: 'test-heading', | ||
}; | ||
|
||
if (props?.humanIllustration) { | ||
return <EmptyState humanIllustration={props.humanIllustration} {...commonProps} {...props} />; | ||
} else { | ||
const { humanIllustration, objectIllustration = { object: 'ACTION' }, ...otherProps } = props ?? {}; | ||
|
||
return <EmptyState objectIllustration={objectIllustration} {...commonProps} {...otherProps} />; | ||
} | ||
}; | ||
|
||
it('renders the EmptyState component stacked with full props and object illustration', () => { | ||
const objectIllustration = { object: 'LIGHTBULB' } as const; | ||
const primaryButton = { label: 'Label' }; | ||
const secondaryButton = { label: 'Label' }; | ||
render(createTestComponent({ objectIllustration, primaryButton, secondaryButton })); | ||
|
||
expect(screen.getByText('test-heading')).toBeInTheDocument(); | ||
const buttons = screen.getAllByRole('button', { name: 'Label' }); | ||
expect(buttons).toHaveLength(2); | ||
expect(screen.getByTestId('LIGHTBULB')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the EmptyState component stacked with full props and human illustration', () => { | ||
const humanIllustration = { body: 'VOTING', expression: 'SMILE' } as const; | ||
const primaryButton = { label: 'Label' }; | ||
const secondaryButton = { label: 'Label' }; | ||
render(createTestComponent({ humanIllustration, primaryButton, secondaryButton })); | ||
|
||
expect(screen.getByText('test-heading')).toBeInTheDocument(); | ||
const buttons = screen.getAllByRole('button', { name: 'Label' }); | ||
expect(buttons).toHaveLength(2); | ||
expect(screen.getByTestId('VOTING')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the EmptyState component unstacked with full props and object illustration', () => { | ||
const objectIllustration = { object: 'LIGHTBULB' } as const; | ||
const primaryButton = { label: 'Label' }; | ||
const secondaryButton = { label: 'Label' }; | ||
const isStacked = false; | ||
render(createTestComponent({ isStacked, objectIllustration, primaryButton, secondaryButton })); | ||
|
||
expect(screen.getByText('test-heading')).toBeInTheDocument(); | ||
const buttons = screen.getAllByRole('button', { name: 'Label' }); | ||
expect(buttons).toHaveLength(2); | ||
const objectImage = screen.getByTestId('LIGHTBULB'); | ||
expect(objectImage).toHaveClass('order-last'); | ||
}); | ||
|
||
it('renders the EmptyState component unstacked with full props and human illustration', () => { | ||
const humanIllustration = { body: 'VOTING', expression: 'SMILE' } as const; | ||
const primaryButton = { label: 'Label' }; | ||
const secondaryButton = { label: 'Label' }; | ||
const isStacked = false; | ||
render(createTestComponent({ isStacked, humanIllustration, primaryButton, secondaryButton })); | ||
|
||
expect(screen.getByText('test-heading')).toBeInTheDocument(); | ||
const buttons = screen.getAllByRole('button', { name: 'Label' }); | ||
expect(buttons).toHaveLength(2); | ||
const humanImage = screen.getByTestId('VOTING'); | ||
// eslint-disable-next-line testing-library/no-node-access -- testid for SVG is nearest accessible attribute and reliable to the illustration | ||
expect(humanImage.parentElement).toHaveClass('order-last'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import classNames from 'classnames'; | ||
import { Button } from '../../button'; | ||
import { IllustrationHuman, IllustrationObject } from '../../illustrations'; | ||
import type { IEmptyStateProps } from './emptyState.api'; | ||
|
||
export const EmptyState: React.FC<IEmptyStateProps> = ({ | ||
heading, | ||
description, | ||
primaryButton, | ||
secondaryButton, | ||
className, | ||
isStacked = true, | ||
objectIllustration, | ||
humanIllustration, | ||
}) => { | ||
const containerClassNames = classNames( | ||
'grid w-[320px] md:w-[640px]', | ||
{ 'grid-cols-1 justify-items-center p-6 md:p-12 gap-4 md:gap-6': isStacked }, | ||
{ | ||
'grid-cols-[auto_max-content] md:grid-cols-[auto_max-content] gap-4 p-4 md:px-6 md:py-5 items-center': | ||
!isStacked, | ||
}, | ||
className, | ||
); | ||
|
||
return ( | ||
<div className={containerClassNames}> | ||
{humanIllustration && ( | ||
<IllustrationHuman | ||
className={classNames({ | ||
'mb-4 h-auto !w-[295px] md:mb-6 md:!w-[400px]': isStacked, | ||
'align-self-center order-last !w-[80px] justify-self-end md:!w-[172px]': !isStacked, | ||
})} | ||
{...humanIllustration} | ||
/> | ||
)} | ||
{objectIllustration && ( | ||
<IllustrationObject | ||
className={classNames({ | ||
'h-auto w-[160px]': isStacked, | ||
'order-last h-auto w-[80px] justify-self-end rounded-full bg-neutral-50 md:w-[96px]': | ||
!isStacked, | ||
})} | ||
{...objectIllustration} | ||
/> | ||
)} | ||
|
||
<div | ||
className={classNames('h-full', { | ||
'flex w-full flex-col items-center': isStacked, | ||
'space-y-6': (isStacked && !!primaryButton) || !!secondaryButton, | ||
'space-y-4': (!isStacked && !!primaryButton) || !!secondaryButton, | ||
})} | ||
> | ||
<div | ||
className={classNames({ | ||
'flex flex-col items-center space-y-1 md:space-y-2': isStacked, | ||
'items-start space-y-0.5 md:space-y-1': !isStacked, | ||
})} | ||
> | ||
<p | ||
className={classNames('font-normal leading-tight text-neutral-800', { | ||
'text-xl md:text-2xl': isStacked, | ||
'text-base md:text-lg': !isStacked, | ||
})} | ||
> | ||
{heading} | ||
</p> | ||
<p | ||
className={classNames('font-normal leading-tight text-neutral-500', { | ||
'text-sm md:text-base': isStacked, | ||
'text-xs md:text-sm': !isStacked, | ||
})} | ||
> | ||
{description} | ||
</p> | ||
</div> | ||
<div | ||
className={classNames({ | ||
'border-w-full flex flex-col items-stretch space-x-0 space-y-3 md:flex-row md:justify-center md:space-x-4 md:space-y-0': | ||
isStacked, | ||
'flex flex-row flex-wrap gap-3': !isStacked, | ||
})} | ||
> | ||
{primaryButton && ( | ||
<Button | ||
{...primaryButton} | ||
size={isStacked ? 'lg' : 'sm'} | ||
responsiveSize={isStacked ? { md: 'lg' } : { md: 'md' }} | ||
variant="primary" | ||
> | ||
{primaryButton.label} | ||
</Button> | ||
)} | ||
{secondaryButton && ( | ||
<Button | ||
{...secondaryButton} | ||
size={isStacked ? 'lg' : 'sm'} | ||
responsiveSize={isStacked ? { md: 'lg' } : { md: 'md' }} | ||
variant="secondary" | ||
> | ||
{secondaryButton.label} | ||
</Button> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export { EmptyState } from './emptyState'; | ||
export type { | ||
IEmptyStateBaseProps, | ||
IEmptyStateButton, | ||
IEmptyStateHumanIllustrationProps, | ||
IEmptyStateObjectIllustrationProps, | ||
IEmptyStateProps, | ||
} from './emptyState.api'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './emptyState'; |