diff --git a/apps/design-land/src/app/app.module.ts b/apps/design-land/src/app/app.module.ts index 40f47cf614..184398a1a2 100644 --- a/apps/design-land/src/app/app.module.ts +++ b/apps/design-land/src/app/app.module.ts @@ -13,7 +13,7 @@ import { DaffButtonModule } from '@daffodil/design/button'; import { DaffLinkSetModule } from '@daffodil/design/link-set'; import { DaffNavbarModule } from '@daffodil/design/navbar'; import { DaffSidebarModule } from '@daffodil/design/sidebar'; -import { DaffToastModule } from '@daffodil/design/toast'; +import { daffProvideToast } from '@daffodil/design/toast'; import { DaffThemeSwitchButtonModule } from '@daffodil/theme-switch'; import { DesignLandAppRoutingModule } from './app-routing.module'; @@ -41,11 +41,11 @@ import { DesignLandTemplateModule } from './core/template/template.module'; FontAwesomeModule, DesignLandNavModule, DesignLandTemplateModule, - DaffToastModule, ], providers: [ DAFF_THEME_INITIALIZER, provideHttpClient(withInterceptorsFromDi()), + daffProvideToast(), ], }) export class AppModule { } diff --git a/apps/design-land/src/app/toast/toast.module.ts b/apps/design-land/src/app/toast/toast.module.ts index 35b22d28c7..c9e35e68f0 100644 --- a/apps/design-land/src/app/toast/toast.module.ts +++ b/apps/design-land/src/app/toast/toast.module.ts @@ -3,7 +3,6 @@ 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'; @@ -22,7 +21,6 @@ import { DesignLandExampleViewerModule } from '../core/code-preview/container/ex DesignLandArticleEncapsulatedModule, DaffArticleModule, - DaffToastModule, ], }) export class DesignLandToastModule {} diff --git a/libs/design/toast/README.md b/libs/design/toast/README.md index f3931beb93..40f91ab337 100644 --- a/libs/design/toast/README.md +++ b/libs/design/toast/README.md @@ -4,10 +4,23 @@ Toasts are small messages designed to mimic push notifications. They are used to ## 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](/libs/design/notification/README.md) component. -### Basic Toast - - - +## Basic Toast + + +## Setting up the component +`daffProviderToast()` should be added as a provider either in your application's root component for global use or in a specific feature component. + +```ts +import { daffProvideToast } from '@daffodil/design/toast'; + +@NgModule({ + providers: [ + daffProvideToast(), + ] +)} + +export class AppModule {} +``` ### Configurations Toast can be configured by using the `DaffToastService`. @@ -15,31 +28,93 @@ 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, - }); +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; + +import { + DaffToast + DaffToastService, +} from '@daffodil/design/toast'; + +@Component({ + selector: 'custom-toast', + templateUrl: './custom-toast.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, +}) +export class CustomToastComponent { + 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.', + }, + { + 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 }, - ] - }); +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + OnInit, +} from '@angular/core'; + +import { DAFF_BUTTON_COMPONENTS } from '@daffodil/design/button'; +import { + DaffToast, + DaffToastAction, + DaffToastService, +} from '@daffodil/design/toast'; + +@Component({ + selector: 'action-toast', + templateUrl: './action-toast.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + DAFF_BUTTON_COMPONENTS, + ], +}) +export class ActionToastComponent implements OnInit { + private toast: DaffToast; + + constructor(private toastService: DaffToastService) {} + + update = new EventEmitter(); + + closeToast = new EventEmitter(); + + 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 }, + ] + }); + } + + ngOnInit() { + this.update.subscribe(() => { + }); + + this.closeToast.subscribe(() => { + this.toastService.close(this.toast); + }); + } } ``` @@ -55,10 +130,10 @@ The following configurations are available in the `DaffToastService`: The `actions` configurations are based on the properties of the `DaffButtonComponent` (view [Button Documentation](/libs/design/button/README.md)) with the addition of `data` and `eventEmitter`. -### Dismissal +## Dismissal A toast can be dismissed via a timed duration, a close button, or the `ESC` key. -##### Timed duration +### 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`. @@ -66,24 +141,24 @@ By default, a toast without actions will be dismissed after `5000ms`. This can b #### Toast with custom duration -##### Close button +### Close button The close button is shown by default but can be hidden by setting `dismissible: false` through the `DaffToastService`. -##### Escape Key +### Escape Key A toast can be dismissed by using the `ESC` key if it has actions and is focus trapped. -### Stacking +## 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 +## Statuses The status color of a toast can be updated by using the `status` property. Supported statuses: `warn | danger | success` -#### Toast with statuses +### Toast with statuses -### Positions +## Positions | Property | Value | Default | | ------------ | ------------------------ | ------- | @@ -106,10 +181,10 @@ providers: [ The position of a toast on a mobile device will always be on the bottom center. -#### Toast with configurable positions +### Toast with configurable positions -### Accessibility +## 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/src/toast-status/toast-status.component.html b/libs/design/toast/examples/src/toast-status/toast-status.component.html index db5b3db778..64ec395255 100644 --- a/libs/design/toast/examples/src/toast-status/toast-status.component.html +++ b/libs/design/toast/examples/src/toast-status/toast-status.component.html @@ -3,5 +3,5 @@ \ 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 index 0bfe08e5a7..c8d3128f50 100644 --- a/libs/design/toast/examples/src/toast-status/toast-status.component.ts +++ b/libs/design/toast/examples/src/toast-status/toast-status.component.ts @@ -19,8 +19,9 @@ import { } from '@daffodil/design/toast'; const status: Record = { - error: { + danger: { title: 'Server error', + message: 'There is a server error.', }, success: { title: 'Update complete', @@ -59,7 +60,7 @@ export class ToastStatusComponent { ...status[this.statusControl.value], }, { - duration: this.statusControl.value === 'error' ? undefined : 5000, + duration: this.statusControl.value === 'danger' ? undefined : 5000, }, ); } diff --git a/libs/design/toast/src/public_api.ts b/libs/design/toast/src/public_api.ts index df18203755..70fa56f105 100644 --- a/libs/design/toast/src/public_api.ts +++ b/libs/design/toast/src/public_api.ts @@ -18,3 +18,4 @@ 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'; +export * from './toast/toast-provider'; diff --git a/libs/design/toast/src/service/toast.service.spec.ts b/libs/design/toast/src/service/toast.service.spec.ts index b17dc0cde4..b9e0087607 100644 --- a/libs/design/toast/src/service/toast.service.spec.ts +++ b/libs/design/toast/src/service/toast.service.spec.ts @@ -13,7 +13,6 @@ import { DaffFocusStackService, DaffPrefixSuffixModule, } from '@daffodil/design'; -import { DaffButtonModule } from '@daffodil/design/button'; import { DaffToastPositionService } from './position.service'; import { DaffToastService } from './toast.service'; @@ -32,22 +31,20 @@ describe('@daffodil/design/toast | DaffToastService', () => { TestBed.configureTestingModule({ imports: [ DaffPrefixSuffixModule, - DaffButtonModule, FontAwesomeModule, PortalModule, OverlayModule, NoopAnimationsModule, - ], - providers: [ - DaffToastPositionService, - ], - declarations: [ + DaffToastComponent, DaffToastActionsDirective, DaffToastTitleDirective, DaffToastMessageDirective, DaffToastTemplateComponent, ], + providers: [ + DaffToastPositionService, + ], }); const overlay = TestBed.inject(Overlay); @@ -73,6 +70,7 @@ describe('@daffodil/design/toast | DaffToastService', () => { TestBed.inject(BreakpointObserver), TestBed.inject(DaffToastPositionService), TestBed.inject(DaffFocusStackService), + null, ); }); diff --git a/libs/design/toast/src/service/toast.service.ts b/libs/design/toast/src/service/toast.service.ts index 268339dade..45003bc8e4 100644 --- a/libs/design/toast/src/service/toast.service.ts +++ b/libs/design/toast/src/service/toast.service.ts @@ -9,13 +9,13 @@ import { EventEmitter, Inject, Injectable, + Injector, OnDestroy, Optional, SkipSelf, } from '@angular/core'; import { EMPTY, - interval, merge, of, Subscription, @@ -49,9 +49,8 @@ import { DaffToastConfiguration, } from '../toast/toast-config'; import { DaffToastTemplateComponent } from '../toast/toast-template.component'; -import { DaffToastModule } from '../toast.module'; -@Injectable({ providedIn: DaffToastModule }) +@Injectable() export class DaffToastService implements OnDestroy { private _sub: Subscription; @@ -69,6 +68,7 @@ export class DaffToastService implements OnDestroy { private mediaQuery: BreakpointObserver, private toastPosition: DaffToastPositionService, private focusStack: DaffFocusStackService, + private injector: Injector, ) { this._sub = this.mediaQuery.observe(DaffBreakpoints.MOBILE).pipe( filter(() => this._overlayRef !== undefined), @@ -84,7 +84,7 @@ export class DaffToastService implements OnDestroy { private _attachToastTemplate( overlayRef: OverlayRef, ): ComponentRef { - const template = overlayRef.attach(new ComponentPortal(DaffToastTemplateComponent)); + const template = overlayRef.attach(new ComponentPortal(DaffToastTemplateComponent, null, this.injector)); return template; } 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 index 7e613342fd..1ff9a12e32 100644 --- a/libs/design/toast/src/toast-actions/toast-actions.directive.spec.ts +++ b/libs/design/toast/src/toast-actions/toast-actions.directive.spec.ts @@ -15,6 +15,10 @@ import { DaffToastActionsDirective } from './toast-actions.directive'; template: `
`, + standalone: true, + imports: [ + DaffToastActionsDirective, + ], }) class WrapperComponent {} @@ -25,8 +29,7 @@ describe('DaffToastActionsDirective', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DaffToastActionsDirective, + imports: [ WrapperComponent, ], }) diff --git a/libs/design/toast/src/toast-actions/toast-actions.directive.ts b/libs/design/toast/src/toast-actions/toast-actions.directive.ts index 1aa5a6a5d2..8faf3bc4c2 100644 --- a/libs/design/toast/src/toast-actions/toast-actions.directive.ts +++ b/libs/design/toast/src/toast-actions/toast-actions.directive.ts @@ -5,6 +5,7 @@ import { @Directive({ selector: '[daffToastActions]', + standalone: true, }) export class DaffToastActionsDirective { 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 index e59919d2a6..bede1d47eb 100644 --- a/libs/design/toast/src/toast-message/toast-message.directive.spec.ts +++ b/libs/design/toast/src/toast-message/toast-message.directive.spec.ts @@ -15,6 +15,10 @@ import { DaffToastMessageDirective } from './toast-message.directive'; template: `
Message
`, + standalone: true, + imports: [ + DaffToastMessageDirective, + ], }) class WrapperComponent {} @@ -25,8 +29,7 @@ describe('DaffToastMessageDirective', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DaffToastMessageDirective, + imports: [ WrapperComponent, ], }) diff --git a/libs/design/toast/src/toast-message/toast-message.directive.ts b/libs/design/toast/src/toast-message/toast-message.directive.ts index 1b21661e2a..4ec3b43af7 100644 --- a/libs/design/toast/src/toast-message/toast-message.directive.ts +++ b/libs/design/toast/src/toast-message/toast-message.directive.ts @@ -5,6 +5,7 @@ import { @Directive({ selector: '[daffToastMessage]', + standalone: true, }) export class DaffToastMessageDirective { diff --git a/libs/design/toast/src/toast-theme.scss b/libs/design/toast/src/toast-theme.scss index 975b989b10..0a030a77ba 100644 --- a/libs/design/toast/src/toast-theme.scss +++ b/libs/design/toast/src/toast-theme.scss @@ -47,7 +47,7 @@ } } - &.daff-error { + &.daff-danger { background: theming.daff-color(theming.$daff-red, 10); color: $black; diff --git a/libs/design/toast/src/toast-title/toast-title.directive.spec.ts b/libs/design/toast/src/toast-title/toast-title.directive.spec.ts index 41a7d634f6..938d8d300b 100644 --- a/libs/design/toast/src/toast-title/toast-title.directive.spec.ts +++ b/libs/design/toast/src/toast-title/toast-title.directive.spec.ts @@ -15,6 +15,10 @@ import { DaffToastTitleDirective } from './toast-title.directive'; template: `

Title

`, + standalone: true, + imports: [ + DaffToastTitleDirective, + ], }) class WrapperComponent {} @@ -25,8 +29,7 @@ describe('DaffToastTitleDirective', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DaffToastTitleDirective, + imports: [ WrapperComponent, ], }) diff --git a/libs/design/toast/src/toast-title/toast-title.directive.ts b/libs/design/toast/src/toast-title/toast-title.directive.ts index 6e3877d8c0..f002c2cfbb 100644 --- a/libs/design/toast/src/toast-title/toast-title.directive.ts +++ b/libs/design/toast/src/toast-title/toast-title.directive.ts @@ -5,6 +5,7 @@ import { @Directive({ selector: '[daffToastTitle]', + standalone: true, }) export class DaffToastTitleDirective { diff --git a/libs/design/toast/src/toast.module.ts b/libs/design/toast/src/toast.module.ts index d5d9c36d06..8fdb503648 100644 --- a/libs/design/toast/src/toast.module.ts +++ b/libs/design/toast/src/toast.module.ts @@ -8,13 +8,14 @@ import { DaffPrefixSuffixModule } from '@daffodil/design'; import { DaffButtonModule } from '@daffodil/design/button'; import { DaffToastPositionService } from './service/position.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'; - +/** + * @deprecated in favor of {@link daffProvideToast} + */ @NgModule({ imports: [ CommonModule, @@ -23,13 +24,10 @@ import { DaffToastTitleDirective } from './toast-title/toast-title.directive'; FontAwesomeModule, PortalModule, OverlayModule, - ], - declarations: [ DaffToastComponent, DaffToastActionsDirective, DaffToastTitleDirective, DaffToastMessageDirective, - DaffToastTemplateComponent, ], exports: [ DaffToastComponent, diff --git a/libs/design/toast/src/toast/toast-provider.ts b/libs/design/toast/src/toast/toast-provider.ts new file mode 100644 index 0000000000..3fe81c8fe8 --- /dev/null +++ b/libs/design/toast/src/toast/toast-provider.ts @@ -0,0 +1,9 @@ +import { Provider } from '@angular/core'; + +import { DaffToastPositionService } from '../service/position.service'; +import { DaffToastService } from '../service/toast.service'; + +export const daffProvideToast = (): Provider[] => [ + DaffToastService, + DaffToastPositionService, +]; diff --git a/libs/design/toast/src/toast/toast-template.component.ts b/libs/design/toast/src/toast/toast-template.component.ts index c26d3f55f5..d41d861a99 100644 --- a/libs/design/toast/src/toast/toast-template.component.ts +++ b/libs/design/toast/src/toast/toast-template.component.ts @@ -4,6 +4,15 @@ import { transition, trigger, } from '@angular/animations'; +import { + NgFor, + NgIf, + NgSwitch, + NgSwitchCase, + NgSwitchDefault, + NgTemplateOutlet, + SlicePipe, +} from '@angular/common'; import { Input, ChangeDetectionStrategy, @@ -13,14 +22,21 @@ import { Output, EventEmitter, } from '@angular/core'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { DAFF_BUTTON_COMPONENTS } from '@daffodil/design/button'; + +import { DaffToastComponent } from './toast.component'; import { DaffToast } from '../interfaces/toast'; import { DaffToastOptions, DAFF_TOAST_OPTIONS, } from '../options/daff-toast-options'; import { DaffToastPositionService } from '../service/position.service'; +import { DaffToastActionsDirective } from '../toast-actions/toast-actions.directive'; +import { DaffToastMessageDirective } from '../toast-message/toast-message.directive'; +import { DaffToastTitleDirective } from '../toast-title/toast-title.directive'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,7 +47,7 @@ import { DaffToastPositionService } from '../service/position.service'; [status]="item.status ?? null" (closeToast)="item.dismiss()" [@slideIn]="slideAnimation" - [attr.role]="item.actions ? 'alertdialog' : undefined" + [attr.role]="item.actions ? 'alertdialog' : 'status'" [attr.aria-labelledby]="item.actions ? item.title : undefined" [attr.aria-describedby]="item.actions ? item.message : undefined">
{{ item.title }}
@@ -109,6 +125,22 @@ import { DaffToastPositionService } from '../service/position.service'; } }), ]), ], + standalone: true, + imports: [ + DAFF_BUTTON_COMPONENTS, + DaffToastComponent, + DaffToastActionsDirective, + DaffToastTitleDirective, + DaffToastMessageDirective, + FaIconComponent, + NgSwitch, + NgFor, + NgSwitchCase, + NgSwitchDefault, + NgIf, + SlicePipe, + NgTemplateOutlet, + ], }) export class DaffToastTemplateComponent { faTimes = faTimes; diff --git a/libs/design/toast/src/toast/toast.component.spec.ts b/libs/design/toast/src/toast/toast.component.spec.ts index 381574aa64..b0605d949c 100644 --- a/libs/design/toast/src/toast/toast.component.spec.ts +++ b/libs/design/toast/src/toast/toast.component.spec.ts @@ -22,6 +22,10 @@ import { DaffToast } from '../interfaces/toast'; [toast]="toast" > `, + standalone: true, + imports: [ + DaffToastComponent, + ], }) class WrapperComponent { @@ -37,8 +41,7 @@ describe('DaffToastComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ - DaffToastComponent, + imports: [ WrapperComponent, ], }) @@ -76,8 +79,4 @@ describe('DaffToastComponent', () => { expect(de.nativeElement.classList.contains('daff-warn')).toEqual(true); }); - - it('should have a role of status', () => { - expect(component.role).toBe('status'); - }); }); diff --git a/libs/design/toast/src/toast/toast.component.ts b/libs/design/toast/src/toast/toast.component.ts index 1eb55f303d..93d974e685 100644 --- a/libs/design/toast/src/toast/toast.component.ts +++ b/libs/design/toast/src/toast/toast.component.ts @@ -2,6 +2,7 @@ import { ConfigurableFocusTrap, ConfigurableFocusTrapFactory, } from '@angular/cdk/a11y'; +import { NgIf } from '@angular/common'; import { Component, ElementRef, @@ -45,14 +46,15 @@ import { DaffToastActionsDirective } from '../toast-actions/toast-actions.direct ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + NgIf, + ], }) export class DaffToastComponent implements DaffPrefixable, AfterContentInit, AfterViewInit, OnDestroy { /** @docs-private */ @HostBinding('class.daff-toast') class = true; - /** @docs-private */ - @HostBinding('attr.role') role = 'status'; - @ContentChild(DaffToastActionsDirective) _actions: DaffToastActionsDirective;