From 013c28d8a32b5b53d3680338f16a707ee439757f Mon Sep 17 00:00:00 2001 From: clungan Date: Wed, 6 Dec 2023 11:49:54 +0200 Subject: [PATCH] feat(Empty State): add new bq-empty-state component --- packages/beeq/src/components.d.ts | 23 ++++ .../__tests__/bq-empty-state.e2e.ts | 36 ++++++ .../empty-state/_storybook/bq-empty-state.mdx | 30 +++++ .../_storybook/bq-empty-state.stories.tsx | 77 ++++++++++++ .../components/empty-state/bq-empty-state.tsx | 117 ++++++++++++++++++ .../empty-state/bq-empty-state.types.ts | 8 ++ .../beeq/src/components/empty-state/readme.md | 41 ++++++ .../empty-state/scss/bq-empty-state.scss | 93 ++++++++++++++ .../scss/bq-empty-state.variables.scss | 22 ++++ packages/beeq/src/components/icon/readme.md | 2 + 10 files changed, 449 insertions(+) create mode 100644 packages/beeq/src/components/empty-state/__tests__/bq-empty-state.e2e.ts create mode 100644 packages/beeq/src/components/empty-state/_storybook/bq-empty-state.mdx create mode 100644 packages/beeq/src/components/empty-state/_storybook/bq-empty-state.stories.tsx create mode 100644 packages/beeq/src/components/empty-state/bq-empty-state.tsx create mode 100644 packages/beeq/src/components/empty-state/bq-empty-state.types.ts create mode 100644 packages/beeq/src/components/empty-state/readme.md create mode 100644 packages/beeq/src/components/empty-state/scss/bq-empty-state.scss create mode 100644 packages/beeq/src/components/empty-state/scss/bq-empty-state.variables.scss diff --git a/packages/beeq/src/components.d.ts b/packages/beeq/src/components.d.ts index 71a18a7bd..fc4e0b4b3 100644 --- a/packages/beeq/src/components.d.ts +++ b/packages/beeq/src/components.d.ts @@ -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"; @@ -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"; @@ -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. */ @@ -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; } @@ -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; @@ -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. */ @@ -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; @@ -3008,6 +3030,7 @@ declare module "@stencil/core" { "bq-dialog": LocalJSX.BqDialog & JSXBase.HTMLAttributes; "bq-divider": LocalJSX.BqDivider & JSXBase.HTMLAttributes; "bq-dropdown": LocalJSX.BqDropdown & JSXBase.HTMLAttributes; + "bq-empty-state": LocalJSX.BqEmptyState & JSXBase.HTMLAttributes; /** * Icons are simplified images that graphically explain the meaning of an object on the screen. */ diff --git a/packages/beeq/src/components/empty-state/__tests__/bq-empty-state.e2e.ts b/packages/beeq/src/components/empty-state/__tests__/bq-empty-state.e2e.ts new file mode 100644 index 000000000..89c2a2744 --- /dev/null +++ b/packages/beeq/src/components/empty-state/__tests__/bq-empty-state.e2e.ts @@ -0,0 +1,36 @@ +import { newE2EPage } from '@stencil/core/testing'; + +describe('bq-empty-state', () => { + it('should render', async () => { + const page = await newE2EPage({ + html: '', + }); + const element = await page.find('bq-empty-state'); + + expect(element).toHaveClass('hydrated'); + }); + + it('should have shadow root', async () => { + const page = await newE2EPage({ + html: '', + }); + 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: ` + + Title + You have a basic empty state + + `, + }); + + const element = await page.find('bq-empty-state >>> slot[name="body"]'); + + expect(element).not.toBeNull(); + }); +}); diff --git a/packages/beeq/src/components/empty-state/_storybook/bq-empty-state.mdx b/packages/beeq/src/components/empty-state/_storybook/bq-empty-state.mdx new file mode 100644 index 000000000..03d28ed4b --- /dev/null +++ b/packages/beeq/src/components/empty-state/_storybook/bq-empty-state.mdx @@ -0,0 +1,30 @@ +import { ArgTypes, Title, Subtitle } from '@storybook/addon-docs'; + +
+
+ Empty state + + 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. + + Usage + + - 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. + + 👍 When to use + + - 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. + + Properties + + +
+
diff --git a/packages/beeq/src/components/empty-state/_storybook/bq-empty-state.stories.tsx b/packages/beeq/src/components/empty-state/_storybook/bq-empty-state.stories.tsx new file mode 100644 index 000000000..1434d0953 --- /dev/null +++ b/packages/beeq/src/components/empty-state/_storybook/bq-empty-state.stories.tsx @@ -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` Title `; + +const TemplateWithBody = (args: Args) => html` +
+ + Title + Description + + + Title Description Link + +
+`; + +const TemplateWithCTA = (args: Args) => html` +
+ + Title Description Link +
+ Button +
+
+ + Title Description Link +
+ Button +
+
+ + Title Description Link +
+ Button + Button +
+
+
+`; + +export const Default: Story = { + render: Template, + args: {}, +}; + +export const WithBody: Story = { + render: TemplateWithBody, + args: {}, +}; + +export const WithCallToAction: Story = { + render: TemplateWithCTA, + args: {}, +}; diff --git a/packages/beeq/src/components/empty-state/bq-empty-state.tsx b/packages/beeq/src/components/empty-state/bq-empty-state.tsx new file mode 100644 index 000000000..4851be202 --- /dev/null +++ b/packages/beeq/src/components/empty-state/bq-empty-state.tsx @@ -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 `
` that wraps the alert description content + * @part footer - The container `
` that wraps the alert footer content + * @part icon - The `` element used to render a predefined icon size based on the empty state size (small, medium, large) + * @part title - The container `
` that wraps the empty state title content + * @part wrapper - The wrapper container `
` 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 ( + +
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+
+ ); + } +} diff --git a/packages/beeq/src/components/empty-state/bq-empty-state.types.ts b/packages/beeq/src/components/empty-state/bq-empty-state.types.ts new file mode 100644 index 000000000..6a1871265 --- /dev/null +++ b/packages/beeq/src/components/empty-state/bq-empty-state.types.ts @@ -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 = { + small: 40, + medium: 80, + large: 180, +}; diff --git a/packages/beeq/src/components/empty-state/readme.md b/packages/beeq/src/components/empty-state/readme.md new file mode 100644 index 000000000..b775d4145 --- /dev/null +++ b/packages/beeq/src/components/empty-state/readme.md @@ -0,0 +1,41 @@ +# bq-empty-state + + + + + + +## 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 `
` that wraps the alert description content | +| `"footer"` | The container `
` that wraps the alert footer content | +| `"icon"` | The `` element used to render a predefined icon size based on the empty state size (small, medium, large) | +| `"title"` | The container `
` that wraps the empty state title content | +| `"wrapper"` | The wrapper container `
` 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/)* diff --git a/packages/beeq/src/components/empty-state/scss/bq-empty-state.scss b/packages/beeq/src/components/empty-state/scss/bq-empty-state.scss new file mode 100644 index 000000000..d9b702b4a --- /dev/null +++ b/packages/beeq/src/components/empty-state/scss/bq-empty-state.scss @@ -0,0 +1,93 @@ +/* -------------------------------------------------------------------------- */ +/* Empty state styles */ +/* -------------------------------------------------------------------------- */ + +@import './bq-empty-state.variables'; + +:host { + @apply relative inline-block; +} + +.bq-empty-state { + @apply flex flex-col items-center; +} + +/** + * Set the font size for title based on the size value selected + */ + +.bq-empty-state-title-font-size__small { + @apply text-[length:var(--bq-empty-state--title-font-size-small)]; +} + +.bq-empty-state-title-font-size__medium { + @apply text-[length:var(--bq-empty-state--title-font-size-medium)]; +} + +.bq-empty-state-title-font-size__large { + @apply text-[length:var(--bq-empty-state--title-font-size-large)]; +} + +/** + * Set the font size for body based on the size value selected + */ + +.bq-empty-state-body-font-size__small { + @apply text-[length:var(--bq-empty-state--body-font-size-small)]; +} + +.bq-empty-state-body-font-size__medium { + @apply text-[length:var(--bq-empty-state--body-font-size-medium)]; +} + +.bq-empty-state-body-font-size__large { + @apply text-[length:var(--bq-empty-state--body-font-size-large)]; +} + +/** + * Set the margin bottom of the icon based on the size value selected + */ + +.bq-empty-state-icon-margin-bottom__small { + @apply mb-m; +} + +.bq-empty-state-icon-margin-bottom__medium { + @apply mb-xl; +} + +.bq-empty-state-icon-margin-bottom__large { + @apply mb-12; +} + +/** + * Set the margin bottom of the title based on the size value selected + */ + +.bq-empty-state-title-margin-bottom__small { + @apply mb-s; +} + +.bq-empty-state-title-margin-bottom__medium { + @apply mb-m; +} + +.bq-empty-state-title-margin-bottom__large { + @apply mb-l; +} + +/** + * Set the margin bottom of the body based on the size value selected + */ + +.bq-empty-state-body-margin-bottom__small { + @apply mb-m; +} + +.bq-empty-state-body-margin-bottom__medium { + @apply mb-l; +} + +.bq-empty-state-body-margin-bottom__large { + @apply mb-xl; +} diff --git a/packages/beeq/src/components/empty-state/scss/bq-empty-state.variables.scss b/packages/beeq/src/components/empty-state/scss/bq-empty-state.variables.scss new file mode 100644 index 000000000..68d7d6af3 --- /dev/null +++ b/packages/beeq/src/components/empty-state/scss/bq-empty-state.variables.scss @@ -0,0 +1,22 @@ +/* -------------------------------------------------------------------------- */ +/* Empty state custom properties */ +/* -------------------------------------------------------------------------- */ + +:host { + /** + * @prop --bq-empty-state--title-font-size-large: The font size of the title in the empty state when it is in the "small" size + * @prop --bq-empty-state--title-font-size-large: The font size of the title in the empty state when it is in the "medium" size + * @prop --bq-empty-state--title-font-size-large: The font size of the title in the empty state when it is in the "large" size + + * @prop --bq-empty-state--body-font-size-large: The font size of the body in the empty state when it is in the "small" size + * @prop --bq-empty-state--body-font-size-large: The font size of the body in the empty state when it is in the "medium" size + * @prop --bq-empty-state--body-font-size-large: The font size of the body in the empty state when it is in the "large" size + */ + --bq-empty-state--title-font-size-small: theme('fontSize.m'); + --bq-empty-state--title-font-size-medium: theme('fontSize.l'); + --bq-empty-state--title-font-size-large: theme('fontSize.xxl2'); + + --bq-empty-state--body-font-size-small: theme('fontSize.s'); + --bq-empty-state--body-font-size-medium: theme('fontSize.m'); + --bq-empty-state--body-font-size-large: theme('fontSize.l'); +} diff --git a/packages/beeq/src/components/icon/readme.md b/packages/beeq/src/components/icon/readme.md index b98b8b0e2..da7d68999 100644 --- a/packages/beeq/src/components/icon/readme.md +++ b/packages/beeq/src/components/icon/readme.md @@ -39,6 +39,7 @@ Icons are simplified images that graphically explain the meaning of an object on - [bq-alert](../alert) - [bq-button](../button) - [bq-dialog](../dialog) + - [bq-empty-state](../empty-state) - [bq-input](../input) - [bq-notification](../notification) - [bq-select](../select) @@ -51,6 +52,7 @@ graph TD; bq-alert --> bq-icon bq-button --> bq-icon bq-dialog --> bq-icon + bq-empty-state --> bq-icon bq-input --> bq-icon bq-notification --> bq-icon bq-select --> bq-icon