diff --git a/.changeset/shaggy-apes-own.md b/.changeset/shaggy-apes-own.md new file mode 100644 index 00000000000..a381ff15a08 --- /dev/null +++ b/.changeset/shaggy-apes-own.md @@ -0,0 +1,18 @@ +--- +"@hashicorp/design-system-components": minor +--- + +`ApplicationState`: + +- Spacing and alignment updates +- New `@align` (`left` (default), `center`) argument for aligning content +- Added new yielded `Media` child component + +`ApplicationState::Header`: + +- The header now supports an optional `@titleTag` argument that can override the default title element (`div`) + +`ApplicationState::Footer`: + +- The footer now yields `Button` and `Dropdown` components as well as `LinkStandalone` +- The visual separator has been removed to modernize the component’s visual look diff --git a/packages/components/package.json b/packages/components/package.json index 08268c67bc7..55cb70bfc5f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -140,6 +140,7 @@ "./components/hds/application-state/footer.js": "./dist/_app_/components/hds/application-state/footer.js", "./components/hds/application-state/header.js": "./dist/_app_/components/hds/application-state/header.js", "./components/hds/application-state/index.js": "./dist/_app_/components/hds/application-state/index.js", + "./components/hds/application-state/media.js": "./dist/_app_/components/hds/application-state/media.js", "./components/hds/badge-count/index.js": "./dist/_app_/components/hds/badge-count/index.js", "./components/hds/badge/index.js": "./dist/_app_/components/hds/badge/index.js", "./components/hds/breadcrumb/index.js": "./dist/_app_/components/hds/breadcrumb/index.js", diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts index 05c0d3f1ade..94917572e53 100644 --- a/packages/components/src/components.ts +++ b/packages/components/src/components.ts @@ -9,6 +9,10 @@ import HdsAlert from './components/hds/alert/index.ts'; import HdsAppFooter from './components/hds/app-footer/index.ts'; import HdsAppFrame from './components/hds/app-frame/index.ts'; import HdsApplicationState from './components/hds/application-state/index.ts'; +import HdsApplicationStateBody from './components/hds/application-state/body.ts'; +import HdsApplicationStateFooter from './components/hds/application-state/footer.ts'; +import HdsApplicationStateHeader from './components/hds/application-state/header.ts'; +import HdsApplicationStateMedia from './components/hds/application-state/media.ts'; import HdsBadge from './components/hds/badge/index.ts'; import HdsBadgeCount from './components/hds/badge-count/index.ts'; import HdsBreadcrumb from './components/hds/breadcrumb/index.ts'; @@ -102,6 +106,10 @@ export { HdsAppFooter, HdsAppFrame, HdsApplicationState, + HdsApplicationStateBody, + HdsApplicationStateFooter, + HdsApplicationStateHeader, + HdsApplicationStateMedia, HdsBadge, HdsBadgeCount, HdsBreadcrumb, diff --git a/packages/components/src/components/hds/application-state/footer.hbs b/packages/components/src/components/hds/application-state/footer.hbs index 6539299fe9d..e8c2e5c1fe9 100644 --- a/packages/components/src/components/hds/application-state/footer.hbs +++ b/packages/components/src/components/hds/application-state/footer.hbs @@ -3,6 +3,12 @@ SPDX-License-Identifier: MPL-2.0 }} -
- {{yield (hash LinkStandalone=(component "hds/link/standalone"))}} + \ No newline at end of file diff --git a/packages/components/src/components/hds/application-state/footer.ts b/packages/components/src/components/hds/application-state/footer.ts index 9dff9af89bc..0458faa994e 100644 --- a/packages/components/src/components/hds/application-state/footer.ts +++ b/packages/components/src/components/hds/application-state/footer.ts @@ -3,9 +3,11 @@ * SPDX-License-Identifier: MPL-2.0 */ -import Component from '@glimmer/component'; +import TemplateOnlyComponent from '@ember/component/template-only'; import type { ComponentLike } from '@glint/template'; import type { HdsLinkStandaloneSignature } from '../link/standalone'; +import type { HdsButtonSignature } from '../button'; +import type { HdsDropdownSignature } from 'src/components/hds/dropdown'; export interface HdsApplicationStateFooterSignature { Args: { @@ -14,6 +16,8 @@ export interface HdsApplicationStateFooterSignature { Blocks: { default?: [ { + Button?: ComponentLike; + Dropdown?: ComponentLike; LinkStandalone?: ComponentLike; }, ]; @@ -21,31 +25,7 @@ export interface HdsApplicationStateFooterSignature { Element: HTMLDivElement; } -export default class HdsApplicationStateFooterComponent extends Component { - /** - * Indicate if the footer should have a top border or not. - * - * @param hasDivider - * @type {boolean} - * @default false - */ - get hasDivider(): boolean { - return this.args.hasDivider ?? false; - } +const HdsApplicationStateFooterComponent = + TemplateOnlyComponent(); - /** - * Get the class names to apply to the component. - * @method classNames - * @return {string} The "class" attribute to apply to the component. - */ - get classNames(): string { - const classes = ['hds-application-state__footer']; - - // add a class based on the existence of @hasDivider argument - if (this.hasDivider) { - classes.push(`hds-application-state__footer--has-divider`); - } - - return classes.join(' '); - } -} +export default HdsApplicationStateFooterComponent; diff --git a/packages/components/src/components/hds/application-state/header.hbs b/packages/components/src/components/hds/application-state/header.hbs index 55e43735263..98a3140edb7 100644 --- a/packages/components/src/components/hds/application-state/header.hbs +++ b/packages/components/src/components/hds/application-state/header.hbs @@ -3,18 +3,24 @@ SPDX-License-Identifier: MPL-2.0 }}
+ {{#if @errorCode}} + + ERROR + {{@errorCode}} + + {{/if}} {{#if @icon}}
- +
{{/if}} - + {{@title}} - {{#if @errorCode}} - - Error - {{@errorCode}} - - {{/if}}
\ No newline at end of file diff --git a/packages/components/src/components/hds/application-state/header.ts b/packages/components/src/components/hds/application-state/header.ts index 24740afec16..6b084dbab65 100644 --- a/packages/components/src/components/hds/application-state/header.ts +++ b/packages/components/src/components/hds/application-state/header.ts @@ -3,20 +3,22 @@ * SPDX-License-Identifier: MPL-2.0 */ -import TemplateOnlyComponent from '@ember/component/template-only'; +import Component from '@glimmer/component'; import type { FlightIconSignature } from '@hashicorp/ember-flight-icons/components/flight-icon'; - +import type { HdsTextDisplaySignature } from '../text/display'; export interface HdsApplicationStateHeaderSignature { Args: { title?: string; + titleTag?: HdsTextDisplaySignature['Args']['tag']; errorCode?: string; icon?: FlightIconSignature['Args']['name']; }; Element: HTMLDivElement; } -const HdsApplicationStateHeaderComponent = - TemplateOnlyComponent(); - -export default HdsApplicationStateHeaderComponent; +export default class HdsApplicationStateHeaderComponent extends Component { + get titleTag() { + return this.args.titleTag ?? 'div'; + } +} diff --git a/packages/components/src/components/hds/application-state/index.hbs b/packages/components/src/components/hds/application-state/index.hbs index e1f53266d8a..1caae7a1905 100644 --- a/packages/components/src/components/hds/application-state/index.hbs +++ b/packages/components/src/components/hds/application-state/index.hbs @@ -2,9 +2,10 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 }} -
+
{{yield (hash + Media=(component "hds/application-state/media") Header=(component "hds/application-state/header") Body=(component "hds/application-state/body") Footer=(component "hds/application-state/footer") diff --git a/packages/components/src/components/hds/application-state/index.ts b/packages/components/src/components/hds/application-state/index.ts index 454c5fb7a3e..0147cd7b7b6 100644 --- a/packages/components/src/components/hds/application-state/index.ts +++ b/packages/components/src/components/hds/application-state/index.ts @@ -3,16 +3,26 @@ * SPDX-License-Identifier: MPL-2.0 */ -import TemplateOnlyComponent from '@ember/component/template-only'; +import Component from '@glimmer/component'; +import { assert } from '@ember/debug'; +import { HdsApplicationStateAlignValues } from './types.ts'; + import type { ComponentLike } from '@glint/template'; +import type { HdsApplicationStateAligns } from './types'; +import type { HdsApplicationStateMediaSignature } from './media'; import type { HdsApplicationStateHeaderSignature } from './header'; import type { HdsApplicationStateBodySignature } from './body'; import type { HdsApplicationStateFooterSignature } from './footer'; +export const ALIGNS: string[] = Object.values(HdsApplicationStateAlignValues); export interface HdsApplicationStateSignature { + Args: { + align?: HdsApplicationStateAligns; + }; Blocks: { default: [ { + Media?: ComponentLike; Header?: ComponentLike; Body?: ComponentLike; Footer?: ComponentLike; @@ -22,7 +32,28 @@ export interface HdsApplicationStateSignature { Element: HTMLDivElement; } -const HdsApplicationStateComponent = - TemplateOnlyComponent(); +export default class HdsApplicationStateComponent extends Component { + get align(): HdsApplicationStateAligns { + const validAlignValues: HdsApplicationStateAligns[] = Object.values( + HdsApplicationStateAlignValues + ); + + assert( + `@align for "Hds::ApplicationState" must be one of the following: ${validAlignValues.join( + ', ' + )}; received: ${this.args.align}`, + this.args.align == null || validAlignValues.includes(this.args.align) + ); + + return this.args.align ?? HdsApplicationStateAlignValues.Left; + } -export default HdsApplicationStateComponent; + get classNames(): string { + const classes = ['hds-application-state']; + + // add a class based on the @align argument + classes.push(`hds-application-state--align-${this.align}`); + + return classes.join(' '); + } +} diff --git a/packages/components/src/components/hds/application-state/media.hbs b/packages/components/src/components/hds/application-state/media.hbs new file mode 100644 index 00000000000..5ccc4f2b2e9 --- /dev/null +++ b/packages/components/src/components/hds/application-state/media.hbs @@ -0,0 +1,8 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +
+ {{yield}} +
\ No newline at end of file diff --git a/packages/components/src/components/hds/application-state/media.ts b/packages/components/src/components/hds/application-state/media.ts new file mode 100644 index 00000000000..a6b71b8b1e5 --- /dev/null +++ b/packages/components/src/components/hds/application-state/media.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +export interface HdsApplicationStateMediaSignature { + Blocks: { + default: []; + }; + Element: HTMLDivElement; +} + +const HdsApplicationStateMediaComponent = + TemplateOnlyComponent(); + +export default HdsApplicationStateMediaComponent; diff --git a/packages/components/src/components/hds/application-state/types.ts b/packages/components/src/components/hds/application-state/types.ts new file mode 100644 index 00000000000..df2c355ab72 --- /dev/null +++ b/packages/components/src/components/hds/application-state/types.ts @@ -0,0 +1,5 @@ +export enum HdsApplicationStateAlignValues { + Left = 'left', + Center = 'center', +} +export type HdsApplicationStateAligns = `${HdsApplicationStateAlignValues}`; diff --git a/packages/components/src/styles/components/application-state.scss b/packages/components/src/styles/components/application-state.scss index 891d2009819..98a6f169497 100644 --- a/packages/components/src/styles/components/application-state.scss +++ b/packages/components/src/styles/components/application-state.scss @@ -7,43 +7,84 @@ // APPLICATION-STATE COMPONENT // -$hds-application-state-padding: 12px 0; +$hds-application-state-content-max-width: 480px; .hds-application-state { - width: 19.5rem; - max-width: 100%; + display: flex; + flex-direction: column; + align-items: start; + width: fit-content; margin: 0 auto; // this will center the component in the parent container + + &.hds-application-state--align-center { + align-items: center; + text-align: center; + + .hds-application-state__header, + .hds-application-state__body, + .hds-application-state__footer { + width: auto; + } + } +} + +.hds-application-state__media { + margin-bottom: 32px; + + > * { + display: block; + max-width: 100%; + } } .hds-application-state__header { display: grid; + grid-template-areas: + "error error" + "icon title"; grid-template-columns: min-content 1fr; - align-items: start; - color: var(--token-color-foreground-faint); + align-items: center; + width: 100%; + max-width: $hds-application-state-content-max-width; + color: var(--token-color-foreground-strong); +} + +.hds-application-state__error-code { + grid-area: error; + margin-bottom: 4px; } .hds-application-state__icon { + grid-area: icon; margin-right: 8px; // instead of column gap padding-top: 4px; // this seems to align the icon along with line 21 } -.hds-application-state__title, -.hds-application-state__error-code { - grid-column-start: 2; +.hds-application-state__title { + grid-row: 2; + font-weight: var(--token-typography-font-weight-semibold); + + &:not(.hds-application-state__icon + &) { + grid-column: icon / title; + } } .hds-application-state__body { - padding: $hds-application-state-padding; - color: var(--token-color-foreground-faint); + width: 100%; + max-width: $hds-application-state-content-max-width; + margin: 12px 0 0; + color: var(--token-color-foreground-primary); } .hds-application-state__footer { display: flex; - gap: 8px; - justify-content: space-between; + gap: 16px; + width: 100%; + max-width: $hds-application-state-content-max-width; + margin-top: 24px; - &.hds-application-state__footer--has-divider { - padding: $hds-application-state-padding; - border-top: 1px solid var(--token-color-border-strong); + // forces the third child and on to be on the right side + > :nth-child(3) { + margin-left: auto; } } diff --git a/packages/components/src/template-registry.ts b/packages/components/src/template-registry.ts index 8217a8c0c1c..d1f49b5d044 100644 --- a/packages/components/src/template-registry.ts +++ b/packages/components/src/template-registry.ts @@ -36,6 +36,7 @@ import type HdsApplicationStateComponent from './components/hds/application-stat import type HdsApplicationStateBodyComponent from './components/hds/application-state/body'; import type HdsApplicationStateFooterComponent from './components/hds/application-state/footer'; import type HdsApplicationStateHeaderComponent from './components/hds/application-state/header'; +import type HdsApplicationStateMediaComponent from './components/hds/application-state/media'; import type HdsCardContainerComponent from './components/hds/card/container.ts'; import type HdsCopyButtonComponent from './components/hds/copy/button/index'; import type HdsCopySnippetComponent from './components/hds/copy/snippet'; @@ -243,6 +244,9 @@ export default interface HdsComponentsRegistry { 'Hds::ApplicationState::Footer': typeof HdsApplicationStateFooterComponent; 'hds/application-state/footer': typeof HdsApplicationStateFooterComponent; + 'Hds::ApplicationState::Media': typeof HdsApplicationStateMediaComponent; + 'hds/application-state/media': typeof HdsApplicationStateMediaComponent; + // Badge 'Hds::Badge': typeof HdsBadgeComponent; 'hds/badge': typeof HdsBadgeComponent; diff --git a/showcase/app/controllers/components/application-state.js b/showcase/app/controllers/components/application-state.js new file mode 100644 index 00000000000..fac5b02a3b2 --- /dev/null +++ b/showcase/app/controllers/components/application-state.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class ApplicationStateController extends Controller { + @tracked showHighlight = false; + + @action + toggleHighlight() { + this.showHighlight = !this.showHighlight; + } +} diff --git a/showcase/app/routes/components/application-state.js b/showcase/app/routes/components/application-state.js index b6597889d66..8edb7f1a3e3 100644 --- a/showcase/app/routes/components/application-state.js +++ b/showcase/app/routes/components/application-state.js @@ -5,4 +5,12 @@ import Route from '@ember/routing/route'; -export default class ComponentsApplicationStateRoute extends Route {} +import { ALIGNS } from '@hashicorp/design-system-components/components/hds/application-state'; + +export default class ComponentsApplicationStateRoute extends Route { + model() { + return { + ALIGNS, + }; + } +} diff --git a/showcase/app/styles/app.scss b/showcase/app/styles/app.scss index e3691e1b1fa..7502284e590 100644 --- a/showcase/app/styles/app.scss +++ b/showcase/app/styles/app.scss @@ -32,6 +32,7 @@ @import "./showcase-pages/alert"; @import "./showcase-pages/app-header"; @import "./showcase-pages/app-footer"; +@import "./showcase-pages/application-state"; @import "./showcase-pages/app-frame"; @import "./showcase-pages/badge"; @import "./showcase-pages/breadcrumb"; diff --git a/showcase/app/styles/showcase-pages/application-state.scss b/showcase/app/styles/showcase-pages/application-state.scss new file mode 100644 index 00000000000..f337920633b --- /dev/null +++ b/showcase/app/styles/showcase-pages/application-state.scss @@ -0,0 +1,45 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// APPLICATION-STATE + +body.application-state { + .shw-component-application-state-button-highlight { + margin: 24px 0; + } + + .shw-component-application-state-layout-highlight { + .shw-flex__item { + outline: 1px dashed #78909c; + } + + .hds-application-state { + // named color usage purely for layout testing + outline: 2px dotted magenta; + } + + .hds-application-state__media { + // named color usage purely for layout testing + outline: 1px solid green; + } + + .hds-application-state__header, + .hds-application-state__body, + .hds-application-state__footer { + // named color usage purely for layout testing + outline: 1px solid cyan; + } + } + + .shw-component-application-state-avatar { + width: 125px; + height: 125px; + border-radius: 100%; + } + + .shw-component-application-state-banner { + width: 680px; + } +} diff --git a/showcase/app/templates/components/application-state.hbs b/showcase/app/templates/components/application-state.hbs index 3ffb37c25e1..4f9696e1749 100644 --- a/showcase/app/templates/components/application-state.hbs +++ b/showcase/app/templates/components/application-state.hbs @@ -7,124 +7,535 @@ ApplicationState -
- - +
+ + Content + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + - + - - + - + - + - - + - - - - - - - + + + + - + - + + + + + + + + + + + + + + + - + - + - - + + + - + - + + - + + - + + + + + + - + - - - - - - + + + + + + + + + + - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Alignment + + + + + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + + + + + + + + {{/each}} + + + + + With media + + + + + {{#each @model.ALIGNS as |align|}} + + + + portrait of a cat wearing old-fashioned formal wear + + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + + + {{/each}} + {{#each @model.ALIGNS as |align|}} + + + + + + + + + + + + + {{/each}} + + + + + With wide media (banner-like) + + + + + {{#each @model.ALIGNS as |align|}} + + + + 3 cats wearing old-fashioned formal wear + + + + + + + + + + + {{/each}} + + + + + Responsiveness + + + + + {{#let + (array + (hash value="100%" label="Both media and content smaller than parent container") + (hash value="550px" label="Media larger and content smaller than parent container") + (hash value="320px" label="Both media and content larger than parent container") + ) + as |widths| + }} + {{#each widths as |width|}} + {{#each @model.ALIGNS as |align|}} + + + + + + 3 cats wearing old-fashioned formal wear + + + + + + + + + + + + {{/each}} + {{/each}} + {{/let}} + + + + + In a container + + + + + + + + + + + + + + + + <:head as |H|> + + Lorem + Ipsum + Dolor + + + <:body as |B|> + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/showcase/public/assets/images/cat-banner.png b/showcase/public/assets/images/cat-banner.png new file mode 100644 index 00000000000..79937c6e347 Binary files /dev/null and b/showcase/public/assets/images/cat-banner.png differ diff --git a/showcase/tests/integration/components/hds/application-state/footer-test.js b/showcase/tests/integration/components/hds/application-state/footer-test.js index 5d94da88872..606998af0f1 100644 --- a/showcase/tests/integration/components/hds/application-state/footer-test.js +++ b/showcase/tests/integration/components/hds/application-state/footer-test.js @@ -24,13 +24,6 @@ module( .dom('#test-application-state-footer') .hasClass('hds-application-state__footer'); }); - test('if @hasDivider is set to true, a class should be applied to render a visual divider', async function (assert) { - await render(hbs` - - `); - - assert.dom('.hds-application-state__footer--has-divider').exists(); - }); // CONTEXTUAL COMPONENTS diff --git a/showcase/tests/integration/components/hds/application-state/header-test.js b/showcase/tests/integration/components/hds/application-state/header-test.js index 7ff3d5c2645..ab1298de22b 100644 --- a/showcase/tests/integration/components/hds/application-state/header-test.js +++ b/showcase/tests/integration/components/hds/application-state/header-test.js @@ -38,5 +38,21 @@ module( assert.dom('.hds-application-state__error-code').exists(); }); + + test('it should render the title with a `div` tag if no `@titleTag` is provided', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.hds-application-state__title').hasTagName('div'); + }); + + test('it should render the title with the tag set for `@titleTag`', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.hds-application-state__title').hasTagName('h1'); + }); } ); diff --git a/showcase/tests/integration/components/hds/application-state/index-test.js b/showcase/tests/integration/components/hds/application-state/index-test.js index ed0dfbb335c..7ca37bfd0ff 100644 --- a/showcase/tests/integration/components/hds/application-state/index-test.js +++ b/showcase/tests/integration/components/hds/application-state/index-test.js @@ -5,7 +5,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'showcase/tests/helpers'; -import { render } from '@ember/test-helpers'; +import { render, resetOnerror, setupOnerror } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; module( @@ -13,6 +13,10 @@ module( function (hooks) { setupRenderingTest(hooks); + hooks.afterEach(() => { + resetOnerror(); + }); + test('it should render with a CSS class that matches the component name', async function (assert) { await render(hbs` @@ -22,5 +26,96 @@ module( assert.dom('#test-application-state').hasClass('hds-application-state'); }); + + test('it should have the correct alignment class when no alignment is provided', async function (assert) { + await render(hbs` + + template block text + + `); + + assert + .dom('#test-application-state') + .hasClass('hds-application-state--align-left'); + }); + + test('it should have the correct alignment class when alignment is set to "left"', async function (assert) { + await render(hbs` + + template block text + + `); + + assert + .dom('#test-application-state') + .hasClass('hds-application-state--align-left'); + }); + + test('it should have the correct alignment class when alignment is set to "center"', async function (assert) { + await render(hbs` + + template block text + + `); + + assert + .dom('#test-application-state') + .hasClass('hds-application-state--align-center'); + }); + + // CONTEXTUAL COMPONENTS + + test('it renders the contextual components', async function (assert) { + await render( + hbs` + ApplicationState Media + + ApplicationState Body + ApplicationState Footer + ` + ); + assert + .dom('.hds-application-state__media') + .hasText('ApplicationState Media'); + assert + .dom('.hds-application-state__header') + .hasText('ApplicationState Title'); + assert + .dom('.hds-application-state__body') + .hasText('ApplicationState Body'); + assert + .dom('.hds-application-state__footer') + .hasText('ApplicationState Footer'); + }); + test('it does not render the contextual components if not provided', async function (assert) { + await render(hbs``); + assert.dom('.hds-application-date__media').doesNotExist(); + assert.dom('.hds-application-date__header').doesNotExist(); + assert.dom('.hds-application-date__body').doesNotExist(); + assert.dom('.hds-application-date__footer').doesNotExist(); + }); + + // ASSERTIONS + + test('it should throw an assertion if an incorrect value for @alignment provided', async function (assert) { + const errorMessage = + '@align for "Hds::ApplicationState" must be one of the following: left, center; received: test'; + + assert.expect(2); + + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + + await render(hbs` + + template block text + + `); + + assert.throws(function () { + throw new Error(errorMessage); + }); + }); } ); diff --git a/showcase/tests/integration/components/hds/application-state/media-test.js b/showcase/tests/integration/components/hds/application-state/media-test.js new file mode 100644 index 00000000000..2128b5ed7ea --- /dev/null +++ b/showcase/tests/integration/components/hds/application-state/media-test.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'showcase/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module( + 'Integration | Component | hds/application-state/media', + function (hooks) { + setupRenderingTest(hooks); + + test('it should render with a CSS class that matches the component name', async function (assert) { + await render( + hbs`` + ); + + assert + .dom('#test-application-state-media') + .hasClass('hds-application-state__media'); + }); + + test('it should render the yielded content when used in block form', async function (assert) { + await render( + hbs` +
test
+
` + ); + assert.dom('#test-application-state-media > pre').exists(); + assert.dom('#test-application-state-media > pre').hasText('test'); + }); + } +); diff --git a/website/docs/components/application-state/index.md b/website/docs/components/application-state/index.md index 093af1feb1b..b04e1eab191 100644 --- a/website/docs/components/application-state/index.md +++ b/website/docs/components/application-state/index.md @@ -11,6 +11,7 @@ navigation: ---
+ @include "partials/guidelines/overview.md" @include "partials/guidelines/guidelines.md"
diff --git a/website/docs/components/application-state/partials/code/component-api.md b/website/docs/components/application-state/partials/code/component-api.md index f968d4af3c4..223c2193d04 100644 --- a/website/docs/components/application-state/partials/code/component-api.md +++ b/website/docs/components/application-state/partials/code/component-api.md @@ -3,6 +3,9 @@ ### ApplicationState + + `ApplicationState::Media` yielded as contextual component (see below). + `ApplicationState::Header` yielded as contextual component (see below). @@ -19,6 +22,19 @@ ### Contextual components +#### [A].Media + +The `ApplicationState::Media` component, yielded as contextual component. + + + + Elements passed as children are yielded as inner content of the "media" block. + + + This component supports use of [`...attributes`](https://guides.emberjs.com/release/in-depth-topics/patterns-for-components/#toc_attribute-ordering). + + + #### [A].Header The `ApplicationState::Header` component, yielded as contextual component. @@ -30,7 +46,12 @@ The `ApplicationState::Header` component, yielded as contextual component. Adds a leading icon to the title. Accepts any [icon](/icons/library) name. - + + The text of the title + + + This component supports use of [`...attributes`](https://guides.emberjs.com/release/in-depth-topics/patterns-for-components/#toc_attribute-ordering). + #### [A].Body @@ -39,10 +60,13 @@ The `ApplicationState::Body` component, yielded as contextual component. - Supports block invocation for custom content (see [Block Content](https://guides.emberjs.com/release/components/block-content/) in Ember docs). + Elements passed as children are yielded as inner content of the "body" block. - Note: use `@text` for an inline invocation only. This component does not support `@text` on the component invocation if it is used as a block. + Note: use `@text` to pass directly text to the "body", with a predefined style. This component does not support `@text` on the component invocation if it is used with yielded content. + + + This component supports use of [`...attributes`](https://guides.emberjs.com/release/in-depth-topics/patterns-for-components/#toc_attribute-ordering). @@ -51,10 +75,16 @@ The `ApplicationState::Body` component, yielded as contextual component. The `ApplicationState::Footer` component, yielded as contextual component. - - Indicates if there should be a visible divider above the footer. + + The `Button` component, yielded as contextual component inside the `"footer"` block of the ApplicationState. It exposes the same API as the [`Button` component](/components/button). + + + The `Dropdown` component, yielded as contextual component inside the `"footer"` block of the ApplicationState. It exposes the same API as the [`Dropdown` component](/components/dropdown). The `Link::Standalone` component, yielded as contextual component inside the `"footer"` block of the ApplicationState. It exposes the same API as the [`Link::Standalone` component](/components/link/standalone). + + This component supports use of [`...attributes`](https://guides.emberjs.com/release/in-depth-topics/patterns-for-components/#toc_attribute-ordering). + diff --git a/website/docs/components/application-state/partials/code/how-to-use.md b/website/docs/components/application-state/partials/code/how-to-use.md index 7699ffbcc81..266a968a6e7 100644 --- a/website/docs/components/application-state/partials/code/how-to-use.md +++ b/website/docs/components/application-state/partials/code/how-to-use.md @@ -33,37 +33,49 @@ This component intends to replace a few different simple error and empty/zero st
``` -#### Empty state with a footer link and divider +#### Empty state with yielded body block ```handlebars - - - + + + + + ``` -#### Empty state with yielded body block +#### Empty state with body text ```handlebars - - - + ``` -#### Empty state with body text +#### Empty state with center alignment + +```handlebars + + + + + + + +``` + +#### Empty state with media ```handlebars + portrait of a cat wearing coat and tie @@ -124,19 +136,37 @@ To indicate that the message is an error state, add `@errorCode` to the `[A].Hea ``` -#### Error state with a footer divider +#### Error state with center alignment + +```handlebars + + + + + + + + +``` + +#### Error state with media ```handlebars + portrait of a cat wearing coat and tie - + - + -``` \ No newline at end of file +``` diff --git a/website/docs/components/application-state/partials/guidelines/guidelines.md b/website/docs/components/application-state/partials/guidelines/guidelines.md index 9616f856119..f565b06ce5b 100644 --- a/website/docs/components/application-state/partials/guidelines/guidelines.md +++ b/website/docs/components/application-state/partials/guidelines/guidelines.md @@ -2,28 +2,117 @@ ### When to use -- When a user’s action does not yield any results due to incorrect or incomplete input. - When an application encounters an issue or error during its operation. +- When emphasis is needed on the creation of a new object within a null state. ### When not to use - When the absence of content is expected and does not require an explanation to the user. - When there is a clear and intuitive way to add or populate content. -## Icon +## Alignment -- The icon should align with the purpose of the content and effectively communicate the same message. +The Application State supports two alignment options: `left` (default) and `center`. The alignment affects text alignment, action placement/alignment in the footer, and media placement; however, it does not change the default page alignment. -## Actions +!!! Info -- In the footer, you can include up to two stand-alone links. -- We don’t recommend using buttons, as most actions will navigate the user away from this page. Learn more about [when to use a link vs. a button](https://helios.hashicorp.design/components/link/standalone#usage). -- For the standalone link, we recommend using the medium size. -- Use footer actions to redirect or guide users in solving errors/access issues with actionable steps. +By default, the Application State has horizontal auto margins applied to it, always centering it on the page or containing element. This can be overridden with CSS properties. -## Content +!!! -- The title should be short and provide a clear and concise message. -- Focus on relevant information and avoid unnecessary details. -- Provide a straightforward explanation of the problem or error. -- Include suggestions or guidance for how the user can resolve the issue, if possible. +### Left alignment + +![Left aligned application state](/assets/components/application-state/application-state-alignment-left.png) + +### Center alignment + +![Center aligned application state](/assets/components/application-state/application-state-alignment-center.png) + +## Media + +The media slot is used to add illustrations to increase the visual impact of the Application State. + +This provides additional visual prominence while also elevating the brand experience. If the illustration has a circular container, we recommend setting the `alignment` to `center`. + +![Empty state for Vault Secrets, guiding user to create new secrets or importing them](/assets/components/application-state/application-state-media-slot-spot-illustration-center-alignment.png) + + +## Header + +### Icon + +When set, the icon is displayed side by side with the title. + +![Showing an icon left of the title, with some body text below it.](/assets/components/application-state/application-state-icon-usage.png) + +This is commonly used when displaying an error state for application failures. The icon must always be accompanied by a title. + +### Title + +The title should be short and provide a clear and concise message. + +### Error code + +If enabled and available, an error code will be shown, providing additional information associated with the title. + +## Body + +Focus on relevant information and avoid unnecessary details. If there is an error, include suggestions or guidance for how the user can resolve the issue, if possible. If no objects are found (zero/empty state), provide a brief explanation on how creating this new object will benefit the user. + +The body allows for two types of content: `text` and `generic`. + +![Showing two different kind of body content types, one as text and another as generic yielded content](/assets/components/application-state/application-state-body-content-types.png) + + +## Footer +The Application State supports up to three actions, including [Dropdown](/components/dropdown), [Standalone Link](/components/link/standalone), and [Button](/components/button) components. Use footer actions to help users resolve errors or access issues with actionable steps. + +![A button set showing a dropdown, secondary button, and stand alone link](/assets/components/application-state/application-state-footer-action-types.png) + +### Using buttons or links + +Buttons, along with links, are the most common actions used in the footer. Buttons are more often used in empty state contexts because they provide high visibility for the primary action on the page. Links are more common in error state contexts as a means to provide a solution to the error they encountered. + +Read more about when to use [Buttons](/components/button) and [Links](/components/link/standalone). + +!!! Dont + +When there is an empty state that occupies the majority of the page, do not display two similar actions in different areas of the UI. In this example, there is a primary button in the Page Header and in the Application State. + +![Showing an empty state with a primary button and a page header with a primary button](/assets/components/application-state/application-state-empty-state-dont-duplicate-buttons.png) + +!!! + +!!! Do + +Instead, use the Application State as the only means of drawing attention to the primary action. + +![Showing an empty state with a primary button with a page header with out a primay button](/assets/components/application-state/application-state-empty-state-do-keep-one-primary-cta.png) + +!!! + +### Using dropdowns + +Dropdowns can be used as actions in the footer in rare cases. Limit dropdowns to one per Application State. + +![Showing an empty state with a primary button with a page header with out one](/assets/components/application-state/application-state-dropdown-actions.png) + +## Width constraints + +The Application State’s content has a max width of 480 pixels. This is done for better readability, ensuring that the max character count is close to 70 characters per line. + +## Examples + +Here are some common use cases for the Application State, however, it is not limited to just these two examples. + +### Error state + +Error states are used when the application encounters an issue or error during its operation. It shows the associated error code, icon, messages, and actions to help users find a solution. + +![Showing an example of an error state with a 404 error code and two links](/assets/components/application-state/application-state-error-state.png) + +### Empty state + +An empty state occurs when a user has yet to create an object. Illustrations are placed using the `media` slot to further elevate the experience and express the purpose of the object. + +![Showing an empty state with a primary and secondary button along with a stand alone link](/assets/components/application-state/application-state-empty-state.png) \ No newline at end of file diff --git a/website/docs/components/application-state/partials/guidelines/overview.md b/website/docs/components/application-state/partials/guidelines/overview.md new file mode 100644 index 00000000000..7b626210f62 --- /dev/null +++ b/website/docs/components/application-state/partials/guidelines/overview.md @@ -0,0 +1 @@ +The Application State is used to communicate status, errors, and zero/empty states. \ No newline at end of file diff --git a/website/docs/components/application-state/partials/specifications/anatomy.md b/website/docs/components/application-state/partials/specifications/anatomy.md index c3ade3e3021..7d5f95a38e6 100644 --- a/website/docs/components/application-state/partials/specifications/anatomy.md +++ b/website/docs/components/application-state/partials/specifications/anatomy.md @@ -4,10 +4,10 @@ | Element | Usage | |------------------|-------------------------------------------------| +| Media | Optional. | | Title | Required. | | Icon | Optional, but recommended with errorCode. | | errorCode | Optional, but recommended when in errorState. | | Body | Required. | -| Separator | Optional, but recommended when separating actions from content. | | Footer | Optional. | -| Action | Required, if no footer; optional. | \ No newline at end of file +| Actions | Required, if no footer; optional. | \ No newline at end of file diff --git a/website/public/assets/components/application-state/application-state-alignment-center.png b/website/public/assets/components/application-state/application-state-alignment-center.png new file mode 100644 index 00000000000..4f554990b79 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-alignment-center.png differ diff --git a/website/public/assets/components/application-state/application-state-alignment-left.png b/website/public/assets/components/application-state/application-state-alignment-left.png new file mode 100644 index 00000000000..a371fdae81b Binary files /dev/null and b/website/public/assets/components/application-state/application-state-alignment-left.png differ diff --git a/website/public/assets/components/application-state/application-state-anatomy-page.png b/website/public/assets/components/application-state/application-state-anatomy-page.png index 71951df5c91..91a3db5d717 100644 Binary files a/website/public/assets/components/application-state/application-state-anatomy-page.png and b/website/public/assets/components/application-state/application-state-anatomy-page.png differ diff --git a/website/public/assets/components/application-state/application-state-body-content-types.png b/website/public/assets/components/application-state/application-state-body-content-types.png new file mode 100644 index 00000000000..4387e7db402 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-body-content-types.png differ diff --git a/website/public/assets/components/application-state/application-state-dropdown-actions.png b/website/public/assets/components/application-state/application-state-dropdown-actions.png new file mode 100644 index 00000000000..c8462b0fe7b Binary files /dev/null and b/website/public/assets/components/application-state/application-state-dropdown-actions.png differ diff --git a/website/public/assets/components/application-state/application-state-empty-state-do-keep-one-primary-cta.png b/website/public/assets/components/application-state/application-state-empty-state-do-keep-one-primary-cta.png new file mode 100644 index 00000000000..4ed668962b7 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-empty-state-do-keep-one-primary-cta.png differ diff --git a/website/public/assets/components/application-state/application-state-empty-state-dont-duplicate-buttons.png b/website/public/assets/components/application-state/application-state-empty-state-dont-duplicate-buttons.png new file mode 100644 index 00000000000..a74d45d172b Binary files /dev/null and b/website/public/assets/components/application-state/application-state-empty-state-dont-duplicate-buttons.png differ diff --git a/website/public/assets/components/application-state/application-state-empty-state.png b/website/public/assets/components/application-state/application-state-empty-state.png new file mode 100644 index 00000000000..6ef8028c2b8 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-empty-state.png differ diff --git a/website/public/assets/components/application-state/application-state-error-state.png b/website/public/assets/components/application-state/application-state-error-state.png new file mode 100644 index 00000000000..41126280e7c Binary files /dev/null and b/website/public/assets/components/application-state/application-state-error-state.png differ diff --git a/website/public/assets/components/application-state/application-state-footer-action-types.png b/website/public/assets/components/application-state/application-state-footer-action-types.png new file mode 100644 index 00000000000..aded3e74f31 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-footer-action-types.png differ diff --git a/website/public/assets/components/application-state/application-state-icon-usage.png b/website/public/assets/components/application-state/application-state-icon-usage.png new file mode 100644 index 00000000000..0a241f606e2 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-icon-usage.png differ diff --git a/website/public/assets/components/application-state/application-state-media-slot-icon-tile-center-alignment.png b/website/public/assets/components/application-state/application-state-media-slot-icon-tile-center-alignment.png new file mode 100644 index 00000000000..868aef1ad8c Binary files /dev/null and b/website/public/assets/components/application-state/application-state-media-slot-icon-tile-center-alignment.png differ diff --git a/website/public/assets/components/application-state/application-state-media-slot-spot-illustration-center-alignment.png b/website/public/assets/components/application-state/application-state-media-slot-spot-illustration-center-alignment.png new file mode 100644 index 00000000000..496bd2e9020 Binary files /dev/null and b/website/public/assets/components/application-state/application-state-media-slot-spot-illustration-center-alignment.png differ diff --git a/website/public/assets/components/application-state/application-state-media-slot.png b/website/public/assets/components/application-state/application-state-media-slot.png new file mode 100644 index 00000000000..9bba981be5f Binary files /dev/null and b/website/public/assets/components/application-state/application-state-media-slot.png differ