diff --git a/.changeset/stale-teachers-flash.md b/.changeset/stale-teachers-flash.md new file mode 100644 index 00000000000..17b08e30189 --- /dev/null +++ b/.changeset/stale-teachers-flash.md @@ -0,0 +1,6 @@ +--- +"@hashicorp/design-system-components": minor +--- + +- Added `Alert` component +- Added `Toast` component diff --git a/packages/components/addon/components/hds/alert/index.hbs b/packages/components/addon/components/hds/alert/index.hbs new file mode 100644 index 00000000000..605bd504957 --- /dev/null +++ b/packages/components/addon/components/hds/alert/index.hbs @@ -0,0 +1,35 @@ + \ No newline at end of file diff --git a/packages/components/addon/components/hds/alert/index.js b/packages/components/addon/components/hds/alert/index.js new file mode 100644 index 00000000000..710d50827d0 --- /dev/null +++ b/packages/components/addon/components/hds/alert/index.js @@ -0,0 +1,128 @@ +import Component from '@glimmer/component'; +import { assert } from '@ember/debug'; + +export const TYPES = ['page', 'inline', 'compact']; +export const DEFAULT_COLOR = 'neutral'; +export const COLORS = [ + 'neutral', + 'highlight', + 'success', + 'warning', + 'critical', +]; +export const MAPPING_COLORS_TO_ICONS = { + neutral: 'info', + highlight: 'info', + success: 'check-circle', + warning: 'alert-triangle', + critical: 'alert-diamond', +}; + +export default class HdsAlertIndexComponent extends Component { + constructor() { + super(...arguments); + + assert( + `@type for "Hds::Alert" must be one of the following: ${TYPES.join( + ', ' + )}; received: ${this.args.type}`, + TYPES.includes(this.args.type) + ); + } + + /** + * @param color + * @type {enum} + * @default neutral + * @description Determines the color scheme for the alert. + */ + get color() { + let { color = DEFAULT_COLOR } = this.args; + + assert( + `@color for "Hds::Alert" must be one of the following: ${COLORS.join( + ', ' + )}; received: ${color}`, + COLORS.includes(color) + ); + + return color; + } + + /** + * @param icon + * @type {string} + * @default null + * @description The name of the icon to be used. + */ + get icon() { + let { icon } = this.args; + + // If `icon` isn't passed, use the pre-defined one from `color` + if (icon === undefined) { + if (this.args.type === 'compact') { + // for the "compact" type by default we use filled icons + return `${MAPPING_COLORS_TO_ICONS[this.color]}-fill`; + } else { + // for all the other types by default we use outlined icons + return MAPPING_COLORS_TO_ICONS[this.color]; + } + // If `icon` is set explicitly to false, user doesn't want any icon in the alert + } else if (icon === false) { + assert( + `@icon for "Hds::Alert" with @type "compact" is required`, + this.args.type !== 'compact' + ); + + return false; + } else { + // If a name for `icon` is passed, set FlightIcon to that name + return icon; + } + } + + /** + * @param onDismiss + * @type {function} + * @default () => {} + */ + get onDismiss() { + let { onDismiss } = this.args; + + if (typeof onDismiss === 'function') { + return onDismiss; + } else { + return false; + } + } + + /** + * @param iconSize + * @type {string} + * @description ensures that the correct icon size is used. Automatically calculated. + */ + get iconSize() { + if (this.args.type === 'compact') { + return '16'; + } else { + return '24'; + } + } + + /** + * Get the class names to apply to the component. + * @method Alert#classNames + * @return {string} The "class" attribute to apply to the component. + */ + get classNames() { + let classes = ['hds-alert']; + + // Add a class based on the @type argument + classes.push(`hds-alert--type-${this.args.type}`); + + // Add a class based on the @color argument + classes.push(`hds-alert--color-${this.color}`); + + return classes.join(' '); + } +} diff --git a/packages/components/addon/components/hds/toast/index.hbs b/packages/components/addon/components/hds/toast/index.hbs new file mode 100644 index 00000000000..875490696ef --- /dev/null +++ b/packages/components/addon/components/hds/toast/index.hbs @@ -0,0 +1,15 @@ + + {{yield + (hash Button=A.Button Link::Standalone=A.Link::Standalone LinkTo::Standalone=A.LinkTo::Standalone Generic=A.Generic) + }} + \ No newline at end of file diff --git a/packages/components/addon/components/hds/yield/index.hbs b/packages/components/addon/components/hds/yield/index.hbs new file mode 100644 index 00000000000..219b6826c6b --- /dev/null +++ b/packages/components/addon/components/hds/yield/index.hbs @@ -0,0 +1,3 @@ +
+ {{yield}} +
\ No newline at end of file diff --git a/packages/components/app/components/hds/alert/index.js b/packages/components/app/components/hds/alert/index.js new file mode 100644 index 00000000000..10ff64ba8eb --- /dev/null +++ b/packages/components/app/components/hds/alert/index.js @@ -0,0 +1 @@ +export { default } from '@hashicorp/design-system-components/components/hds/alert/index'; diff --git a/packages/components/app/components/hds/toast/index.js b/packages/components/app/components/hds/toast/index.js new file mode 100644 index 00000000000..80f9ea5d95d --- /dev/null +++ b/packages/components/app/components/hds/toast/index.js @@ -0,0 +1 @@ +export { default } from '@hashicorp/design-system-components/components/hds/toast/index'; diff --git a/packages/components/app/components/hds/yield/index.js b/packages/components/app/components/hds/yield/index.js new file mode 100644 index 00000000000..49a0647b226 --- /dev/null +++ b/packages/components/app/components/hds/yield/index.js @@ -0,0 +1 @@ +export { default } from '@hashicorp/design-system-components/components/hds/yield/index'; diff --git a/packages/components/app/styles/@hashicorp/design-system-components.scss b/packages/components/app/styles/@hashicorp/design-system-components.scss index d685ef95876..3acf12c7edf 100644 --- a/packages/components/app/styles/@hashicorp/design-system-components.scss +++ b/packages/components/app/styles/@hashicorp/design-system-components.scss @@ -5,6 +5,7 @@ @use "helpers/focus-ring"; @use "helpers/typography"; +@use "../components/alert"; @use "../components/badge"; @use "../components/badge-count"; @use "../components/breadcrumb"; @@ -15,6 +16,7 @@ @use "../components/icon-tile"; @use "../components/link/cta"; @use "../components/link/standalone"; +@use "../components/toast"; .sr-only { border: 0 !important; diff --git a/packages/components/app/styles/components/alert.scss b/packages/components/app/styles/components/alert.scss new file mode 100644 index 00000000000..63239043e8b --- /dev/null +++ b/packages/components/app/styles/components/alert.scss @@ -0,0 +1,261 @@ +// +// ALERT +// +// properties within each class are sorted alphabetically +// +// + +@use "../mixins/focus-ring" as *; + +.hds-alert { + align-items: flex-start; + display: flex; +} + +// ICON + +.hds-alert__icon { + flex: none; + height: 20px; + margin-right: 12px; + width: 20px; + + .hds-alert--type-compact & { + height: 14px; + width: 14px; + } +} + +// CONTENT (TEXT + ACTIONS + GENERIC) + +.hds-alert__content { + flex: 1 1 auto; +} + +// TEXT (TITLE & DESCRIPTION) + +.hds-alert__text { + color: var(--token-color-foreground-warning-on-surface); + display: flex; + flex-direction: column; + font-family: var(--token-typography-body-200-font-family); + font-size: var(--token-typography-body-200-font-size); + justify-content: center; + line-height: var(--token-typography-body-200-line-height); + + .hds-alert--type-compact & { + font-family: var(--token-typography-body-100-font-family); + font-size: var(--token-typography-body-100-font-size); + line-height: var(--token-typography-body-100-line-height); + } +} + +.hds-alert__title { + font-weight: var(--token-typography-font-weight-semibold); +} + +.hds-alert__description { + color: var(--token-color-foreground-primary); + font-weight: var(--token-typography-font-weight-regular); + + .hds-alert__title + & { + margin-top: 4px; + } + + // we add very basic styling for elements that may be injected via the "description" string + + strong { + font-weight: var(--token-typography-font-weight-semibold); + } + + code, pre { + font-family: var(--token-typography-font-stack-code); + } + + // Notice: in the future this may become a "Link::Inline" component (for now we declare the styles directly here) + a { + color: var(--token-color-foreground-action); + // At the moment the "focus" state is not well defined in design (the one that is in Figma does not work) so we just apply a simple color to the default outline + outline-color: var(--token-color-focus-action-external); + + &:hover { + color: var(--token-color-foreground-action-hover); + } + + &:active { + color: var(--token-color-foreground-action-active); + } + } +} + +// ACTIONS + +.hds-alert__actions { + align-items: center; + display: flex; + + > * { + margin-top: 16px; + } + + > * + * { + margin-left: 8px; + } +} + +// DISMISS + +.hds-alert__dismiss { + background-color: transparent; + border: none; + color: var(--token-color-foreground-faint); + cursor: pointer; + flex: none; + margin-left: 16px; + margin-top: 2px; // for alignment with the main icon and text + padding: 0; + + .hds-alert--type-compact & { + margin-top: 1px; + } + + &:hover, + &.is-hover { + &::before { // we re-use the pseudo-element created by the "focus-ring" mixin + background-color: rgba(#dedfe3, 0.4); + } + } + + // notice: this is used not only for the focus, but also to increase the clickable area + @include hds-focus-ring-with-pseudo-element($top: -4px, $right: -4px, $bottom: -4px, $left: -4px); + + &:active, + &.is-active { + color: var(--token-color-foreground-secondary); + &::before { + background-color: rgba(#dedfe3, 0.4); + border: 1px solid var(--token-color-border-strong); + } + } +} + + +// TYPES + +.hds-alert--type-page { + padding: 16px 48px; // by design +} + +.hds-alert--type-inline { + border-radius: 6px; + border-style: solid; + border-width: 1px; + padding: 16px; +} + +.hds-alert--type-compact { + .hds-alert__icon { + margin-right: 8px; + margin-top: 2px; // notice: the icon size for the "compact" type is 14px, not 20px, so this 2px extra are needed to reach the exact same height as the "description" text line-height + } + + // extra safety + .hds-alert__title { + display: none; + & + .hds-alert__description { + margin-top: 0; + } + } +} + +// COLORS (& TYPES) + +.hds-alert--color-neutral { + + &.hds-alert--type-page { + background-color: var(--token-color-surface-faint); + box-shadow: 0px 1px 0px 0px var(--token-color-palette-alpha-300); + } + + &.hds-alert--type-inline { + background-color: var(--token-color-surface-faint); + border-color: var(--token-color-border-strong); // notice: in the "neutral" color the "inline" has a slightly darker border color compared to the others to increase contrast (eg. could be used on a light gray background) + } + + // different color by design + .hds-alert__icon { + color: var(--token-color-foreground-faint); + } + + .hds-alert__title { + color: var(--token-color-foreground-primary); + } +} + +.hds-alert--color-highlight { + + &.hds-alert--type-page { + background-color: var(--token-color-surface-highlight); + box-shadow: 0px 1px 0px 0px var(--token-color-border-highlight); + } + + &.hds-alert--type-inline { + background-color: var(--token-color-surface-highlight); + border-color: var(--token-color-border-highlight); + } + + .hds-alert__icon, .hds-alert__title { + color: var(--token-color-foreground-highlight-on-surface); + } +} + +.hds-alert--color-success { + + &.hds-alert--type-page { + background-color: var(--token-color-surface-success); + box-shadow: 0px 1px 0px 0px var(--token-color-border-success); + } + + &.hds-alert--type-inline { + background-color: var(--token-color-surface-success); + border-color: var(--token-color-border-success); + } + + .hds-alert__icon, .hds-alert__title { + color: var(--token-color-foreground-success-on-surface); + } +} + +.hds-alert--color-warning { + + &.hds-alert--type-page { + background-color: var(--token-color-surface-warning); + box-shadow: 0px 1px 0px 0px var(--token-color-border-warning); + } + + &.hds-alert--type-inline { + background-color: var(--token-color-surface-warning); + border-color: var(--token-color-border-warning); + } + + .hds-alert__icon, .hds-alert__title { + color: var(--token-color-foreground-warning-on-surface); + } +} + +.hds-alert--color-critical { + + &.hds-alert--type-page { + background-color: var(--token-color-surface-critical); + box-shadow: 0px 1px 0px 0px var(--token-color-border-critical); + } + + &.hds-alert--type-inline { + background-color: var(--token-color-surface-critical); + border-color: var(--token-color-border-critical); + } + + .hds-alert__icon, .hds-alert__title { + color: var(--token-color-foreground-critical-on-surface); + } +} diff --git a/packages/components/app/styles/components/toast.scss b/packages/components/app/styles/components/toast.scss new file mode 100644 index 00000000000..499864f4220 --- /dev/null +++ b/packages/components/app/styles/components/toast.scss @@ -0,0 +1,16 @@ +// +// TOAST +// +// properties within each class are sorted alphabetically +// +// + +.hds-toast { + // we apply an elevation to the "alert/inline" element + box-shadow: var(--token-elevation-higher-box-shadow); + + // per design specs + min-width: min(360px, 80vw); + max-width: min(500px, 80vw); + width: fit-content; +} diff --git a/packages/components/tests/acceptance/percy-test.js b/packages/components/tests/acceptance/percy-test.js index 18abb6a409b..c1fe3fcbf2b 100644 --- a/packages/components/tests/acceptance/percy-test.js +++ b/packages/components/tests/acceptance/percy-test.js @@ -26,6 +26,9 @@ module('Acceptance | Percy test', function (hooks) { await visit('/foundations/focus-ring'); await percySnapshot('FocusRing'); + await visit('/components/alert'); + await percySnapshot('Alert'); + await visit('/components/badge'); await percySnapshot('Badge'); @@ -53,6 +56,9 @@ module('Acceptance | Percy test', function (hooks) { await visit('/components/link-to/standalone'); await percySnapshot('LinkTo Standalone'); + await visit('/components/toast'); + await percySnapshot('Toast'); + assert.ok(true); }); }); diff --git a/packages/components/tests/dummy/app/components/dummy-placeholder/index.js b/packages/components/tests/dummy/app/components/dummy-placeholder/index.js index 7562df21639..d9ab854d3f7 100644 --- a/packages/components/tests/dummy/app/components/dummy-placeholder/index.js +++ b/packages/components/tests/dummy/app/components/dummy-placeholder/index.js @@ -1,4 +1,5 @@ import Component from '@glimmer/component'; +import { htmlSafe } from '@ember/template'; export default class DummyPlaceholderIndexComponent extends Component { /** @@ -51,6 +52,6 @@ export default class DummyPlaceholderIndexComponent extends Component { styles.push(`background: ${this.args.background}`); } - return styles.length > 0 ? styles.join('; ') : undefined; + return styles.length > 0 ? htmlSafe(styles.join('; ')) : undefined; } } diff --git a/packages/components/tests/dummy/app/controllers/components/alert.js b/packages/components/tests/dummy/app/controllers/components/alert.js new file mode 100644 index 00000000000..e7f213770a3 --- /dev/null +++ b/packages/components/tests/dummy/app/controllers/components/alert.js @@ -0,0 +1,9 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; + +export default class AlertController extends Controller { + // notice: this is used as "noop" function for the onDismiss callback of the Alert component + // I tried to use an helper, but without success (see https://hashicorp.slack.com/archives/C11JCBJTW/p1648751235987409) + @action + noop() {} +} diff --git a/packages/components/tests/dummy/app/controllers/components/toast.js b/packages/components/tests/dummy/app/controllers/components/toast.js new file mode 100644 index 00000000000..5013431a909 --- /dev/null +++ b/packages/components/tests/dummy/app/controllers/components/toast.js @@ -0,0 +1,9 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; + +export default class ToastController extends Controller { + // notice: this is used as "noop" function for the onDismiss callback of the Toast component + // I tried to use an helper, but without success (see https://hashicorp.slack.com/archives/C11JCBJTW/p1648751235987409) + @action + noop() {} +} diff --git a/packages/components/tests/dummy/app/router.js b/packages/components/tests/dummy/app/router.js index 6d37a9e2d23..6626fdc226d 100644 --- a/packages/components/tests/dummy/app/router.js +++ b/packages/components/tests/dummy/app/router.js @@ -15,10 +15,12 @@ Router.map(function () { this.route('focus-ring'); }); this.route('components', function () { + this.route('alert'); this.route('badge'); this.route('breadcrumb'); this.route('button'); this.route('card'); + this.route('dropdown'); this.route('icon-tile'); this.route('link', function () { this.route('standalone'); @@ -28,7 +30,7 @@ Router.map(function () { this.route('standalone'); this.route('cta'); }); - this.route('dropdown'); + this.route('toast'); }); this.route('content', function () { this.route('writing-guidelines'); diff --git a/packages/components/tests/dummy/app/routes/components/alert.js b/packages/components/tests/dummy/app/routes/components/alert.js new file mode 100644 index 00000000000..8347f3e4a50 --- /dev/null +++ b/packages/components/tests/dummy/app/routes/components/alert.js @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; + +import { + TYPES, + COLORS, +} from '@hashicorp/design-system-components/components/hds/alert'; + +export default class ComponentsAlertRoute extends Route { + model() { + return { + TYPES, + COLORS, + }; + } +} diff --git a/packages/components/tests/dummy/app/routes/components/toast.js b/packages/components/tests/dummy/app/routes/components/toast.js new file mode 100644 index 00000000000..96b19bd9190 --- /dev/null +++ b/packages/components/tests/dummy/app/routes/components/toast.js @@ -0,0 +1,12 @@ +import Route from '@ember/routing/route'; + +// the "Toast" is built on top of the "Alert" so it shares the same colors +import { COLORS } from '@hashicorp/design-system-components/components/hds/alert'; + +export default class ComponentsToastRoute extends Route { + model() { + return { + COLORS, + }; + } +} diff --git a/packages/components/tests/dummy/app/styles/app.scss b/packages/components/tests/dummy/app/styles/app.scss index 92bc037327e..673402f05d6 100644 --- a/packages/components/tests/dummy/app/styles/app.scss +++ b/packages/components/tests/dummy/app/styles/app.scss @@ -2,6 +2,7 @@ @import "./_typography"; +@import "./pages/db-alert"; @import "./pages/db-badge"; @import "./pages/db-breadcrumb"; @import "./pages/db-button"; @@ -14,6 +15,7 @@ @import "./pages/db-focus-ring"; @import "./pages/db-icon-tile"; @import "./pages/db-link"; +@import "./pages/db-toast"; @import "./pages/db-tokens"; @import "./pages/db-typography"; diff --git a/packages/components/tests/dummy/app/styles/pages/db-alert.scss b/packages/components/tests/dummy/app/styles/pages/db-alert.scss new file mode 100644 index 00000000000..dfa6e9c40ef --- /dev/null +++ b/packages/components/tests/dummy/app/styles/pages/db-alert.scss @@ -0,0 +1,28 @@ +// ALERT + +.dummy-alert-sample-grid { + align-items: start; + display: grid; + grid-gap: 1rem; + grid-template-columns: repeat(3, 1fr); + + .dummy-alert-sample-grid__title { + grid-column: 1 / 4; + } + + &.dummy-alert-sample-grid--wide-content { + grid-template-columns: repeat(2, 1fr); + } +} + +.dummy-alert-sample-item--type-compact { + outline: 1px dotted #eee; +} + +.dummy-alert-sample-custom-content-after-actions { + @include dummyFontFamily(); + @include dummyFontSize(0.8rem); + display: block; + font-style: italic; + margin-top: 1rem; +} \ No newline at end of file diff --git a/packages/components/tests/dummy/app/styles/pages/db-toast.scss b/packages/components/tests/dummy/app/styles/pages/db-toast.scss new file mode 100644 index 00000000000..a4bc1eec015 --- /dev/null +++ b/packages/components/tests/dummy/app/styles/pages/db-toast.scss @@ -0,0 +1,33 @@ +// TOAST + +.dummy-toast-base-sample { + display: flex; + gap: 16px; + align-items: center; + flex-wrap: wrap; +} + +.dummy-toast-sample-grid { + align-items: start; + display: grid; + grid-gap: 1rem; + grid-template-columns: repeat(4, 1fr); + + &.dummy-toast-sample-grid--wide-content { + grid-template-columns: repeat(2, 1fr); + } +} + +.dummy-toast-sample-custom-actions__actions { + display: flex; + gap: 16px; + align-items: center; +} + +.dummy-toast-sample-custom-actions__text { + @include dummyFontFamily(); + @include dummyFontSize(0.8rem); + display: block; + color: #999; + margin-top: 1rem; +} \ No newline at end of file diff --git a/packages/components/tests/dummy/app/templates/components/alert.hbs b/packages/components/tests/dummy/app/templates/components/alert.hbs new file mode 100644 index 00000000000..06dec8828c7 --- /dev/null +++ b/packages/components/tests/dummy/app/templates/components/alert.hbs @@ -0,0 +1,732 @@ +{{page-title "Alert component"}} + +

Alert

+ +
+

§ Overview

+ +

+ An Alert is an element intended for + system-generated messages. It is a live region with important, usually time-sensitive information. + The use of this alert component will cause immediate notifications for users with assistive technology. Since alerts + are not required to receive focus, it should not be required that the user close the alert.

+

For messages that are the result of a user's actions see the + Toast + component.) +

+ +

+ Typically it displays a brief, important message in a way that attracts the user's attention, without interrupting + the user's task. +

+ +

+ There are three types of alerts, each slightly different one from another. +

+ +

Page

+

It is rectangular (without a radius) and a visible border only at the bottom. Typically + only has adjacent whitespace to the bottom of it, meaning it's usually flush to the parent container. +

+

+ It can have an + icon + (optional), a + title + and/or + description + (required to have at least one of the two), some + actions + (optional) and a + dismiss/close + button (optional).

+ +

Inline

+

+ It has a border on all sides and a radius. Typically it has adjacent whitespace on all four sides. +

+

+ It can have an + icon + (optional), a + title + and/or + description + (required to have at least one of the two), some + actions + (optional) and a + dismiss/close + button (optional).

+

+ Notice: the "inline" alert is used to build the + Toast + component.

+ +

Compact

+

+ It's without border or internal padding, and so it has smaller proportions than the others. +

+

+ It only contains an + icon + and + description + (hence they are both required for this type of alert).

+

The default icon is also slightly different from the other alert types: it's filled instead + of outlined. +

+
+ +
+

§ Component API

+

Here is the API for the component:

+ +
+
type enum
+
+

+ Sets the type of alert. +

+ +

Acceptable values:

+
    +
  1. page
  2. +
  3. inline
  4. +
  5. compact
  6. +
+
+ +
color enum
+
+

+ Sets the color scheme for + background, + border, + title, and + description, which + cannot + be overridden. + color + results in a default + icon, which + can + be overridden. +

+

Acceptable values:

+
    +
  1. neutral
  2. +
  3. highlight
  4. +
  5. success
  6. +
  7. warning
  8. +
  9. critical
  10. +
+
+
icon string | false
+
+

Override the default + icon + name, which is determined by the + color + argument.

+

Acceptable values: any + + Flight + icon name or pass + false + for no icon.

+
+ +
title string
+
+

The title text in the alert.

+
+ +
description string
+
+

The description text in the alert.

+
+
onDismiss function
+
+

+ The alert can be dismissed by the user. When a function is passed, the "dismiss" button is displayed. +

+
+ +
...attributes
+
+

...attributes spreading is supported on this component.

+
+
+ +

Contextual components

+

Actions and generic content can optionally be passed into + the alert as yielded components, using the + Button, + Link::Standalone, + LinkTo::Standalone, + Generic + keys.

+
+
<[A].Button> yielded component
+
+

It is a yielded + HDS::Button + component, so it exposes exactly + the same API of the + Button + component, apart from the + @size + argument that is pre-defined to be + small, and the + @color + argument that accepts only + secondary + or + tertiary.

+
+
<[A].Link::Standalone> yielded component
+
+

It is a yielded + HDS::Link::Standalone + component, so it exposes exactly + the same API of the + Link::Standalone + component, apart from the + @size + argument that is pre-defined to be + small.

+
+
<[A].LinkTo::Standalone> yielded component
+
+

It is a yielded + HDS::LinkTo::Standalone + component, so it exposes exactly + the same API of the + LinkTo::Standalone + component, apart from the + @size + argument that is pre-defined to be + small.

+
+
<[A].Generic> yielded component
+
+

It is a very simple component that yields its content (inside a + <div>) and accepts + ...attributes + spreading.

+

Notice: generic the content will appear at the bottom, after title, description and actions, and the + developer will need to take care of spacing, layout and styling of the custom content in this case.

+

🚨 + Important: this method should be used only in special cases and as an escape hatch. If you + find yourself in need to use it, we suggest to speak with the design system team to check that the solution is + conformant and satifies the accessibility criteria. +

+
+
+ +

+ For more details about how to invoke these contextual components see the sections + "How to use > Actions" + and + "How to use > Generic content" + below. +

+
+ +
+

§ How to use

+ +

Basic use

+

+ The most basic invocation requires the + type, + title + and/or + text + arguments to be passed. By default a + neutral + alert is generated (with a neutral color applied and a specific icon visible). +

+ {{! prettier-ignore-start }} + + {{! prettier-ignore-end }} +

Renders to:

+ +

+ If needed, you can pass only + title + or only + text + as argument. +

+ {{! prettier-ignore-start }} + + + {{! prettier-ignore-end }} +

Renders to:

+ +
+ + +

Type

+

+ A different type of alert can be invoked using the + type + argument. +

+ {{! prettier-ignore-start }} + + {{! prettier-ignore-end }} +

Renders to:

+ + +

Color

+

+ A different color can be applied to the alert using the + color + argument. This will also determine the icon default used in the alert (unless overwritten, see below). +

+ {{! prettier-ignore-start }} + + {{! prettier-ignore-end }} +

Renders to:

+ + +

Icon

+

+ A different icon can be used in the alert using the + icon + argument. +

+ {{! prettier-ignore-start }} + + {{! prettier-ignore-end }} +

Renders to:

+ +

+ If instead you want to completely hide the icon you have to pass a + false + value to the + icon + argument. +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + +

Dismiss

+

+ In some cases the alert needs to be dismissable. In this case you have to pass a callback function to the + onDismiss + argument. This will also automatically add a "dismiss/close" button to the alert, that when clicked will execute the + callback function. +

+

+ 🚨 + Important: the actual implementation of what happens to the alert when the callback function is + invoked is left to the developer (this will likely depent on the type of alert, on the context of where it's used, + on the specific use case, etc.). +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + +

Actions

+

Actions can optionally be passed to component using one of the suggested + Button, + Link::Standalone + or + LinkTo::Standalone + contextual components.

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + + + + + +

Generic content

+

It's also possible to insert custom content in the component using the + Generic + contextual component. +

+

Notice: the content will appear at the bottom, after title, description and actions, + and the developer will need to take care of spacing, layout and styling of the custom content in this case. +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + + [your content here] + + +

🚨 + Important: this method should be used only in special cases and as an escape hatch. If you find + yourself in need to use it, we suggest to speak with the design system team to check that the solution is + conformant and satifies the accessibility criteria. +

+ +
+ +
+

§ + Design guidelines

+
+

Figma UI Kit

+
+ + +
+
+ +
+

§ Accessibility

+

This component has been designed and implemented with accessibility in mind. When used as + designed, there should not be any accessibility issues with this component. +

+

+ Applicable WCAG Success Criteria (Reference) +

+

+ This section is for reference only. This component intends to conform to the following WCAG success criteria: +

+ +
+ +
+

Showcase

+ +

👀 Note: the compact alert is borderless, but shown with a dotted border throughout the + "Showcase" for clarity.

+ +

Type

+ {{#each @model.TYPES as |type|}} +

{{capitalize type}}

+
+
+ +
+ {{/each}} + +

Color

+
+ {{#each @model.COLORS as |color|}} +

{{capitalize color}}

+ {{#each @model.TYPES as |type|}} +
+ +
+ {{/each}} + {{/each}} +
+ +

Icon

+
+ + + + +
+ +

Content

+
+ + + + + + + + + + +
+ +

Actions

+
+ + + + + + + + + + + + +
This for example could be extra text, specific for + a special use case.
+
+
+
+ +

Dismiss

+
+ + + + +
+ +
\ No newline at end of file diff --git a/packages/components/tests/dummy/app/templates/components/link/cta.hbs b/packages/components/tests/dummy/app/templates/components/link/cta.hbs index 017cd691a0f..946ad1ac13b 100644 --- a/packages/components/tests/dummy/app/templates/components/link/cta.hbs +++ b/packages/components/tests/dummy/app/templates/components/link/cta.hbs @@ -91,7 +91,7 @@
-

§ How to use

+

§ How to use

Basic use

The most basic invocation requires text diff --git a/packages/components/tests/dummy/app/templates/components/toast.hbs b/packages/components/tests/dummy/app/templates/components/toast.hbs new file mode 100644 index 00000000000..598e13d2016 --- /dev/null +++ b/packages/components/tests/dummy/app/templates/components/toast.hbs @@ -0,0 +1,471 @@ +{{page-title "Toast component"}} + +

Toast

+ +
+

§ Overview

+ +

+ A Toast is an element intended for + messages that are the result of a user's actions + (for system-generated messages see the + Alert + component.). +

+ +

Typically it displays a brief, temporary notification and it disappears on its own (after a + time interval) or as a result of a user interacting with it. +

+ +

+ It can have an + icon + (optional), a + title + and/or + description + (required to have at least one of the two), some + actions + (optional) and a + dismiss/close + button.

+

+ 🚨 + Important: we provide only the visual styling to this element, so other features like + placement, animations/transitions, and what happens on dismiss, will need to be implemented in your app (eg. with + an Ember addon). + +

+
+ +
+

§ Component API

+
+

Notice: the + Hds::Toast + component is built out of the + Hds::Alert(Inline) + component, so it shares the same API. Please refer to + the Alert Component API documentation + to know more.

+
+ +
+ +
+

§ How to use

+ +

Basic use

+

+ The most basic invocation requires the + type, + title + and/or + text + arguments to be passed, and an + onDismiss + callback function. By default a + neutral + toast is generated (with a neutral color applied and a specific icon visible). +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ +

+ 🚨 + Important: the actual implementation of what happens to the alert when the + onDismiss + function is invoked is left to the developer. +

+ +

+ If needed, you can pass only + title + or only + text + as argument. +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ +
+ + +

Color

+

+ A different color can be applied to the toast using the + color + argument. This will also determine the icon default used in the toast (unless overwritten, see below). +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + +

Icon

+

+ A different icon can be used in the toast using the + icon + argument. +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ +

+ If instead you want to completely hide the icon you have to pass a + false + value to the + icon + argument. +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + +

Actions

+

Actions can optionally be passed into the component using one of the suggested + Button, + Link::Standalone + or + LinkTo::Standalone + yielded components.

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + + + + +

Generic content

+

It's also possible to insert custom content in the + Generic + contextual component. +

+

Notice: the content will appear at the bottom, after title, description and actions, + and the developer will need to take care of spacing, layout and styling of the custom content in this case. +

+ {{! prettier-ignore-start }} + {{! template-lint-disable no-unbalanced-curlies }} + + {{! template-lint-enable no-unbalanced-curlies }} + {{! prettier-ignore-end }} +

Renders to:

+ + + [your content here] + + +

🚨 + Important: this method should be used only in special cases and as an escape hatch. If you find + yourself in need to use it, we suggest to speak with the design system team to check that the solution is + conformant and satifies the accessibility criteria. +

+
+ +
+

§ + Design guidelines

+
+

Figma UI Kit

+
+ +
+
+ +
+

§ Accessibility

+

This component has been designed and implemented with accessibility in mind. When used as + designed, there should not be any accessibility issues with this component. +

+

+ Applicable WCAG Success Criteria (Reference) +

+

+ This section is for reference only. This component intends to conform to the following WCAG success criteria: +

+ +
+ +
+

§ Showcase

+ +

Color

+
+ {{#each @model.COLORS as |color|}} + + {{/each}} +
+ +

Icon

+
+ + + + +
+ +

Content

+
+ + + + + + + + + + +
+ +

Actions

+
+ + + + + + + + +
+
\ No newline at end of file diff --git a/packages/components/tests/dummy/app/templates/index.hbs b/packages/components/tests/dummy/app/templates/index.hbs index c02cffee2a0..bdedd771b32 100644 --- a/packages/components/tests/dummy/app/templates/index.hbs +++ b/packages/components/tests/dummy/app/templates/index.hbs @@ -32,6 +32,11 @@ Components:
    +
  1. + + Alert + +
  2. Badge @@ -82,6 +87,11 @@ LinkTo::Standalone
  3. +
  4. + + Toast + +

Utilities: diff --git a/packages/components/tests/dummy/public/assets/images/alert-design-usage-part1.png b/packages/components/tests/dummy/public/assets/images/alert-design-usage-part1.png new file mode 100644 index 00000000000..e8fd5b84322 Binary files /dev/null and b/packages/components/tests/dummy/public/assets/images/alert-design-usage-part1.png differ diff --git a/packages/components/tests/dummy/public/assets/images/alert-design-usage-part2.png b/packages/components/tests/dummy/public/assets/images/alert-design-usage-part2.png new file mode 100644 index 00000000000..2f0290df67f Binary files /dev/null and b/packages/components/tests/dummy/public/assets/images/alert-design-usage-part2.png differ diff --git a/packages/components/tests/dummy/public/assets/images/toast-design-usage.png b/packages/components/tests/dummy/public/assets/images/toast-design-usage.png new file mode 100644 index 00000000000..7ae0da031f4 Binary files /dev/null and b/packages/components/tests/dummy/public/assets/images/toast-design-usage.png differ diff --git a/packages/components/tests/integration/components/hds/alert/index-test.js b/packages/components/tests/integration/components/hds/alert/index-test.js new file mode 100644 index 00000000000..a2efd261026 --- /dev/null +++ b/packages/components/tests/integration/components/hds/alert/index-test.js @@ -0,0 +1,216 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, resetOnerror, setupOnerror } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | hds/alert/index', function (hooks) { + setupRenderingTest(hooks); + + hooks.afterEach(() => { + resetOnerror(); + }); + + test('it renders the alert container', async function (assert) { + await render(hbs``); + assert.dom(this.element).exists(); + }); + test('it should render with a CSS class that matches the component name', async function (assert) { + await render( + hbs`` + ); + assert.dom('#test-alert').hasClass('hds-alert'); + }); + + // TYPE + + test('it should render the correct CSS type class if @type prop is declared', async function (assert) { + await render( + hbs`` + ); + assert.dom('#test-alert').hasClass('hds-alert--type-inline'); + }); + + // ICON + + test('it should render an icon by default depending on the type and color', async function (assert) { + // here we don't test all the possible combinations, only some of them as precaution + assert.expect(6); + await render(hbs``); + assert.dom('.flight-icon-info').exists(); + await render( + hbs`` + ); + assert.dom('.flight-icon-info-fill').exists(); + await render( + hbs`` + ); + assert.dom('.flight-icon-info').exists(); + await render( + hbs`` + ); + assert.dom('.flight-icon-check-circle').exists(); + await render( + hbs`` + ); + assert.dom('.flight-icon-alert-triangle').exists(); + await render( + hbs`` + ); + assert.dom('.flight-icon-alert-diamond').exists(); + }); + + test('if an icon is declared, the icon should render in the component and override the default one', async function (assert) { + assert.expect(2); + await render( + hbs`` + ); + assert.dom('.flight-icon-clipboard-copy').exists(); + await render( + hbs`` + ); + assert.dom('.flight-icon-clipboard-copy').exists(); + }); + + test('it should display no icon when @icon is set to false', async function (assert) { + await render( + hbs`` + ); + assert.dom('.flight-icon').doesNotExist(); + }); + + // TEXT (TITLE + DESCRIPTION) + + test('it should render the title when the @title argument is provided', async function (assert) { + await render(hbs``); + assert.dom(this.element).hasText('This is the title'); + }); + test('it should render the description when the @description argument is provided', async function (assert) { + await render( + hbs`` + ); + assert.dom(this.element).hasText('This is the description'); + }); + test('it should render both the title and the description when both the @title and @description arguments are provided', async function (assert) { + assert.expect(2); + await render( + hbs`` + ); + assert.dom('.hds-alert__title').hasText('This is the title'); + assert.dom('.hds-alert__description').hasText('This is the description'); + }); + test('it should render rich HTML when the @description argument contains HTML tags', async function (assert) { + assert.expect(8); + await render( + hbs`` + ); + assert.dom('.hds-alert__description strong').exists().hasText('strong'); + assert.dom('.hds-alert__description em').exists().hasText('em'); + assert.dom('.hds-alert__description code').exists().hasText('code'); + assert.dom('.hds-alert__description a').exists().hasText('link'); + }); + + // ACTIONS + + test('it should render an Hds::Button component yielded to the "actions" container', async function (assert) { + assert.expect(5); + await render( + hbs`` + ); + assert + .dom('#test-alert .hds-alert__actions button') + .exists() + .hasClass('hds-button') + .hasClass('hds-button--size-small') + .hasClass('hds-button--color-secondary') + .hasText('I am a button'); + }); + test('it should render an Hds::Link::Standalone component yielded to the "actions" container', async function (assert) { + assert.expect(5); + await render( + hbs`` + ); + assert + .dom('#test-alert .hds-alert__actions a') + .exists() + .hasClass('hds-link-standalone') + .hasClass('hds-link-standalone--size-small') + .hasClass('hds-link-standalone--color-secondary') + .hasText('I am a link'); + }); + + // GENERIC + + test('it should render any content passed to the "generic" contextual component', async function (assert) { + assert.expect(2); + await render( + hbs`
test
` + ); + assert.dom('#test-alert .hds-alert__content pre').exists().hasText('test'); + }); + + // DISMISS + + test('it should not render the "dismiss" button by default', async function (assert) { + await render(hbs``); + assert.dom('button.hds-alert__dismiss').doesNotExist(); + }); + test('it should render the "dismiss" button if a callback function is passed to the @onDismiss argument', async function (assert) { + this.set('NOOP', () => {}); + await render( + hbs`` + ); + assert.dom('button.hds-alert__dismiss').exists(); + }); + + // A11Y + + test('it should render with the correct semantic tags and aria attributes', async function (assert) { + await render( + hbs`` + ); + assert.dom('#test-alert').hasAttribute('role', 'alert'); + }); + + // ASSERTIONS + + test('it should throw an assertion if an incorrect value for @type is provided', async function (assert) { + const errorMessage = + '@type for "Hds::Alert" must be one of the following: page, inline, compact; received: foo'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + await render(hbs``); + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + test('it should throw an assertion if a "compact" alerts is rendered with @icon equal to false', async function (assert) { + const errorMessage = + '@icon for "Hds::Alert" with @type "compact" is required'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + await render( + hbs`` + ); + assert.throws(function () { + throw new Error(errorMessage); + }); + }); + test('it should throw an assertion if an incorrect value for @color is provided', async function (assert) { + const errorMessage = + '@color for "Hds::Alert" must be one of the following: neutral, highlight, success, warning, critical; received: foo'; + assert.expect(2); + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + await render( + hbs`` + ); + assert.throws(function () { + throw new Error(errorMessage); + }); + }); +}); diff --git a/packages/components/tests/integration/components/hds/toast/index-test.js b/packages/components/tests/integration/components/hds/toast/index-test.js new file mode 100644 index 00000000000..b68e8dff5a2 --- /dev/null +++ b/packages/components/tests/integration/components/hds/toast/index-test.js @@ -0,0 +1,20 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | hds/toast/index', function (hooks) { + setupRenderingTest(hooks); + + // notice: "toast" is a wrapper around the "hds::alert" so we test only very specific things + + test('it renders the "toast"', async function (assert) { + await render(hbs``); + assert.dom(this.element).exists(); + }); + + test('it should render with a CSS class that matches the component name', async function (assert) { + await render(hbs``); + assert.dom('#test-toast').hasClass('hds-toast'); + }); +});