-
Notifications
You must be signed in to change notification settings - Fork 322
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
ee2970b
commit a67ccbc
Showing
10 changed files
with
297 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
.emptyState { | ||
.image { | ||
pointer-events: none; | ||
user-select: none; | ||
} | ||
} |
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,74 @@ | ||
import React, { forwardRef, useRef } from "react"; | ||
import cx from "classnames"; | ||
import { useMergeRefs } from "../../hooks"; | ||
import VibeComponentProps from "../../types/VibeComponentProps"; | ||
import VibeComponent from "../../types/VibeComponent"; | ||
import { getTestId } from "../../tests/test-ids-utils"; | ||
import { ComponentDefaultTestId } from "../../tests/constants"; | ||
import styles from "./EmptyState.module.scss"; | ||
import Flex from "../Flex/Flex"; | ||
import Heading from "../Heading/Heading"; | ||
import Text from "../Text/Text"; | ||
import Button from "../Button/Button"; | ||
|
||
export interface EmptyStateProps extends VibeComponentProps { | ||
imgSrc: string; | ||
title: string; | ||
body: string; | ||
onPrimaryActionClick?: () => void; | ||
primaryActionLabel?: string; | ||
onSecondaryActionClick?: () => void; | ||
secondaryActionLabel?: string; | ||
imgClassName?: string; | ||
} | ||
|
||
const EmptyState: VibeComponent<EmptyStateProps, HTMLElement> = forwardRef( | ||
( | ||
{ | ||
imgSrc, | ||
title, | ||
body, | ||
onPrimaryActionClick, | ||
primaryActionLabel, | ||
onSecondaryActionClick, | ||
secondaryActionLabel, | ||
className, | ||
imgClassName, | ||
id, | ||
"data-testid": dataTestId | ||
}, | ||
ref | ||
) => { | ||
const componentRef = useRef(null); | ||
const mergedRef = useMergeRefs({ refs: [ref, componentRef] }); | ||
|
||
return ( | ||
<Flex | ||
ref={mergedRef} | ||
className={cx(styles.emptyState, className)} | ||
direction={Flex.directions.COLUMN} | ||
align={Flex.align.CENTER} | ||
justify={Flex.justify.CENTER} | ||
gap={Flex.gaps.LARGE} | ||
id={id} | ||
data-testid={dataTestId || getTestId(ComponentDefaultTestId.EMPTY_STATE, id)} | ||
> | ||
<img src={imgSrc} alt={title} className={cx(styles.image, imgClassName)} /> | ||
<Flex direction={Flex.directions.COLUMN} gap={Flex.gaps.SMALL} align={Flex.align.CENTER}> | ||
<Heading type={Heading.types.H2}>{title}</Heading> | ||
<Text type={Text.types.TEXT1}>{body}</Text> | ||
</Flex> | ||
<Flex gap={Flex.gaps.SMALL} align={Flex.align.CENTER}> | ||
{secondaryActionLabel && ( | ||
<Button kind={Button.kinds.TERTIARY} onClick={onSecondaryActionClick}> | ||
{secondaryActionLabel} | ||
</Button> | ||
)} | ||
{primaryActionLabel && <Button onClick={onPrimaryActionClick}>{primaryActionLabel}</Button>} | ||
</Flex> | ||
</Flex> | ||
); | ||
} | ||
); | ||
|
||
export default EmptyState; |
54 changes: 54 additions & 0 deletions
54
src/components/EmptyState/__stories__/EmptyState.stories.mdx
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,54 @@ | ||
import EmptyState from "../EmptyState"; | ||
import { ArgsTable, Story, Canvas, Meta } from "@storybook/addon-docs"; | ||
import { createStoryMetaSettingsDecorator } from "../../../storybook"; | ||
import { createComponentTemplate } from "vibe-storybook-components"; | ||
import emptyStateImage from "./assets/empty_state_img.svg"; | ||
|
||
export const metaSettings = createStoryMetaSettingsDecorator({ | ||
component: EmptyState, | ||
actionPropsArray: ["onPrimaryActionClick", "onSecondaryActionClick"] | ||
}); | ||
|
||
<Meta | ||
title="Feedback/EmptyState" | ||
component={metaSettings.component} | ||
argTypes={metaSettings.argTypes} | ||
decorators={metaSettings.decorators} | ||
/> | ||
|
||
<!--- Component template --> | ||
|
||
export const emptyStateTemplate = createComponentTemplate(EmptyState); | ||
export const emptyStateTemplateDefaults = { | ||
imgSrc: emptyStateImage, | ||
title: "This is a title", | ||
body: "This is a body, more detailed description", | ||
primaryActionLabel: "Do something", | ||
secondaryActionLabel: "Learn more" | ||
}; | ||
|
||
<!--- Component documentation --> | ||
|
||
# EmptyState | ||
|
||
- [Overview](#overview) | ||
- [Props](#props) | ||
- [Feedback](#feedback) | ||
|
||
## Overview | ||
|
||
Empty states are used when a list, table, or chart has no items or data to show. | ||
|
||
By providing constructive guidance about next steps, they enlighten users about what they would see if they had data. | ||
|
||
An empty state ensures a smooth experience, even when things do not work as expected. | ||
|
||
<Canvas> | ||
<Story name="Overview" args={emptyStateTemplateDefaults}> | ||
{emptyStateTemplate.bind({})} | ||
</Story> | ||
</Canvas> | ||
|
||
## Props | ||
|
||
<ArgsTable story="Overview" /> |
1 change: 1 addition & 0 deletions
1
src/components/EmptyState/__stories__/assets/empty_state_img.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions
48
src/components/EmptyState/__tests__/__snapshots__/emptyState-snapshot-tests.jest.js.snap
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,48 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`EmptyState should render correctly with required props 1`] = ` | ||
<div | ||
className="container directionColumn justifyCenter alignCenter emptyState" | ||
data-testid="empty-state" | ||
style={ | ||
Object { | ||
"gap": "24px", | ||
} | ||
} | ||
> | ||
<img | ||
alt="This is title" | ||
className="image" | ||
src="someImg" | ||
/> | ||
<div | ||
className="container directionColumn justifyStart alignCenter" | ||
style={ | ||
Object { | ||
"gap": "8px", | ||
} | ||
} | ||
> | ||
<h2 | ||
className="typography primary start singleLineEllipsis heading h2Normal" | ||
data-testid="text" | ||
> | ||
This is title | ||
</h2> | ||
<div | ||
className="typography primary start singleLineEllipsis text text1Normal" | ||
data-testid="text" | ||
> | ||
This is body | ||
</div> | ||
</div> | ||
<div | ||
className="container directionRow justifyStart alignCenter" | ||
style={ | ||
Object { | ||
"gap": "8px", | ||
} | ||
} | ||
/> | ||
</div> | ||
`; |
16 changes: 16 additions & 0 deletions
16
src/components/EmptyState/__tests__/emptyState-snapshot-tests.jest.js
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,16 @@ | ||
import React from "react"; | ||
import renderer from "react-test-renderer"; | ||
import EmptyState from "../EmptyState"; | ||
|
||
const defaultProps = { | ||
imgSrc: "someImg", | ||
title: "This is title", | ||
body: "This is body" | ||
}; | ||
|
||
describe("EmptyState", () => { | ||
it("should render correctly with required props", () => { | ||
const tree = renderer.create(<EmptyState {...defaultProps} />).toJSON(); | ||
expect(tree).toMatchSnapshot(); | ||
}); | ||
}); |
95 changes: 95 additions & 0 deletions
95
src/components/EmptyState/__tests__/emptyState-tests.jest.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,95 @@ | ||
import React from "react"; | ||
import "@testing-library/jest-dom"; | ||
import { fireEvent, render } from "@testing-library/react"; | ||
import EmptyState, { EmptyStateProps } from "../EmptyState"; | ||
|
||
const defaultProps = { | ||
imgSrc: "someImg", | ||
title: "This is title", | ||
body: "This is body" | ||
}; | ||
|
||
const renderComponent = (props: Partial<EmptyStateProps> = {}) => { | ||
return render(<EmptyState {...defaultProps} {...props} />); | ||
}; | ||
|
||
describe("EmptyState", () => { | ||
describe("props sanity", () => { | ||
it("should render different title", () => { | ||
const { getByText } = renderComponent({ title: "different title" }); | ||
expect(getByText("different title")).toBeInTheDocument(); | ||
}); | ||
|
||
it("should render different body", () => { | ||
const { getByText } = renderComponent({ body: "different body" }); | ||
expect(getByText("different body")).toBeInTheDocument(); | ||
}); | ||
|
||
it("should render different image", () => { | ||
const { getByAltText, getByRole } = renderComponent({ imgSrc: "different image" }); | ||
expect(getByRole("img")).toHaveAttribute("src", "different image"); | ||
expect(getByAltText("This is title")).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe("actions", () => { | ||
describe("rendering", () => { | ||
it("should not render action buttons", () => { | ||
const { queryByRole } = renderComponent(); | ||
expect(queryByRole("button")).toBeFalsy(); | ||
}); | ||
|
||
it("should render primary button", () => { | ||
const { getByText, getAllByRole } = renderComponent({ | ||
primaryActionLabel: "primary", | ||
onPrimaryActionClick: jest.fn() | ||
}); | ||
expect(getAllByRole("button")).toHaveLength(1); | ||
expect(getByText("primary")).toBeInTheDocument(); | ||
}); | ||
|
||
it("should render primary and secondary buttons", () => { | ||
const { getByText, getAllByRole } = renderComponent({ | ||
primaryActionLabel: "primary", | ||
onPrimaryActionClick: jest.fn(), | ||
secondaryActionLabel: "secondary", | ||
onSecondaryActionClick: jest.fn() | ||
}); | ||
expect(getAllByRole("button")).toHaveLength(2); | ||
expect(getByText("primary")).toBeInTheDocument(); | ||
expect(getByText("secondary")).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe("functionality", () => { | ||
const primaryActionMock = jest.fn(); | ||
const secondaryActionMock = jest.fn(); | ||
const renderComponentWithActions = () => | ||
renderComponent({ | ||
primaryActionLabel: "primary", | ||
onPrimaryActionClick: primaryActionMock, | ||
secondaryActionLabel: "secondary", | ||
onSecondaryActionClick: secondaryActionMock | ||
}); | ||
|
||
afterEach(() => { | ||
primaryActionMock.mockClear(); | ||
secondaryActionMock.mockClear(); | ||
}); | ||
|
||
it("should call onPrimaryActionClick once", () => { | ||
const { getByText } = renderComponentWithActions(); | ||
fireEvent.click(getByText("primary")); | ||
expect(primaryActionMock).toHaveBeenCalledTimes(1); | ||
expect(secondaryActionMock).toHaveBeenCalledTimes(0); | ||
}); | ||
|
||
it("should call onSecondaryActionClick once", () => { | ||
const { getByText } = renderComponentWithActions(); | ||
fireEvent.click(getByText("secondary")); | ||
expect(secondaryActionMock).toHaveBeenCalledTimes(1); | ||
expect(primaryActionMock).toHaveBeenCalledTimes(0); | ||
}); | ||
}); | ||
}); | ||
}); |
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