Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Empty State): add new bq-empty-state component #694

Merged
merged 5 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: {},
};
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
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>
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
<div class="bq-empty-state" part="wrapper">
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
<div class={{ [`bq-empty-state-icon-margin-bottom__${this.size}`]: true }}>
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
<slot name="icon">
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
<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"
>
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
<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"
>
Cata1989 marked this conversation as resolved.
Show resolved Hide resolved
<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