Toasts are small messages designed to mimic push notifications. They are used to provide users with application level information.
+
+
Overview
+
Toasts should be used to display temporary messages about actions or events that occured or in need of attention, with no relation to content on a page. For messaging that provide context in close promixity to a piece of content within a page, use the Notification component.
+
+
Basic toast
+
+
+
+
+
Configurations
+Toasts can be configured by using the DaffToastService.
+
+The following is an example of a toast with a duration:
+
+
constructor(private toastService: DaffToastService) {}
+
+ open() {
+ this.toast = this.toastService.open({
+ title: 'Update Complete',
+ message: 'This page has been updated to the newest version.',
+ },
+ {
+ duration: 5000,
+ });
+ }
+
+The following is an example of a toast with actions:
+
+
constructor(private toastService: DaffToastService) {}
+
+open() {
+ this.toast = this.toastService.open({
+ title: 'Update Available',
+ message: 'A new version of this page is available.',
+ actions: [
+ { content: 'Update', color: 'theme-contrast', size: 'sm', eventEmitter: this.update },
+ { content: 'Remind me later', type: 'flat', size: 'sm', eventEmitter: this.closeToast },
+ ],
+ });
+}
+
+The following configurations are available in the DaffToastService:
+
+
+
+
+
Property
+
Type
+
Description
+
Default
+
+
+
+
+
title
+
string
+
A quick overview of the toast
+
--
+
+
+
message
+
string
+
Additional details about the message that should be limited to one or two sentences
+
--
+
+
+
actions
+
DaffToastAction
+
Adds a daff-button that allow users to perform an action related to the message. Actions should be limited to two buttons.
+
--
+
+
+
dismissible
+
boolean
+
Allows a toast to be dismissible via a close button
+
true
+
+
+
duration
+
number
+
The duration in milliseconds that a toast is visible before it's dismissed
+
5000
+
+
+
+
+The actions configurations are based on the properties of the DaffButtonComponent (view Button Documentation), with the addition of data and eventEmitter.
+
+
Dismissal
+
A toast can be dismissed via a timed duration, a close button, or the ESC key.
+
+
Timed duration
+
A toast with actions will persist until one of the actions have been interacted with, or is dismissed by the close button or the ESC key. Setting a duration should be avoided for toasts that have actions as users may need to interact with the actions.
+
+
Toast with custom duration
+
+
+
+
+
By default, a toast without actions will be dismissed after 5000ms. This can be updated by setting duration through the DaffToastService.
+
+
Close button
+
The close button is shown by default but can be hidden by setting dismissible: false through the DaffToastService.
+
+
Escape key
+
A toast can be dismissed by using the ESC key if it has actions and is focus trapped.
+
+
Stacking
+
A maximum of three toasts can be shown at a time. Toasts are stacked vertically, with the most recent toast displayed on top.
+
+
Statuses
+
The status color of a toast can be updated by using the status property.
+
+
Supported statuses: warn | danger | success
+
+
Toast with statuses
+
+
+
+
+
Positions
+
+
+
+
+
Property
+
Value
+
Default
+
+
+
+
+
vertical
+
top | bottom
+
top
+
+
+
horizontal
+
left | center | right
+
right
+
+
+
+
+
To change the horizontal and vertical position of a toast, add the provideDaffToastOptions dependency key to the providers key in the module as shown below:
The position of a toast on a mobile device will always be on the bottom center.
+
+
Toast with configurable positions
+
+
+
+
+
Accessibility
+
By default, toasts use a role="status" to announce messages. It's the equivalent of aria-live="polite", which does not interrupt a user's current activity and waits until they are idle to make the announcement. When a toast has actions, a role="alertdialog" is used. The toast will also be focus trapped, and the focus immediately moves to the actions.
+
+
Avoid setting a duration on toasts with actions because they will disappear automatically, making it difficult for users to interact with the actions.
diff --git a/apps/design-land/src/app/toast/toast.component.scss b/apps/design-land/src/app/toast/toast.component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/design-land/src/app/toast/toast.component.spec.ts b/apps/design-land/src/app/toast/toast.component.spec.ts
new file mode 100644
index 0000000000..228536207a
--- /dev/null
+++ b/apps/design-land/src/app/toast/toast.component.spec.ts
@@ -0,0 +1,29 @@
+import {
+ ComponentFixture,
+ TestBed,
+ waitForAsync,
+} from '@angular/core/testing';
+
+import { DesignLandToastComponent } from './toast.component';
+
+describe('DesignLandToastComponent', () => {
+ let component: DesignLandToastComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [ DesignLandToastComponent ],
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DesignLandToastComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/apps/design-land/src/app/toast/toast.component.ts b/apps/design-land/src/app/toast/toast.component.ts
new file mode 100644
index 0000000000..60021ad3be
--- /dev/null
+++ b/apps/design-land/src/app/toast/toast.component.ts
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'design-land-toast',
+ templateUrl: './toast.component.html',
+ styleUrls: ['./toast.component.scss'],
+})
+export class DesignLandToastComponent {}
diff --git a/apps/design-land/src/app/toast/toast.module.ts b/apps/design-land/src/app/toast/toast.module.ts
new file mode 100644
index 0000000000..35b22d28c7
--- /dev/null
+++ b/apps/design-land/src/app/toast/toast.module.ts
@@ -0,0 +1,28 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { DaffArticleModule } from '@daffodil/design/article';
+import { DaffToastModule } from '@daffodil/design/toast';
+
+import { DesignLandToastRoutingModule } from './toast-routing-module';
+import { DesignLandToastComponent } from './toast.component';
+import { DesignLandArticleEncapsulatedModule } from '../core/article-encapsulated/article-encapsulated.module';
+import { DesignLandExampleViewerModule } from '../core/code-preview/container/example-viewer.module';
+
+@NgModule({
+ declarations: [
+ DesignLandToastComponent,
+ ],
+ imports: [
+ CommonModule,
+ RouterModule,
+ DesignLandToastRoutingModule,
+ DesignLandExampleViewerModule,
+ DesignLandArticleEncapsulatedModule,
+
+ DaffArticleModule,
+ DaffToastModule,
+ ],
+})
+export class DesignLandToastModule {}
diff --git a/apps/design-land/src/assets/nav.json b/apps/design-land/src/assets/nav.json
index a506f9ba56..a9f1e67769 100644
--- a/apps/design-land/src/assets/nav.json
+++ b/apps/design-land/src/assets/nav.json
@@ -242,6 +242,13 @@
"id": "sidebar",
"items": [],
"data": {}
+ },
+ {
+ "title": "Toast",
+ "url": "toast",
+ "id": "toast",
+ "items": [],
+ "data": {}
}
],
"data": {}
diff --git a/libs/design/modal/src/service/modal.service.ts b/libs/design/modal/src/service/modal.service.ts
index d9ac73ab05..0a3baa18e0 100644
--- a/libs/design/modal/src/service/modal.service.ts
+++ b/libs/design/modal/src/service/modal.service.ts
@@ -45,8 +45,7 @@ export class DaffModalService {
return this.overlay.create({
hasBackdrop: true,
positionStrategy: new GlobalPositionStrategy()
- .centerHorizontally()
- .centerVertically(),
+ .centerHorizontally(),
scrollStrategy: this.overlay.scrollStrategies.block(),
});
}
diff --git a/libs/design/scss/theme.scss b/libs/design/scss/theme.scss
index fb39ddf6f5..b2a6c08f7d 100644
--- a/libs/design/scss/theme.scss
+++ b/libs/design/scss/theme.scss
@@ -40,6 +40,7 @@
@use '../sidebar/src/sidebar-theme' as sidebar;
@use '../scss/state/skeleton/mixins' as skeleton;
@use '../tree/src/tree-theme' as tree;
+@use '../toast/src/toast-theme' as toast;
//
// Generates the styles of a @daffodil/design theme.
@@ -81,4 +82,5 @@
@include paginator.daff-paginator-theme($theme);
@include sidebar.daff-sidebar-theme($theme);
@include tree.daff-tree-theme($theme);
+ @include toast.daff-toast-theme($theme);
}
diff --git a/libs/design/toast/README.md b/libs/design/toast/README.md
new file mode 100644
index 0000000000..fb2c310c1a
--- /dev/null
+++ b/libs/design/toast/README.md
@@ -0,0 +1,115 @@
+# Toast
+Toasts are small messages designed to mimic push notifications. They are used to provide users with application level information.
+
+## Overview
+Toasts should be used to display temporary messages about actions or events that occured or in need of attention, with no relation to content on a page. For messaging that provide context in close promixity to a piece of content within a page, use the [Notification](../notification/README.md) component.
+
+### Basic Toast
+
+
+
+
+### Configurations
+Toast can be configured by using the `DaffToastService`.
+
+The following is an example of a toast with a duration:
+
+```ts
+constructor(private toastService: DaffToastService) {}
+
+open() {
+ this.toast = this.toastService.open({
+ title: 'Update Complete',
+ message: 'This page has been updated to the newest version.',
+ },
+ {
+ duration: 5000,
+ });
+}
+```
+
+The following is an example of a toast with actions:
+
+```ts
+open() {
+ this.toast = this.toastService.open({
+ title: 'Update Available',
+ message: 'A new version of this page is available.',
+ actions: [
+ { content: 'Update', color: 'theme-contrast', size: 'sm', eventEmitter: this.update },
+ { content: 'Remind me later', type: 'flat', size: 'sm', eventEmitter: this.closeToast },
+ ]
+ });
+}
+```
+
+The following configurations are available in the `DaffToastService`:
+
+| Property | Type | Description | Default |
+| -------- | ------ | ------------------------------- | ------- |
+| title | string | A quick overview of the toast | -- |
+| message | string | Additional details about the message that should be limited to one or two sentences | -- |
+| actions | `DaffToastAction` | Adds a `daff-button` that allow users to perform an action related to the message. Actions should be limited to two buttons. | -- |
+| dismissible | boolean | Allows a toast to be dismissible via a close button | true |
+| duration | number | The duration in milliseconds that a toast is visible before it's dismissed | 5000 |
+
+The `actions` configurations are based on the properties of the `DaffButtonComponent` (view [Button Documentation](../src/atoms/button/README.md)) with the addition of `data` and `eventEmitter`.
+
+### Dismissal
+A toast can be dismissed via a timed duration, a close button, or the `ESC` key.
+
+##### Timed duration
+A toast with actions will persist until one of the actions have been interacted with, or is dismissed by the close button or the `ESC` key. Actionable toasts should be persistent, but a duration is allowed to be set. If duration must be set, make sure it's long enough for users to engage with the actions.
+
+By default, a toast without actions will be dismissed after `5000ms`. This can be updated by setting `duration` through the `DaffToastService`.
+
+#### Toast with custom duration
+
+
+##### Close button
+The close button is shown by default but can be hidden by setting `dismissible: false` through the `DaffToastService`.
+
+##### Escape Key
+A toast can be dismissed by using the `ESC` key if it has actions and is focus trapped.
+
+### Stacking
+A maximum of three toasts can be shown at a time. Toasts are stacked vertically, with the most recent toast displayed on top.
+
+### Statuses
+The status color of a toast can be updated by using the `status` property.
+
+Supported statuses: `warn | danger | success`
+
+#### Toast with statuses
+
+
+### Positions
+
+| Property | Value | Default |
+| ------------ | ------------------------ | ------- |
+| `vertical` | `top | bottom` | top |
+| `horizontal` | `left | center | right ` | right |
+
+To change the horizontal and vertical position of a toast, add the `provideDaffToastOptions` dependency key to the `providers` key in the module as shown below:
+
+```ts
+providers: [
+ provideDaffToastOptions({
+ position: {
+ vertical: 'bottom',
+ horizontal: 'center',
+ }
+ useParent: false,
+ })
+]
+```
+
+The position of a toast on a mobile device will always be on the bottom center.
+
+#### Toast with configurable positions
+
+
+### Accessibility
+By default, toasts use a `role="status"` to announce messages. It's the equivalent of `aria-live="polite"`, which does not interrupt a user's current activity and waits until they are idle to make the announcement. When a toast has actions, a `role="alertdialog"` is used. The toast will be focus trapped and focus immediately moves to the actions.
+
+Avoid setting a duration on toasts with actions because they will disappear automatically, making it difficult for users to interact with the actions.
\ No newline at end of file
diff --git a/libs/design/toast/examples/ng-package.json b/libs/design/toast/examples/ng-package.json
new file mode 100644
index 0000000000..1fad56732b
--- /dev/null
+++ b/libs/design/toast/examples/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
+ "lib": {
+ "entryFile": "src/index.ts",
+ "styleIncludePaths": ["../../src/scss"]
+ }
+}
\ No newline at end of file
diff --git a/libs/design/toast/examples/src/default-toast/default-toast.component.html b/libs/design/toast/examples/src/default-toast/default-toast.component.html
new file mode 100644
index 0000000000..d48b22c39e
--- /dev/null
+++ b/libs/design/toast/examples/src/default-toast/default-toast.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/libs/design/toast/examples/src/default-toast/default-toast.component.ts b/libs/design/toast/examples/src/default-toast/default-toast.component.ts
new file mode 100644
index 0000000000..8242d53638
--- /dev/null
+++ b/libs/design/toast/examples/src/default-toast/default-toast.component.ts
@@ -0,0 +1,52 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ OnInit,
+} from '@angular/core';
+
+import {
+ DaffToast,
+ DaffToastAction,
+ DaffToastService,
+} from '@daffodil/design/toast';
+
+@Component({
+ // eslint-disable-next-line @angular-eslint/component-selector
+ selector: 'default-toast',
+ templateUrl: './default-toast.component.html',
+ styles: [],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DefaultToastComponent implements OnInit {
+ private toast: DaffToast;
+
+ constructor(private toastService: DaffToastService) {}
+
+ update = new EventEmitter();
+
+ closeToast = new EventEmitter();
+
+ open() {
+ this.toast = this.toastService.open({
+ title: 'Update Available' + ' ' + this.count++,
+ message: 'A new version of this page is available.',
+ actions: [
+ { content: 'Update', color: 'theme-contrast', size: 'sm', eventEmitter: this.update },
+ { content: 'Remind me later', type: 'flat', size: 'sm', eventEmitter: this.closeToast },
+ ],
+ });
+ }
+
+ ngOnInit() {
+ this.update.subscribe(() => {
+ console.log('test');
+ });
+
+ this.closeToast.subscribe(() => {
+ this.toastService.close(this.toast);
+ });
+ }
+
+ private count = 0;
+}
diff --git a/libs/design/toast/examples/src/default-toast/default-toast.module.ts b/libs/design/toast/examples/src/default-toast/default-toast.module.ts
new file mode 100644
index 0000000000..8e8de2c3dc
--- /dev/null
+++ b/libs/design/toast/examples/src/default-toast/default-toast.module.ts
@@ -0,0 +1,24 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+
+import { DaffButtonModule } from '@daffodil/design/button';
+import { DaffToastModule } from '@daffodil/design/toast';
+
+import { DefaultToastComponent } from './default-toast.component';
+
+@NgModule({
+ declarations: [
+ DefaultToastComponent,
+ ],
+ imports: [
+ CommonModule,
+ DaffToastModule,
+ FontAwesomeModule,
+ DaffButtonModule,
+ ],
+ exports: [
+ DefaultToastComponent,
+ ],
+})
+export class DefaultToastModule { }
diff --git a/libs/design/toast/examples/src/index.ts b/libs/design/toast/examples/src/index.ts
new file mode 100644
index 0000000000..4aaf8f92ed
--- /dev/null
+++ b/libs/design/toast/examples/src/index.ts
@@ -0,0 +1 @@
+export * from './public_api';
diff --git a/libs/design/toast/examples/src/public_api.ts b/libs/design/toast/examples/src/public_api.ts
new file mode 100644
index 0000000000..7d427a7852
--- /dev/null
+++ b/libs/design/toast/examples/src/public_api.ts
@@ -0,0 +1,17 @@
+import { ComponentExample } from '@daffodil/design';
+
+import { DefaultToastComponent } from './default-toast/default-toast.component';
+import { DefaultToastModule } from './default-toast/default-toast.module';
+import { ToastPositionsComponent } from './toast-positions/toast-positions.component';
+import { ToastPositionsModule } from './toast-positions/toast-positions.module';
+import { ToastStatusComponent } from './toast-status/toast-status.component';
+import { ToastStatusModule } from './toast-status/toast-status.module';
+import { ToastWithCustomDurationComponent } from './toast-with-custom-duration/toast-with-custom-duration.component';
+import { ToastWithCustomDurationModule } from './toast-with-custom-duration/toast-with-custom-duration.module';
+
+export const TOAST_EXAMPLES: ComponentExample[] = [
+ { component: DefaultToastComponent, module: DefaultToastModule },
+ { component: ToastStatusComponent, module: ToastStatusModule },
+ { component: ToastPositionsComponent, module: ToastPositionsModule },
+ { component: ToastWithCustomDurationComponent, module: ToastWithCustomDurationModule },
+];
diff --git a/libs/design/toast/examples/src/toast-positions/toast-positions.component.html b/libs/design/toast/examples/src/toast-positions/toast-positions.component.html
new file mode 100644
index 0000000000..4505cae642
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-positions/toast-positions.component.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/libs/design/toast/examples/src/toast-positions/toast-positions.component.scss b/libs/design/toast/examples/src/toast-positions/toast-positions.component.scss
new file mode 100644
index 0000000000..8740c10ebe
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-positions/toast-positions.component.scss
@@ -0,0 +1,7 @@
+.toast-positions {
+ &__options {
+ display: flex;
+ gap: 8px;
+ margin: 0 0 16px;
+ }
+}
\ No newline at end of file
diff --git a/libs/design/toast/examples/src/toast-positions/toast-positions.component.ts b/libs/design/toast/examples/src/toast-positions/toast-positions.component.ts
new file mode 100644
index 0000000000..3c615b098d
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-positions/toast-positions.component.ts
@@ -0,0 +1,56 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ OnInit,
+} from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { combineLatest } from 'rxjs';
+
+import {
+ DaffToast,
+ DaffToastService,
+ DaffToastPositionService,
+} from '@daffodil/design/toast';
+
+@Component({
+ // eslint-disable-next-line @angular-eslint/component-selector
+ selector: 'toast-positions',
+ templateUrl: './toast-positions.component.html',
+ styleUrls: ['./toast-positions.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ToastPositionsComponent implements OnInit {
+ private toast: DaffToast;
+
+ constructor(
+ private toastService: DaffToastService,
+ private toastPositionService: DaffToastPositionService,
+ ) {}
+
+ open() {
+ this.toast = this.toastService.open({
+ title: 'Update complete' + ' ' + this.count++,
+ message: 'This page is now up-to-date.',
+ dismissible: true,
+ });
+ }
+
+ private count = 0;
+
+ horizontalControl: FormControl = new FormControl('right');
+ verticalControl: FormControl = new FormControl('top');
+
+ ngOnInit() {
+ combineLatest([
+ this.horizontalControl.valueChanges,
+ this.verticalControl.valueChanges,
+ ]).subscribe(([
+ horizontal, vertical,
+ ]) => {
+ this.toastPositionService.setPosition({
+ horizontal,
+ vertical,
+ });
+ });
+ }
+}
diff --git a/libs/design/toast/examples/src/toast-positions/toast-positions.module.ts b/libs/design/toast/examples/src/toast-positions/toast-positions.module.ts
new file mode 100644
index 0000000000..7e3b403713
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-positions/toast-positions.module.ts
@@ -0,0 +1,39 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+
+import { DaffButtonModule } from '@daffodil/design/button';
+import {
+ DaffToastModule,
+ provideDaffToastOptions,
+} from '@daffodil/design/toast';
+
+import { ToastPositionsComponent } from './toast-positions.component';
+
+@NgModule({
+ declarations: [
+ ToastPositionsComponent,
+ ],
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+
+ DaffToastModule,
+ FontAwesomeModule,
+ DaffButtonModule,
+ ],
+ exports: [
+ ToastPositionsComponent,
+ ],
+ providers: [
+ provideDaffToastOptions({
+ position: {
+ vertical: 'top',
+ horizontal: 'right',
+ },
+ useParent: false,
+ }),
+ ],
+})
+export class ToastPositionsModule { }
diff --git a/libs/design/toast/examples/src/toast-status/toast-status.component.html b/libs/design/toast/examples/src/toast-status/toast-status.component.html
new file mode 100644
index 0000000000..db5b3db778
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-status/toast-status.component.html
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/libs/design/toast/examples/src/toast-status/toast-status.component.scss b/libs/design/toast/examples/src/toast-status/toast-status.component.scss
new file mode 100644
index 0000000000..728192d684
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-status/toast-status.component.scss
@@ -0,0 +1,6 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+}
\ No newline at end of file
diff --git a/libs/design/toast/examples/src/toast-status/toast-status.component.ts b/libs/design/toast/examples/src/toast-status/toast-status.component.ts
new file mode 100644
index 0000000000..f8bb44d799
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-status/toast-status.component.ts
@@ -0,0 +1,57 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+} from '@angular/core';
+import { FormControl } from '@angular/forms';
+import {
+ faCheck,
+ faExclamation,
+ faInfoCircle,
+} from '@fortawesome/free-solid-svg-icons';
+
+import {
+ DaffToastData,
+ DaffToastService,
+} from '@daffodil/design/toast';
+
+const status: Record = {
+ error: {
+ title: 'Server error',
+ },
+ success: {
+ title: 'Update complete',
+ message: 'The app is now up-to-date with the newest version.',
+ },
+ warn: {
+ title: 'The app is outdated',
+ message: 'Update the app now. The version you are using may have security vulnerabilities.',
+ },
+};
+
+@Component({
+ // eslint-disable-next-line @angular-eslint/component-selector
+ selector: 'toast-status',
+ templateUrl: './toast-status.component.html',
+ styleUrls: ['./toast-status.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ToastStatusComponent {
+ faInfoCircle = faInfoCircle;
+ faCheck = faCheck;
+ faExclamation = faExclamation;
+
+ statusControl: FormControl = new FormControl('success');
+
+ constructor(private toastService: DaffToastService) {}
+
+ open() {
+ this.toastService.open({
+ status: this.statusControl.value,
+ ...status[this.statusControl.value],
+ },
+ {
+ duration: this.statusControl.value === 'error' ? undefined : 5000,
+ },
+ );
+ }
+}
diff --git a/libs/design/toast/examples/src/toast-status/toast-status.module.ts b/libs/design/toast/examples/src/toast-status/toast-status.module.ts
new file mode 100644
index 0000000000..8a58c6709a
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-status/toast-status.module.ts
@@ -0,0 +1,26 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+
+import { DaffButtonModule } from '@daffodil/design/button';
+import { DaffToastModule } from '@daffodil/design/toast';
+
+import { ToastStatusComponent } from './toast-status.component';
+
+@NgModule({
+ declarations: [
+ ToastStatusComponent,
+ ],
+ imports: [
+ CommonModule,
+ DaffToastModule,
+ DaffButtonModule,
+ FontAwesomeModule,
+ ReactiveFormsModule,
+ ],
+ exports: [
+ ToastStatusComponent,
+ ],
+})
+export class ToastStatusModule { }
diff --git a/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.component.html b/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.component.html
new file mode 100644
index 0000000000..d48b22c39e
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.component.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.component.ts b/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.component.ts
new file mode 100644
index 0000000000..d3478738c5
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.component.ts
@@ -0,0 +1,35 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ OnInit,
+} from '@angular/core';
+
+import {
+ DaffToast,
+ DaffToastService,
+} from '@daffodil/design/toast';
+
+@Component({
+ // eslint-disable-next-line @angular-eslint/component-selector
+ selector: 'toast-with-custom-duration',
+ templateUrl: './toast-with-custom-duration.component.html',
+ styles: [],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ToastWithCustomDurationComponent {
+ private toast: DaffToast;
+
+ constructor(private toastService: DaffToastService) {}
+
+ open() {
+ this.toast = this.toastService.open({
+ title: 'Update Complete',
+ message: 'This page has been updated to the newest version.',
+ status: 'success',
+ },
+ {
+ duration: 7000,
+ });
+ }
+}
diff --git a/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.module.ts b/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.module.ts
new file mode 100644
index 0000000000..5b343c62cd
--- /dev/null
+++ b/libs/design/toast/examples/src/toast-with-custom-duration/toast-with-custom-duration.module.ts
@@ -0,0 +1,22 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { DaffButtonModule } from '@daffodil/design/button';
+import { DaffToastModule } from '@daffodil/design/toast';
+
+import { ToastWithCustomDurationComponent } from './toast-with-custom-duration.component';
+
+@NgModule({
+ declarations: [
+ ToastWithCustomDurationComponent,
+ ],
+ imports: [
+ CommonModule,
+ DaffToastModule,
+ DaffButtonModule,
+ ],
+ exports: [
+ ToastWithCustomDurationComponent,
+ ],
+})
+export class ToastWithCustomDurationModule { }
diff --git a/libs/design/toast/ng-package.json b/libs/design/toast/ng-package.json
new file mode 100644
index 0000000000..14833aac7d
--- /dev/null
+++ b/libs/design/toast/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
+ "lib": {
+ "entryFile": "src/index.ts",
+ "styleIncludePaths": ["../src/scss"]
+ }
+}
\ No newline at end of file
diff --git a/libs/design/toast/src/index.ts b/libs/design/toast/src/index.ts
new file mode 100644
index 0000000000..4aaf8f92ed
--- /dev/null
+++ b/libs/design/toast/src/index.ts
@@ -0,0 +1 @@
+export * from './public_api';
diff --git a/libs/design/toast/src/interfaces/toast-action.ts b/libs/design/toast/src/interfaces/toast-action.ts
new file mode 100644
index 0000000000..4546dd9444
--- /dev/null
+++ b/libs/design/toast/src/interfaces/toast-action.ts
@@ -0,0 +1,51 @@
+import { EventEmitter } from '@angular/core';
+
+import {
+ DaffPalette,
+ DaffStatus,
+} from '@daffodil/design';
+
+/**
+ * An interface for properties of actions, specifically the DaffButtonComponent, placed inside of the toast.
+ */
+export interface DaffToastAction {
+ /**
+ * The types of buttons available to be used, as defined in the {@link DaffButtonComponent}.
+ */
+ type?: 'raised' | 'underline' | 'stroked' | 'flat' | undefined;
+
+ /**
+ * The text for the button
+ */
+ content: string;
+
+ /**
+ * The size of the button, as defined in the {@link DaffButtonComponent}.
+ */
+ size?: 'sm' | 'md' | 'lg' | undefined;
+
+ /**
+ * The color of the button, as defined in the {@link DaffButtonComponent}.
+ * Color and status should not be used simultaneously.
+ */
+ color?: DaffPalette;
+
+ /**
+ * The status of the button, as defined in the {@link DaffButtonComponent}.
+ * Color and status should not be used simultaneously.
+ */
+ status?: DaffStatus;
+
+ data?: Record;
+
+ /**
+ * Sets an EventEmitter on a DaffToastAction
+ */
+ eventEmitter?: EventEmitter;
+}
+
+export interface DaffToastActionEvent {
+ event: MouseEvent;
+
+ action: DaffToastAction;
+}
diff --git a/libs/design/toast/src/interfaces/toast.ts b/libs/design/toast/src/interfaces/toast.ts
new file mode 100644
index 0000000000..b92b132a8c
--- /dev/null
+++ b/libs/design/toast/src/interfaces/toast.ts
@@ -0,0 +1,28 @@
+import { Observable } from 'rxjs';
+
+import { DaffStatus } from '@daffodil/design';
+
+import { DaffToastAction } from './toast-action';
+
+/** Possible data that can be shown on a toast */
+export interface DaffToastData {
+ /** A title that provides a quick oveview of the toast */
+ title: string;
+
+ /** A short message used to provide additional details about the toast */
+ message?: string;
+
+ /** Sets a status on the toast */
+ status?: DaffStatus;
+
+ /** Used to display actions in the toast */
+ actions?: DaffToastAction[];
+
+ /** Whether or not the toast is dismissible */
+ dismissible?: boolean;
+}
+
+export interface DaffToast extends DaffToastData {
+ dismiss: () => void;
+ dismissalStream: Observable;
+}
diff --git a/libs/design/toast/src/options/daff-toast-options.ts b/libs/design/toast/src/options/daff-toast-options.ts
new file mode 100644
index 0000000000..ad1ad413bd
--- /dev/null
+++ b/libs/design/toast/src/options/daff-toast-options.ts
@@ -0,0 +1,33 @@
+import {
+ InjectionToken,
+ ValueProvider,
+} from '@angular/core';
+
+type Without = { [P in Exclude]?: never };
+type XOR = (T | U) extends Record ? (Without & U) | (Without & T) : T | U;
+
+export interface VerticalPositionTypes { vertical: 'top' | 'bottom' };
+
+export interface HorizontalPositionTypes { horizontal: 'left' | 'center' | 'right' };
+
+export type DaffToastPosition = VerticalPositionTypes & HorizontalPositionTypes;
+
+export interface DaffToastOptions {
+ position: DaffToastPosition;
+ useParent: boolean;
+}
+
+export const daffToastDefaultOptions: DaffToastOptions = {
+ position: {
+ vertical: 'top',
+ horizontal: 'right',
+ },
+ useParent: true,
+};
+
+export const DAFF_TOAST_OPTIONS = new InjectionToken('DAFF_TOAST_OPTIONS', { providedIn: 'root', factory: () => daffToastDefaultOptions });
+
+export const provideDaffToastOptions = (options: DaffToastOptions): ValueProvider => ({ provide: DAFF_TOAST_OPTIONS, useValue: {
+ ...daffToastDefaultOptions,
+ ...options,
+}});
diff --git a/libs/design/toast/src/public_api.ts b/libs/design/toast/src/public_api.ts
new file mode 100644
index 0000000000..df18203755
--- /dev/null
+++ b/libs/design/toast/src/public_api.ts
@@ -0,0 +1,20 @@
+export { DaffToastPositionService } from './service/position.service';
+export { DaffToastModule } from './toast.module';
+export { DaffToastService } from './service/toast.service';
+export { DaffToastConfiguration } from './toast/toast-config';
+export {
+ DaffToast,
+ DaffToastData,
+} from './interfaces/toast';
+
+export { DaffToastAction } from './interfaces/toast-action';
+
+export {
+ DAFF_TOAST_OPTIONS,
+ provideDaffToastOptions,
+} from './options/daff-toast-options';
+
+export * from './toast/toast.component';
+export * from './toast-actions/toast-actions.directive';
+export * from './toast-title/toast-title.directive';
+export * from './toast-message/toast-message.directive';
diff --git a/libs/design/toast/src/service/changes-focus.ts b/libs/design/toast/src/service/changes-focus.ts
new file mode 100644
index 0000000000..84f0c00af9
--- /dev/null
+++ b/libs/design/toast/src/service/changes-focus.ts
@@ -0,0 +1,3 @@
+import { DaffToast } from '../interfaces/toast';
+
+export const daffToastChangesFocus = (toast: DaffToast): boolean => toast.actions?.length > 0;
diff --git a/libs/design/toast/src/service/position-strategy.ts b/libs/design/toast/src/service/position-strategy.ts
new file mode 100644
index 0000000000..0977bcede7
--- /dev/null
+++ b/libs/design/toast/src/service/position-strategy.ts
@@ -0,0 +1,37 @@
+import {
+ GlobalPositionStrategy,
+ PositionStrategy,
+} from '@angular/cdk/overlay';
+
+import { DaffToastPosition } from '../options/daff-toast-options';
+
+export const createPositionStrategy = (position: DaffToastPosition): PositionStrategy => {
+ const strat = new GlobalPositionStrategy();
+
+ switch ( position.horizontal ) {
+ case 'left':
+ strat.left('48px');
+ break;
+ case 'right':
+ strat.right('48px');
+ break;
+ case 'center':
+ strat.centerHorizontally();
+ break;
+ default:
+ strat.right('48px');
+ }
+
+ switch(position.vertical) {
+ case 'top':
+ strat.top('80px');
+ break;
+ case 'bottom':
+ strat.bottom('48px');
+ break;
+ default:
+ strat.top('80px');
+ }
+
+ return strat;
+};
diff --git a/libs/design/toast/src/service/position.service.spec.ts b/libs/design/toast/src/service/position.service.spec.ts
new file mode 100644
index 0000000000..c4173386da
--- /dev/null
+++ b/libs/design/toast/src/service/position.service.spec.ts
@@ -0,0 +1,33 @@
+import { BreakpointObserver } from '@angular/cdk/layout';
+import { TestBed } from '@angular/core/testing';
+
+import { DaffToastPositionService } from './position.service';
+
+describe('@daffodil/design/toast | DaffToastPositionService', () => {
+ let service: DaffToastPositionService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ ],
+ providers: [
+ DaffToastPositionService,
+ ],
+ });
+
+ service = new DaffToastPositionService(
+ {
+ position: {
+ horizontal: 'right',
+ vertical: 'bottom',
+ },
+ useParent: false,
+ },
+ TestBed.inject(BreakpointObserver),
+ );
+ });
+
+ it('should create', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/libs/design/toast/src/service/position.service.ts b/libs/design/toast/src/service/position.service.ts
new file mode 100644
index 0000000000..c46c07c1eb
--- /dev/null
+++ b/libs/design/toast/src/service/position.service.ts
@@ -0,0 +1,38 @@
+import { BreakpointObserver } from '@angular/cdk/layout';
+import {
+ Inject,
+ Injectable,
+} from '@angular/core';
+
+import { DaffBreakpoints } from '@daffodil/design';
+
+import {
+ DaffToastOptions,
+ DaffToastPosition,
+ DAFF_TOAST_OPTIONS,
+} from '../options/daff-toast-options';
+
+@Injectable()
+export class DaffToastPositionService {
+
+ constructor(@Inject(DAFF_TOAST_OPTIONS) private options: DaffToastOptions, private mediaQuery: BreakpointObserver) {
+ this._config = options.position;
+ }
+
+ private _config: DaffToastPosition;
+ private _position: DaffToastPosition;
+
+ get config(): DaffToastPosition {
+ return this.mediaQuery.isMatched(DaffBreakpoints.MOBILE)
+ ? this._position ?? this._config
+ : { vertical: 'bottom', horizontal: 'center' };
+ }
+
+ set config(val: DaffToastPosition) {
+ this._config = val;
+ }
+
+ setPosition(position: DaffToastPosition) {
+ this._position = position;
+ }
+}
diff --git a/libs/design/toast/src/service/toast.service.spec.ts b/libs/design/toast/src/service/toast.service.spec.ts
new file mode 100644
index 0000000000..5dd9d2e4af
--- /dev/null
+++ b/libs/design/toast/src/service/toast.service.spec.ts
@@ -0,0 +1,111 @@
+import { BreakpointObserver } from '@angular/cdk/layout';
+import {
+ Overlay,
+ OverlayModule,
+ OverlayRef,
+} from '@angular/cdk/overlay';
+import { PortalModule } from '@angular/cdk/portal';
+import { TestBed } from '@angular/core/testing';
+import { NoopAnimationsModule } from '@angular/platform-browser/animations';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+
+import {
+ DaffFocusStackService,
+ DaffPrefixSuffixModule,
+} from '@daffodil/design';
+import { DaffButtonModule } from '@daffodil/design/button';
+import { DaffToast } from '@daffodil/design/toast';
+
+import { DaffToastPositionService } from './position.service';
+import { DaffToastService } from './toast.service';
+import { DaffToastTemplateComponent } from '../toast/toast-template.component';
+import { DaffToastComponent } from '../toast/toast.component';
+import { DaffToastActionsDirective } from '../toast-actions/toast-actions.directive';
+import { DaffToastMessageDirective } from '../toast-message/toast-message.directive';
+import { DaffToastTitleDirective } from '../toast-title/toast-title.directive';
+
+describe('@daffodil/design/toast | DaffToastService', () => {
+ let service: DaffToastService;
+ let mockOverlayRef: jasmine.SpyObj;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ DaffPrefixSuffixModule,
+ DaffButtonModule,
+ FontAwesomeModule,
+ PortalModule,
+ OverlayModule,
+ NoopAnimationsModule,
+ ],
+ providers: [
+ DaffToastPositionService,
+ ],
+ declarations: [
+ DaffToastComponent,
+ DaffToastActionsDirective,
+ DaffToastTitleDirective,
+ DaffToastMessageDirective,
+ DaffToastTemplateComponent,
+ ],
+ });
+
+ const overlay = TestBed.inject(Overlay);
+ mockOverlayRef = jasmine.createSpyObj('OverlayRef', {
+ attach: {
+ instance: { items: []},
+ destroy: () => {},
+ },
+ dispose: null,
+ });
+ spyOn(overlay, 'create').and.returnValue(mockOverlayRef);
+
+ service = new DaffToastService(
+ overlay,
+ {
+ position: {
+ horizontal: 'right',
+ vertical: 'bottom',
+ },
+ useParent: false,
+ },
+ null,
+ TestBed.inject(BreakpointObserver),
+ TestBed.inject(DaffToastPositionService),
+ TestBed.inject(DaffFocusStackService),
+ );
+ });
+
+ it('should create', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('open', () => {
+ let result: DaffToast;
+
+ beforeEach(() => {
+ result = service.open({
+ title: 'title',
+ });
+ });
+
+ it('should create an overlay with the toast template component', () => {
+ expect(mockOverlayRef.attach).toHaveBeenCalledWith(jasmine.objectContaining({ component: DaffToastTemplateComponent }));
+ });
+
+ it('should return a toast capable of dismissing the toast', () => {
+ result.dismiss();
+ expect(() => service.close(result)).toThrowError('The Toast that you are trying to remove does not exist.');
+ });
+ });
+
+ describe('close', () => {
+ it('should error when trying to close a toast that has not been opened', () => {
+ expect(() => service.close({
+ title: 'title',
+ dismiss: null,
+ dismissalStream: null,
+ })).toThrowError('The Toast that you are trying to remove does not exist.');
+ });
+ });
+});
diff --git a/libs/design/toast/src/service/toast.service.ts b/libs/design/toast/src/service/toast.service.ts
new file mode 100644
index 0000000000..540775a283
--- /dev/null
+++ b/libs/design/toast/src/service/toast.service.ts
@@ -0,0 +1,176 @@
+import { BreakpointObserver } from '@angular/cdk/layout';
+import {
+ Overlay,
+ OverlayRef,
+} from '@angular/cdk/overlay';
+import { ComponentPortal } from '@angular/cdk/portal';
+import {
+ ComponentRef,
+ EventEmitter,
+ Inject,
+ Injectable,
+ OnDestroy,
+ Optional,
+ SkipSelf,
+} from '@angular/core';
+import {
+ EMPTY,
+ interval,
+ merge,
+ of,
+ Subscription,
+} from 'rxjs';
+import {
+ delay,
+ filter,
+ map,
+ take,
+ tap,
+} from 'rxjs/operators';
+
+import {
+ DaffBreakpoints,
+ DaffFocusStackService,
+} from '@daffodil/design';
+
+import { daffToastChangesFocus } from './changes-focus';
+import { createPositionStrategy } from './position-strategy';
+import { DaffToastPositionService } from './position.service';
+import {
+ DaffToast,
+ DaffToastData,
+} from '../interfaces/toast';
+import {
+ DAFF_TOAST_OPTIONS,
+ DaffToastOptions,
+} from '../options/daff-toast-options';
+import {
+ daffDefaultToastConfiguration,
+ DaffToastConfiguration,
+} from '../toast/toast-config';
+import { DaffToastTemplateComponent } from '../toast/toast-template.component';
+import { DaffToastModule } from '../toast.module';
+
+@Injectable({ providedIn: DaffToastModule })
+export class DaffToastService implements OnDestroy {
+
+ private _sub: Subscription;
+
+ private _toasts: DaffToast[] = [];
+
+ private _overlayRef?: OverlayRef;
+
+ private _template?: ComponentRef;
+
+ constructor(
+ private overlay: Overlay,
+ @Inject(DAFF_TOAST_OPTIONS) private options: DaffToastOptions,
+ @Optional() @SkipSelf() private _parentToast: DaffToastService,
+ private mediaQuery: BreakpointObserver,
+ private toastPosition: DaffToastPositionService,
+ private focusStack: DaffFocusStackService,
+ ) {
+ this._sub = this.mediaQuery.observe(DaffBreakpoints.MOBILE).pipe(
+ filter(() => this._overlayRef !== undefined),
+ map((position) => createPositionStrategy(this.toastPosition.config)),
+ tap((strategy) => this._overlayRef.updatePositionStrategy(strategy)),
+ ).subscribe();
+ }
+
+ ngOnDestroy(): void {
+ this._sub.unsubscribe();
+ }
+
+ private _attachToastTemplate(
+ overlayRef: OverlayRef,
+ ): ComponentRef {
+ const template = overlayRef.attach(new ComponentPortal(DaffToastTemplateComponent));
+ return template;
+ }
+
+ private _createOverlayRef(): OverlayRef {
+ return this.overlay.create({
+ hasBackdrop: false,
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
+ positionStrategy: createPositionStrategy(this.toastPosition.config),
+ });
+ }
+
+ open(
+ toast: DaffToastData,
+ configuration?: Partial,
+ ): DaffToast {
+ if(this._parentToast && this.options.useParent) {
+ return this._parentToast.open(toast, configuration);
+ }
+
+ const config: DaffToastConfiguration = {
+ ...daffDefaultToastConfiguration,
+ // sets the default duration to 5000ms if a toast does not have actions
+ duration: toast.actions?.length > 0 ? undefined : 5000,
+ ...configuration,
+ };
+ if(this._toasts.length === 0) {
+ this._overlayRef = this._createOverlayRef();
+ this._template = this._attachToastTemplate(this._overlayRef);
+ }
+ const dismissEvent = new EventEmitter();
+ const _toastPlus: DaffToast = {
+ dismissible: true,
+ ...toast,
+ dismiss: () => {
+ dismissEvent.emit();
+ },
+ dismissalStream: merge(
+ config.duration ? of(undefined).pipe(delay(config.duration)) : EMPTY,
+ dismissEvent,
+ ).pipe(
+ take(1),
+ ),
+ };
+
+ _toastPlus.dismissalStream.subscribe(() => {
+ this.close(_toastPlus);
+ });
+
+ this._toasts = [
+ _toastPlus,
+ ...this._toasts,
+ ];
+
+ this._template.instance.items = this._toasts;
+
+ return _toastPlus;
+ }
+
+ close(toast: DaffToast): void {
+ if(this._parentToast && this.options.useParent) {
+ this._parentToast.close(toast);
+ return;
+ }
+
+ if(daffToastChangesFocus(toast)) {
+ this.focusStack.pop();
+ }
+
+ const index = this._toasts.indexOf(toast);
+ if (index === -1) {
+ throw new Error(
+ 'The Toast that you are trying to remove does not exist.',
+ );
+ }
+
+ this._toasts = this._toasts.filter(m => m !== toast);
+ this._template.instance.items = [...this._toasts];
+
+ // This currently overrides the ":leave" animation as we currently
+ // remove the animating element immediately after there are no more toasts,
+ // without waiting for the animation to complete.
+ if(this._toasts.length === 0) {
+ this._overlayRef.dispose();
+ this._template.destroy();
+ this._overlayRef = undefined;
+ this._template = undefined;
+ }
+ }
+}
diff --git a/libs/design/toast/src/toast-actions/toast-actions.directive.spec.ts b/libs/design/toast/src/toast-actions/toast-actions.directive.spec.ts
new file mode 100644
index 0000000000..7e613342fd
--- /dev/null
+++ b/libs/design/toast/src/toast-actions/toast-actions.directive.spec.ts
@@ -0,0 +1,52 @@
+import {
+ Component,
+ DebugElement,
+} from '@angular/core';
+import {
+ waitForAsync,
+ ComponentFixture,
+ TestBed,
+} from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { DaffToastActionsDirective } from './toast-actions.directive';
+
+@Component({
+ template: `
+
+ `,
+})
+class WrapperComponent {}
+
+describe('DaffToastActionsDirective', () => {
+ let wrapper: WrapperComponent;
+ let de: DebugElement;
+ let fixture: ComponentFixture;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ DaffToastActionsDirective,
+ WrapperComponent,
+ ],
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(WrapperComponent);
+ wrapper = fixture.componentInstance;
+ de = fixture.debugElement.query(By.css('[daffToastActions]'));
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(wrapper).toBeTruthy();
+ });
+
+ describe('[daffToastActions]', () => {
+ it('should add a class of `daff-toast__actions` to its host element', () => {
+ expect(de.nativeElement.classList.contains('daff-toast__actions')).toEqual(true);
+ });
+ });
+});
diff --git a/libs/design/toast/src/toast-actions/toast-actions.directive.ts b/libs/design/toast/src/toast-actions/toast-actions.directive.ts
new file mode 100644
index 0000000000..1aa5a6a5d2
--- /dev/null
+++ b/libs/design/toast/src/toast-actions/toast-actions.directive.ts
@@ -0,0 +1,13 @@
+import {
+ Directive,
+ HostBinding,
+} from '@angular/core';
+
+@Directive({
+ selector: '[daffToastActions]',
+})
+
+export class DaffToastActionsDirective {
+
+ @HostBinding('class.daff-toast__actions') class = true;
+}
diff --git a/libs/design/toast/src/toast-message/toast-message.directive.spec.ts b/libs/design/toast/src/toast-message/toast-message.directive.spec.ts
new file mode 100644
index 0000000000..e59919d2a6
--- /dev/null
+++ b/libs/design/toast/src/toast-message/toast-message.directive.spec.ts
@@ -0,0 +1,52 @@
+import {
+ Component,
+ DebugElement,
+} from '@angular/core';
+import {
+ waitForAsync,
+ ComponentFixture,
+ TestBed,
+} from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+
+import { DaffToastMessageDirective } from './toast-message.directive';
+
+@Component({
+ template: `
+