Skip to content

Commit

Permalink
feat(Empty State): add new bq-empty-state component
Browse files Browse the repository at this point in the history
  • Loading branch information
Cata1989 committed Dec 6, 2023
1 parent b6b3aa4 commit 013c28d
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 0 deletions.
23 changes: 23 additions & 0 deletions packages/beeq/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TButtonAppearance, TButtonSize, TButtonType, TButtonVariant } from "./c
import { TDialogFooterAppearance, TDialogSize } from "./components/dialog/bq-dialog.types";
import { TDividerOrientation, TDividerStrokeLinecap, TDividerTitleAlignment } from "./components/divider/bq-divider.types";
import { FloatingUIPlacement } from "./services/interfaces";
import { TEmptyStateSize } from "./components/empty-state/bq-empty-state.types";
import { TIconWeight } from "./components/icon/bq-icon.types";
import { TInputType, TInputValidation, TInputValue } from "./components/input/bq-input.types";
import { TNotificationType } from "./components/notification/bq-notification.types";
Expand All @@ -33,6 +34,7 @@ export { TButtonAppearance, TButtonSize, TButtonType, TButtonVariant } from "./c
export { TDialogFooterAppearance, TDialogSize } from "./components/dialog/bq-dialog.types";
export { TDividerOrientation, TDividerStrokeLinecap, TDividerTitleAlignment } from "./components/divider/bq-divider.types";
export { FloatingUIPlacement } from "./services/interfaces";
export { TEmptyStateSize } from "./components/empty-state/bq-empty-state.types";
export { TIconWeight } from "./components/icon/bq-icon.types";
export { TInputType, TInputValidation, TInputValue } from "./components/input/bq-input.types";
export { TNotificationType } from "./components/notification/bq-notification.types";
Expand Down Expand Up @@ -367,6 +369,12 @@ export namespace Components {
*/
"strategy"?: 'fixed' | 'absolute';
}
interface BqEmptyState {
/**
* The size of the empty state component
*/
"size": TEmptyStateSize;
}
/**
* Icons are simplified images that graphically explain the meaning of an object on the screen.
*/
Expand Down Expand Up @@ -1350,6 +1358,12 @@ declare global {
prototype: HTMLBqDropdownElement;
new (): HTMLBqDropdownElement;
};
interface HTMLBqEmptyStateElement extends Components.BqEmptyState, HTMLStencilElement {
}
var HTMLBqEmptyStateElement: {
prototype: HTMLBqEmptyStateElement;
new (): HTMLBqEmptyStateElement;
};
interface HTMLBqIconElementEventMap {
"svgLoaded": any;
}
Expand Down Expand Up @@ -1725,6 +1739,7 @@ declare global {
"bq-dialog": HTMLBqDialogElement;
"bq-divider": HTMLBqDividerElement;
"bq-dropdown": HTMLBqDropdownElement;
"bq-empty-state": HTMLBqEmptyStateElement;
"bq-icon": HTMLBqIconElement;
"bq-input": HTMLBqInputElement;
"bq-notification": HTMLBqNotificationElement;
Expand Down Expand Up @@ -2118,6 +2133,12 @@ declare namespace LocalJSX {
*/
"strategy"?: 'fixed' | 'absolute';
}
interface BqEmptyState {
/**
* The size of the empty state component
*/
"size"?: TEmptyStateSize;
}
/**
* Icons are simplified images that graphically explain the meaning of an object on the screen.
*/
Expand Down Expand Up @@ -2966,6 +2987,7 @@ declare namespace LocalJSX {
"bq-dialog": BqDialog;
"bq-divider": BqDivider;
"bq-dropdown": BqDropdown;
"bq-empty-state": BqEmptyState;
"bq-icon": BqIcon;
"bq-input": BqInput;
"bq-notification": BqNotification;
Expand Down Expand Up @@ -3008,6 +3030,7 @@ declare module "@stencil/core" {
"bq-dialog": LocalJSX.BqDialog & JSXBase.HTMLAttributes<HTMLBqDialogElement>;
"bq-divider": LocalJSX.BqDivider & JSXBase.HTMLAttributes<HTMLBqDividerElement>;
"bq-dropdown": LocalJSX.BqDropdown & JSXBase.HTMLAttributes<HTMLBqDropdownElement>;
"bq-empty-state": LocalJSX.BqEmptyState & JSXBase.HTMLAttributes<HTMLBqEmptyStateElement>;
/**
* Icons are simplified images that graphically explain the meaning of an object on the screen.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { newE2EPage } from '@stencil/core/testing';

describe('bq-empty-state', () => {
it('should render', async () => {
const page = await newE2EPage({
html: '<bq-empty-state></bq-empty-state>',
});
const element = await page.find('bq-empty-state');

expect(element).toHaveClass('hydrated');
});

it('should have shadow root', async () => {
const page = await newE2EPage({
html: '<bq-empty-state></bq-empty-state>',
});
const element = await page.find('bq-empty-state');

expect(element.shadowRoot).not.toBeNull();
});

it('should render a basic empty state', async () => {
const page = await newE2EPage({
html: `
<bq-empty-state>
Title
<span slot="body">You have a basic empty state</span>
</bq-empty-state>
`,
});

const element = await page.find('bq-empty-state >>> slot[name="body"]');

expect(element).not.toBeNull();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ArgTypes, Title, Subtitle } from '@storybook/addon-docs';

<div className="bq-doc__wrapper" data-theme="light">
<div className="bq-doc__container">
<Title>Empty state</Title>

An Empty State is a UI component that is displayed when a user interacts with an application or system and there
is no data or content available to display. Empty States are common in applications that have dynamic or changing data,
or when the user is in a state of no activity or interaction.

<Subtitle>Usage</Subtitle>

- Use clear and concise language to explain the context of the empty state, so that users can understand the reason why there is no data or content available.
- Use visually appealing and positive design elements, such as illustrations, icons, or images, to keep users engaged and reduce frustration.
- Consider the use of humor or lightheartedness, if appropriate, to maintain a positive and inviting experience for users.
- Provide a clear and concise call-to-action that helps users navigate to other parts of the application or system, where they can find the data or content they need.
- Test the empty state with real users to ensure that it is effective in communicating the message and providing the guidance needed.

<Subtitle>👍 When to use</Subtitle>

- When to display a helpful message or guidance to users when there is no data or content available.
- When to provide a clear and concise way to inform users that there is no data or content available, so that they can understand the context and meaning of the empty state.
- When to provide an inviting and positive visual experience, to maintain user engagement and reduce frustration.
- When to include a call-to-action (CTA) that helps users navigate to other parts of the application or system, where they can find the data or content they need.

<Title>Properties</Title>

<ArgTypes of="bq-empty-state" />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Args, Meta, StoryObj } from '@storybook/web-components';
import { html } from 'lit-html';

import mdx from './bq-empty-state.mdx';
import { EMPTY_STATE_SIZE } from '../bq-empty-state.types';

const meta: Meta = {
title: 'Components/Empty state',
component: 'bq-empty-state',
parameters: {
docs: {
page: mdx,
},
},
argTypes: {
size: { control: 'select', options: [...EMPTY_STATE_SIZE] },
},
args: {
size: 'medium',
},
};
export default meta;

type Story = StoryObj;

const Template = (args: Args) => html` <bq-empty-state size=${args.size}> Title </bq-empty-state> `;

const TemplateWithBody = (args: Args) => html`
<div class="flex flex-row gap-20">
<bq-empty-state size=${args.size}>
Title
<span slot="body"> Description </span>
</bq-empty-state>
<bq-empty-state size=${args.size}>
Title <span slot="body"> Description <a class="bq-link" href="https://example.com">Link</a> </span>
</bq-empty-state>
</div>
`;

const TemplateWithCTA = (args: Args) => html`
<div class="flex flex-row gap-20">
<bq-empty-state size=${args.size}>
Title <span slot="body"> Description <a class="bq-link" href="https://example.com">Link</a> </span>
<div class="flex gap-xs" slot="footer">
<bq-button appearance="primary" size="small"> Button </bq-button>
</div>
</bq-empty-state>
<bq-empty-state size=${args.size}>
Title <span slot="body"> Description <a class="bq-link" href="https://example.com">Link</a> </span>
<div class="flex gap-xs" slot="footer">
<bq-button size="small" variant="ghost"> Button </bq-button>
</div>
</bq-empty-state>
<bq-empty-state size=${args.size}>
Title <span slot="body"> Description <a class="bq-link" href="https://example.com">Link</a> </span>
<div class="flex gap-xs" slot="footer">
<bq-button size="small" variant="ghost"> Button </bq-button>
<bq-button appearance="primary" size="small"> Button </bq-button>
</div>
</bq-empty-state>
</div>
`;

export const Default: Story = {
render: Template,
args: {},
};

export const WithBody: Story = {
render: TemplateWithBody,
args: {},
};

export const WithCallToAction: Story = {
render: TemplateWithCTA,
args: {},
};
117 changes: 117 additions & 0 deletions packages/beeq/src/components/empty-state/bq-empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Component, Element, h, Host, Prop, Watch } from '@stencil/core';

import { EMPTY_STATE_SIZE, SIZE_TO_VALUE_MAP, TEmptyStateSize } from './bq-empty-state.types';
import { validatePropValue } from '../../shared/utils';

/**
* @part body - The container `<div>` that wraps the alert description content
* @part footer - The container `<div>` that wraps the alert footer content
* @part icon - The `<bq-icon>` element used to render a predefined icon size based on the empty state size (small, medium, large)
* @part title - The container `<div>` that wraps the empty state title content
* @part wrapper - The wrapper container `<div>` of the element inside the shadow DOM
*/

@Component({
tag: 'bq-empty-state',
styleUrl: './scss/bq-empty-state.scss',
shadow: true,
})
export class BqEmptyState {
// Own Properties
// ====================

// Reference to host HTML element
// ===================================

@Element() el!: HTMLBqEmptyStateElement;

// State() variables
// Inlined decorator, alphabetical order
// =======================================

// Public Property API
// ========================

/** The size of the empty state component */
@Prop({ reflect: true, mutable: true }) size: TEmptyStateSize = 'medium';

// Prop lifecycle events
// =======================

@Watch('size')
checkPropValues() {
validatePropValue(EMPTY_STATE_SIZE, 'medium', this.el, 'size');
}

// Events section
// Requires JSDocs for public API documentation
// ==============================================

// Component lifecycle events
// Ordered by their natural call order
// =====================================

componentWillLoad() {
this.checkPropValues();
}

// Listeners
// ==============

// Public methods API
// These methods are exposed on the host element.
// Always use two lines.
// Public Methods must be async.
// Requires JSDocs for public API documentation.
// ===============================================

// Local methods
// Internal business logic.
// These methods cannot be called from the host element.
// =======================================================

private get iconSize(): number {
return SIZE_TO_VALUE_MAP[this.size] || SIZE_TO_VALUE_MAP.medium;
}

// render() function
// Always the last one in the class.
// ===================================

render() {
return (
<Host>
<div class="bq-empty-state" part="wrapper">
<div class={{ [`bq-empty-state-icon-margin-bottom__${this.size}`]: true }}>
<slot name="icon">
<bq-icon size={this.iconSize} name="database" part="icon" exportparts="base,svg" />
</slot>
</div>
<div
class={{
'title-font font-bold leading-regular text-text-primary': true,
[`bq-empty-state-title-font-size__${this.size}`]: true,
[`bq-empty-state-title-margin-bottom__${this.size}`]: true,
}}
part="title"
>
<slot />
</div>
<div
class={{
'font-normal leading-regular': true,
[`bq-empty-state-body-font-size__${this.size}`]: true,
[`bq-empty-state-body-margin-bottom__${this.size}`]: true,
}}
part="body"
>
<slot name="body" />
</div>
<div class={{ 'flex items-start gap-xs': true }} part="footer">
<slot name="footer" />
</div>
</div>
</Host>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const EMPTY_STATE_SIZE = ['small', 'medium', 'large'] as const;
export type TEmptyStateSize = (typeof EMPTY_STATE_SIZE)[number];

export const SIZE_TO_VALUE_MAP: Record<TEmptyStateSize, number> = {
small: 40,
medium: 80,
large: 180,
};
41 changes: 41 additions & 0 deletions packages/beeq/src/components/empty-state/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# bq-empty-state



<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| -------- | --------- | ------------------------------------- | -------------------------------- | ---------- |
| `size` | `size` | The size of the empty state component | `"large" \| "medium" \| "small"` | `'medium'` |


## Shadow Parts

| Part | Description |
| ----------- | ------------------------------------------------------------------------------------------------------------------ |
| `"body"` | The container `<div>` that wraps the alert description content |
| `"footer"` | The container `<div>` that wraps the alert footer content |
| `"icon"` | The `<bq-icon>` element used to render a predefined icon size based on the empty state size (small, medium, large) |
| `"title"` | The container `<div>` that wraps the empty state title content |
| `"wrapper"` | The wrapper container `<div>` of the element inside the shadow DOM |


## Dependencies

### Depends on

- [bq-icon](../icon)

### Graph
```mermaid
graph TD;
bq-empty-state --> bq-icon
style bq-empty-state fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
Loading

0 comments on commit 013c28d

Please sign in to comment.