diff --git a/ngx-fudis/projects/dev/src/app/app.component.html b/ngx-fudis/projects/dev/src/app/app.component.html index 1a745a373..191aaf1c8 100644 --- a/ngx-fudis/projects/dev/src/app/app.component.html +++ b/ngx-fudis/projects/dev/src/app/app.component.html @@ -1,6 +1,6 @@
- + @@ -24,7 +24,7 @@ - + This value comes from the closed dialog input: diff --git a/ngx-fudis/projects/dev/src/app/app.component.ts b/ngx-fudis/projects/dev/src/app/app.component.ts index e385190d0..d8da7f6ec 100644 --- a/ngx-fudis/projects/dev/src/app/app.component.ts +++ b/ngx-fudis/projects/dev/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { TranslocoService } from '@ngneat/transloco'; import { - // FudisAlertService, + FudisAlertService, FudisDialogService, FudisGridService, FudisTranslationService, @@ -10,7 +10,7 @@ import { } from 'ngx-fudis'; import { DOCUMENT } from '@angular/common'; import { FudisSelectOption, FudisCheckboxOption } from 'dist/ngx-fudis/lib/types/forms'; -// import { FudisAlert } from 'dist/ngx-fudis/lib/types/miscellaneous'; +import { FudisAlert } from 'dist/ngx-fudis/lib/types/miscellaneous'; import { DialogTestContentComponent } from './dialog-test/dialog-test-content/dialog-test-content.component'; import { FudisGridAlign } from 'projects/ngx-fudis/src/lib/types/grid'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; @@ -28,7 +28,7 @@ export class AppComponent implements OnInit { private _translocoService: TranslocoService, private _gridService: FudisGridService, private _fudisLanguage: FudisTranslationService, - // private _alertService: FudisAlertService, + private _alertService: FudisAlertService, private _errorSummaryService: FudisErrorSummaryService, private _breakpointService: FudisBreakpointService, ) { @@ -93,16 +93,14 @@ export class AppComponent implements OnInit { this.getApplicationFontSize(); } - // triggerAlert(): void { - // const newAlert: FudisAlert = { - // message: 'Something dangerous MIGHT happen.', - // type: 'warning', - // id: 'my-own-id-3', - // routerLinkUrl: '/', - // linkTitle: 'More info about this warning.', - // }; - // this._alertService.addAlert(newAlert); - // } + triggerAlert(): void { + const newAlert: FudisAlert = { + message: this._translocoService.selectTranslate('alertText'), + type: 'warning', + id: 'my-own-id-3', + }; + this._alertService.addAlert(newAlert); + } getApplicationFontSize(): void { this.fontSize = getComputedStyle( diff --git a/ngx-fudis/projects/dev/src/assets/i18n/en.json b/ngx-fudis/projects/dev/src/assets/i18n/en.json index c48233da1..c1aad6d7d 100644 --- a/ngx-fudis/projects/dev/src/assets/i18n/en.json +++ b/ngx-fudis/projects/dev/src/assets/i18n/en.json @@ -1,4 +1,5 @@ { + "alertText": "Something dangerous MIGHT happen", "label": "This is text input", "change_language": "Change language", "helpText": "This is English.", diff --git a/ngx-fudis/projects/dev/src/assets/i18n/fi.json b/ngx-fudis/projects/dev/src/assets/i18n/fi.json index 15bc87258..05b27b5a2 100644 --- a/ngx-fudis/projects/dev/src/assets/i18n/fi.json +++ b/ngx-fudis/projects/dev/src/assets/i18n/fi.json @@ -1,4 +1,5 @@ { + "alertText": "Jotain kamalaa voipi tapahtua", "label": "Tämä on tekstikenttä", "change_language": "Vaihda kieli", "helpText": "Kivaa jos näet minut suomeksi!", diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.html b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.html index 8bde6d6c4..620d3eff9 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.html +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.html @@ -1,18 +1,15 @@
- +
diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.spec.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.spec.ts index 7fdd5eda3..327879f86 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.spec.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.spec.ts @@ -1,31 +1,30 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MockComponent } from 'ng-mocks'; import { AlertGroupComponent } from './alert-group.component'; import { FudisDialogService } from '../../../services/dialog/dialog.service'; import { BodyTextComponent } from '../../typography/body-text/body-text.component'; import { FudisAlertService } from '../../../services/alert/alert.service'; -import { FudisAlert } from '../../../types/miscellaneous'; +import { FudisAlert, fudisAlertPositionArray } from '../../../types/miscellaneous'; import { AlertComponent } from '../alert/alert.component'; import { IconComponent } from '../../icon/icon.component'; import { getElement, sortClasses } from '../../../utilities/tests/utilities'; +import { BehaviorSubject } from 'rxjs'; +import { SimpleChange } from '@angular/core'; -// TODO: fix & refactor when these are again published - -describe.skip('AlertGroupComponent', () => { +describe('AlertGroupComponent', () => { let component: AlertGroupComponent; let fixture: ComponentFixture; let alertService: FudisAlertService; let dialogService: FudisDialogService; - beforeEach(() => { - TestBed.configureTestingModule({ + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [MatDialogModule], declarations: [ AlertGroupComponent, - BodyTextComponent, AlertComponent, + BodyTextComponent, MockComponent(IconComponent), ], providers: [ @@ -39,7 +38,10 @@ describe.skip('AlertGroupComponent', () => { useValue: [], }, ], - }); + }).compileComponents(); + + alertService = TestBed.inject(FudisAlertService); + dialogService = TestBed.inject(FudisDialogService); fixture = TestBed.createComponent(AlertGroupComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -50,55 +52,33 @@ describe.skip('AlertGroupComponent', () => { }); describe('Basic inputs', () => { - it('should have default CSS classes', () => { - const element = getElement(fixture, 'section'); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-alert-group fudis-alert-group__fixed'), - ); - }); - - it('should have absolute position', () => { - component.position = 'absolute'; - fixture.detectChanges(); - - const element = getElement(fixture, 'section'); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-alert-group fudis-alert-group__absolute'), - ); - }); - - it('should have static position', () => { - component.position = 'static'; - fixture.detectChanges(); - - const element = getElement(fixture, 'section'); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-alert-group fudis-alert-group__static'), - ); + it('should update CSS class according to position input', () => { + fudisAlertPositionArray.forEach((position) => { + const element = getElement(fixture, '.fudis-alert-group'); + fixture.componentRef.setInput('position', `${position}`); + fixture.detectChanges(); + + expect(sortClasses(element.className)).toEqual( + sortClasses(`fudis-alert-group fudis-alert-group__${position}`), + ); + }); }); }); describe('Functionality with services', () => { beforeEach(() => { - alertService = TestBed.inject(FudisAlertService); - dialogService = TestBed.inject(FudisDialogService); - - jest.spyOn(alertService, 'getAlertsSignal').mockImplementation(); - const firstAlert: FudisAlert = { - message: 'Test message', + message: new BehaviorSubject('Test message'), id: 'my-test-id-1', type: 'info', }; const secondAlert: FudisAlert = { - message: 'Second test message', + message: new BehaviorSubject('Second test message'), id: 'my-test-id-2', type: 'warning', }; + alertService.addAlert(firstAlert); alertService.addAlert(secondAlert); }); @@ -111,7 +91,7 @@ describe.skip('AlertGroupComponent', () => { expect(childAlerts.length).toEqual(2); }); - it('should have one alert as children after one is dismissed', () => { + it('should have one alert as child after one is dismissed', () => { alertService.dismissAlert('my-test-id-1'); fixture.detectChanges(); @@ -123,7 +103,7 @@ describe.skip('AlertGroupComponent', () => { }); it('should not be visible, if Dialog is open', () => { - dialogService.setDialogOpenSignal(true); + dialogService.setDialogOpenStatus(true); fixture.detectChanges(); @@ -132,7 +112,7 @@ describe.skip('AlertGroupComponent', () => { }); it('should be visible, if Dialog is closed', () => { - dialogService.setDialogOpenSignal(false); + dialogService.setDialogOpenStatus(false); fixture.detectChanges(); @@ -141,11 +121,14 @@ describe.skip('AlertGroupComponent', () => { }); it('should not be visible, if Dialog is not open and Alert Group is inside dialog', () => { - component.insideDialog = true; + dialogService.setDialogOpenStatus(false); - component.ngAfterViewInit(); + component.insideDialog = true; + component.ngOnChanges({ + insideDialog: new SimpleChange('false', component.insideDialog, true), + }); - fixture.detectChanges(); + fixture.autoDetectChanges(); expect(fixture.nativeElement.querySelector('section')).toBeNull(); expect(component.getVisibleStatus()).toEqual(false); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.ts index be4547827..9e8a75814 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.component.ts @@ -1,82 +1,85 @@ -import { ChangeDetectorRef, Component, Input, AfterViewInit, Signal, effect } from '@angular/core'; - -import { FudisAlertElement } from '../../../types/miscellaneous'; +import { Component, Input, effect, OnChanges, ChangeDetectionStrategy } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { + FudisAlertElement, + FudisAlertPosition, + FudisComponentChanges, +} from '../../../types/miscellaneous'; import { FudisTranslationService } from '../../../services/translation/translation.service'; import { FudisAlertService } from '../../../services/alert/alert.service'; import { FudisDialogService } from '../../../services/dialog/dialog.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'fudis-alert-group', templateUrl: './alert-group.component.html', styleUrls: ['./alert-group.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AlertGroupComponent implements AfterViewInit { +export class AlertGroupComponent implements OnChanges { constructor( - private _alertService: FudisAlertService, + protected _alertService: FudisAlertService, private _translationService: FudisTranslationService, - private readonly _changeDetectorRef: ChangeDetectorRef, private _dialogService: FudisDialogService, ) { - effect(() => { - this._alertList = this._alertService.getAlertsSignal(); - // TODO: To Observable - this._alertGroupLabel = this._translationService.getTranslations()().ALERT.HEADING_LABEL; + this._dialogService + .getDialogOpenStatus() + .pipe(takeUntilDestroyed()) + .subscribe((value) => { + this._setVisibility(value); + }); - this._dialogStatus = this._dialogService.getDialogOpenSignal()(); - - this._setVisibility(); + effect(() => { + this._alertGroupLabel.next(this._translationService.getTranslations()().ALERT.HEADING_LABEL); }); } /** - * CSS position of alerts. Defaults to fixed. + * CSS position of alerts */ - @Input() position: 'fixed' | 'absolute' | 'static' = 'fixed'; + @Input() position: FudisAlertPosition = 'fixed'; /** - * Boolean to determine if Alert Group is used as child in Fudis Dialog. + * Boolean to determine if Alert Group is used inside Fudis Dialog Component */ - @Input() insideDialog: boolean = false; + @Input() insideDialog: boolean; /** - * List of Alerts fetched from service + * List of Alerts fetched from Alert Service */ - protected _alertList: Signal; + protected _alertList = new BehaviorSubject([]); /** * Label for section element containing alerts */ - protected _alertGroupLabel: string; + protected _alertGroupLabel = new BehaviorSubject(''); /** - * Boolean to determine if Alert group is visible. Used with _dialogStatus boolean. + * Boolean to determine if Alert group is visible */ - protected _visible: boolean = false; + protected _visible = new BehaviorSubject(false); - /** - * Boolean from service to determine if Fudis Dialog is open. - */ - private _dialogStatus: boolean; - - ngAfterViewInit(): void { - this._setVisibility(); + ngOnChanges(changes: FudisComponentChanges): void { + if (changes.insideDialog?.currentValue !== changes.insideDialog?.previousValue) { + this._setVisibility(this._dialogService.getDialogOpenStatus().value); + } } /** - * Getter for visible status + * Get Alert Group's visible status */ public getVisibleStatus(): boolean { - return this._visible; + return this._visible.value; } /** - * Set visibility when Fudis Dialog is opened and closed. + * Set visibility when Fudis Dialog is opened and closed */ - private _setVisibility(): void { - if ((this._dialogStatus && this.insideDialog) || (!this._dialogStatus && !this.insideDialog)) { - this._visible = true; + private _setVisibility(dialogStatus: boolean): void { + if ((dialogStatus && this.insideDialog) || (!dialogStatus && !this.insideDialog)) { + this._visible.next(true); } else { - this._visible = false; + this._visible.next(false); } } } diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.stories.hidden b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.stories.hidden deleted file mode 100644 index a100790c1..000000000 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.stories.hidden +++ /dev/null @@ -1,167 +0,0 @@ -// // also exported from '@storybook/angular' if you can deal with breaking changes in 6.1 -// import { StoryFn, Meta, moduleMetadata } from '@storybook/angular'; -// import { AfterViewInit, Component, Signal, TemplateRef, ViewChild, effect } from '@angular/core'; -// import { RouterTestingModule } from '@angular/router/testing'; - -// import readme from '../readme.mdx'; -// import { AlertGroupComponent } from './alert-group.component'; - -// import { FudisAlert, FudisAlertElement } from '../../../types/miscellaneous'; -// import { FudisDialogService } from '../../../services/dialog/dialog.service'; -// import { FudisAlertService } from '../../../services/alert/alert.service'; - -// @Component({ -// selector: 'example-add-alerts', -// template: `
-// -// -// -// -// -// -// -// -// -// -// -//
-// -// -// Small test dialog -// -// Some content -// -// -// -// -// -// `, -// }) -// class AddAlertsComponent implements AfterViewInit { -// constructor( -// private _dialog: FudisDialogService, -// private _alertService: FudisAlertService, -// ) { -// effect(() => { -// this._alerts = this._alertService.getAlertsSignal(); -// this._marginCounter = 2 + this._alerts().length * 2; -// }); -// } - -// @ViewChild('exampleDialogTemplate', { static: true }) templateRef: TemplateRef; - -// protected _marginCounter = 2; - -// protected _alerts: Signal; - -// openDialog(): void { -// this._dialog.open(this.templateRef); -// } - -// addDanger(): void { -// const newAlert: FudisAlert = { -// message: 'Something dangerous happened', -// type: 'danger', -// id: 'my-own-id-1', -// }; - -// this._alertService.addAlert(newAlert); -// } - -// addWarning(): void { -// const newAlert: FudisAlert = { -// message: 'Something dangerous MIGHT happen', -// type: 'warning', -// id: 'my-own-id-2', -// }; - -// this._alertService.addAlert(newAlert); -// } - -// addWarningWithLink(): void { -// const newAlert: FudisAlert = { -// message: 'Something dangerous MIGHT happen.', -// type: 'warning', -// id: 'my-own-id-3', -// routerLinkUrl: '/', -// linkTitle: 'More info about this warning.', -// }; - -// this._alertService.addAlert(newAlert); -// } - -// addSuccess(): void { -// const newAlert: FudisAlert = { -// message: 'Yippee Ki-Yay! You were successful!', -// type: 'success', -// id: 'my-own-id-4', -// }; - -// this._alertService.addAlert(newAlert); -// } - -// addInfo(): void { -// const newAlert: FudisAlert = { -// message: 'Nothing special here.', -// type: 'info', -// id: 'my-own-id-5', -// }; - -// this._alertService.addAlert(newAlert); -// } - -// addInfoWithLink(): void { -// const newAlert: FudisAlert = { -// message: 'Mostly neutral information here.', -// type: 'info', -// id: 'my-own-id-6', -// routerLinkUrl: '/', -// linkTitle: 'Additional information about this situation.', -// }; - -// this._alertService.addAlert(newAlert); -// } - -// dismissRandom(): void { -// const alerts = this._alertService.getAlertsSignal(); -// const alertsLength = alerts().length; - -// if (alertsLength > 0) { -// const random = Math.floor(Math.random() * alertsLength); -// this._alertService.dismissAlert(alerts()[random].id); -// } -// } - -// dismissAll(): void { -// this._alertService.dismissAll(); -// } - -// ngAfterViewInit(): void { -// this.addDanger(); -// this.addInfo(); -// this.addWarning(); -// this.addSuccess(); -// } -// } - -// export default { -// title: 'Components/Alert Group', -// component: AlertGroupComponent, -// decorators: [ -// moduleMetadata({ -// imports: [RouterTestingModule], -// declarations: [AddAlertsComponent], -// }), -// ], -// parameters: { -// docs: { -// page: readme, -// }, -// }, -// } as Meta; - -// const html = String.raw; - -// export const Example: StoryFn = () => ({ -// template: html` `, -// }); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.stories.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.stories.ts new file mode 100644 index 000000000..aa50f1866 --- /dev/null +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert-group/alert-group.stories.ts @@ -0,0 +1,162 @@ +import { StoryFn, Meta, moduleMetadata } from '@storybook/angular'; +import { AfterViewInit, Component, TemplateRef, ViewChild, effect } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AlertGroupComponent } from './alert-group.component'; +import { FudisAlert, FudisAlertElement } from '../../../types/miscellaneous'; +import { FudisDialogService } from '../../../services/dialog/dialog.service'; +import { FudisAlertService } from '../../../services/alert/alert.service'; +import { alertGroupExclude } from '../../../utilities/storybook'; +import docs from '../alert.docs.mdx'; + +@Component({ + selector: 'example-add-alerts', + template: `
+ + + + + + + + + +
+ + + Small test dialog + + Some content + + + + + + `, +}) +class AddAlertsComponent implements AfterViewInit { + constructor( + private _dialog: FudisDialogService, + private _alertService: FudisAlertService, + ) { + effect(() => { + this._alerts = this._alertService.alerts; + this._marginCounter = 2 + this._alerts.getValue().length * 2; + }); + } + + @ViewChild('exampleDialogTemplate', { static: true }) templateRef: TemplateRef; + + protected _marginCounter: number = 2; + + protected _alerts: BehaviorSubject; + + openDialog(): void { + this._dialog.open(this.templateRef); + } + + addDanger(): void { + const newAlert: FudisAlert = { + message: new BehaviorSubject('Something dangerous happened'), + type: 'danger', + id: 'my-own-id-1', + }; + + this._alertService.addAlert(newAlert); + } + + addWarning(): void { + const newAlert: FudisAlert = { + message: new BehaviorSubject('Something dangerous MIGHT happen'), + type: 'warning', + id: 'my-own-id-2', + }; + + this._alertService.addAlert(newAlert); + } + + alertWarningDemoLinkClick(): void { + alert('yikes!'); + } + + alertInfoDemoLinkClick(): void { + alert('Nothing really interesting here.'); + } + + addSuccess(): void { + const newAlert: FudisAlert = { + message: new BehaviorSubject('Yippee Ki-Yay! You were successful!'), + type: 'success', + id: 'my-own-id-4', + }; + + this._alertService.addAlert(newAlert); + } + + addInfo(): void { + const newAlert: FudisAlert = { + message: new BehaviorSubject('Nothing special here.'), + type: 'info', + id: 'my-own-id-5', + }; + + this._alertService.addAlert(newAlert); + } + + dismissRandom(): void { + const alerts = this._alertService.alerts.getValue(); + + if (alerts.length > 0) { + const random = Math.floor(Math.random() * alerts.length); + this._alertService.dismissAlert(alerts[random].id); + } + } + + dismissAll(): void { + this._alertService.dismissAll(); + } + + ngAfterViewInit(): void { + this.addDanger(); + this.addInfo(); + this.addWarning(); + this.addSuccess(); + } +} + +export default { + title: 'Components/Alert Group', + component: AlertGroupComponent, + decorators: [ + moduleMetadata({ + imports: [RouterTestingModule], + declarations: [AddAlertsComponent], + }), + ], + parameters: { + docs: { + page: docs, + }, + controls: { + exclude: alertGroupExclude, + }, + }, + argTypes: { + position: { + options: ['fixed', 'static', 'absolute'], + control: { type: 'radio' }, + }, + }, +} as Meta; + +const html = String.raw; + +const Template: StoryFn = (args: AlertGroupComponent) => ({ + props: args, + template: html` `, +}); + +export const Example = Template.bind({}); +Example.args = { + position: 'fixed', +}; diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert.docs.mdx b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert.docs.mdx new file mode 100644 index 000000000..073a09202 --- /dev/null +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert.docs.mdx @@ -0,0 +1,96 @@ +import { ArgTypes, Meta, Canvas, Source } from "@storybook/blocks"; +import { AlertGroupComponent } from "./alert-group/alert-group.component.ts"; + + + +# Alert Group + +**Please note, that Alert Group is not compatible with Sisu alert logic.** + +Alert Group Component displays list of toaster-like Alert Components with four variants: `success`, `info`, `warning` and `danger.` These variants are the same ones as with [Notification Component](/docs/components-notification--documentation). + +## Adding and Dismissing Alerts + +Alert Group listens to `FudisAlertService` where application or UI sends information about adding or dismissing alerts. + +### Add New Alert + +To add new alert, send an object of type `FudisAlert` to `FudisAlertService` using `addAlert()`. + +Note that `id` doesn't need to be unique for AlertService. + + + +### Dismiss Alert + +#### Dismiss By Id + +Previously sent alert can be dismissed with `dismissAlert()`. If multiple alerts with the same `id` is sent, all of them will be dismissed. + + + +#### Dismiss All + +All alerts can be dismissed by calling `dismissAll()`. + + + +#### Dismiss From UI + +When user clicks alert's close button, it will dismiss that single alert from the service. But it will not affect the other alerts with the same `id`. + +## Positioning + +Usually Alert Group is positioned to the top of the screen, right after navigation. + +CSS position of Alert Group can be set using `position` attribute with three currently provided options. By default it is `fixed` but can be set to `static` or `absolute` as well. + +## With Fudis Dialog + +Fudis automatically embeddes Alert Group inside [Dialog Component](/docs/components-dialog--documentation), so that new and existing alerts are visible and available for keyboard users when dialog is open. +Alerts are reloaded to the DOM each time dialog is opened and closed. + +## Accessibility and Keyboard Behavior + +### Alert Variants + +`danger` and `warning` variants are communicated to screen readers, as they have html attribute of `role='alert'`. Other variants have `role='status'`. Focus does not change even if new alert appears. + +### Closing Alert + +When user closes an alert, focus will move automatically to the last alert in the list. If there are no alerts left, Alert Group tries to return focus to that UI element where focus was before focusing on the alert. + +### With Fudis Dialog Open + +[Dialog Component](/docs/components-dialog--documentation) has a focus trap, and navigating outside of the dialog is not possible until the dialog is closed. If there are visible alerts, those are included as focusable elements inside the dialog's focus trap. If user closes all the alerts when dialog is open, focus returns to dialog's close button. + +### Other Mentionable Accessibility Details + +- Focusable elements inside alert have visible focus state +- Danger and warning alerts are communicated to user using `role='alert'` attribute +- Alert Group is wrapped inside a `section` element which has an automatic `aria-label` describing its content. E.g. _'Notifications - Number of notifications: 5'_ + +## Properties + + diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.html b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.html index 4d78b4be2..f5edc7684 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.html +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.html @@ -1,31 +1,15 @@
- {{ message }} - + {{ message | async }}
`, }) -class MockAlertComponent { - constructor(private _alertService: FudisAlertService) { +class MockAlertComponent implements OnInit { + constructor( + private _alertService: FudisAlertService, + private _cdRef: ChangeDetectorRef, + ) { const firstAlert: FudisAlert = { - message: 'Test message', + message: new BehaviorSubject('Test message'), id: 'my-test-id-1', type: 'info', }; @@ -42,21 +42,23 @@ class MockAlertComponent { _alertService.addAlert(firstAlert); const secondAlert: FudisAlert = { - message: 'Second test message', + message: new BehaviorSubject('Second test message'), id: 'my-test-id-2', type: 'warning', - linkTitle: 'Test link', - routerLinkUrl: '/test/url', }; this._alertService.addAlert(secondAlert); } -} -// TODO: fix & refactor when these are again published + ngOnInit(): void { + this._cdRef.detectChanges(); + } +} -describe.skip('AlertComponent', () => { +describe('AlertComponent', () => { let component: AlertComponent; - let fixture: ComponentFixture | ComponentFixture; + let fixture: ComponentFixture; + let mockComponent: MockAlertComponent; + let mockFixture: ComponentFixture; let alertService: FudisAlertService; let focusService: FudisFocusService; @@ -65,14 +67,14 @@ describe.skip('AlertComponent', () => { declarations: [ AlertComponent, AlertGroupComponent, - ButtonComponent, - MockAlertComponent, BodyTextComponent, - MockComponent(IconComponent), + ButtonComponent, LinkComponent, LinkDirective, + MockAlertComponent, + MockComponent(IconComponent), ], - imports: [MatDialogModule, RouterTestingModule, RouterModule.forRoot([])], + imports: [MatDialogModule, RouterModule.forRoot([])], providers: [ FudisDialogService, FudisAlertService, @@ -88,6 +90,8 @@ describe.skip('AlertComponent', () => { }); fixture = TestBed.createComponent(AlertComponent); component = fixture.componentInstance; + mockFixture = TestBed.createComponent(MockAlertComponent); + mockComponent = mockFixture.componentInstance; component.buttonId = testButtonId; component.htmlId = testHtmlId; component.message = testMessage; @@ -95,96 +99,67 @@ describe.skip('AlertComponent', () => { fixture.detectChanges(); }); - describe('basic inputs', () => { - it('should create alert with basic inputs', () => { - fixture.detectChanges(); - - const element = getElement(fixture, '.fudis-alert'); - - // Test @Input() htmlId - expect(element.getAttribute('id')).toEqual(testHtmlId); - - // Test @Input() buttonId - expect(element.querySelector('button')?.getAttribute('id')).toEqual(testButtonId); - - // Test @Input() message - expect(element.querySelector('button')?.getAttribute('aria-label')).toEqual( - `${testMessage}, Close`, - ); - - // Test @Input() message - expect(element.querySelector('p')?.textContent).toContain(testMessage); - - // Test that link does not exist by default - expect(element.querySelector('fudis-link')).toBeFalsy(); - }); - - it('should create alert with a link', () => { - component.linkTitle = testLinkTitle; - component.link = testRouterLinkUrl; - component.initialFocus = true; - - fixture.detectChanges(); - - const linkElement = getElement(fixture, '.fudis-alert p fudis-link'); - const anchorElement = linkElement.querySelector('a'); - - // Test that inputs forwarded to Fudis Link are correct - expect(linkElement).toBeTruthy(); + it('should create', () => { + expect(component).toBeTruthy(); + }); - expect(sortClasses(linkElement.className)).toEqual(['fudis-alert__link']); + describe('Inputs', () => { + it('should create alert with basic inputs', async () => { + await fixture.whenStable().finally(() => { + fixture.detectChanges(); + const element = getElement(fixture, '.fudis-alert'); - expect(linkElement?.getAttribute('aria-label')).toEqual(testMessage); + // Test @Input() message + expect(element.querySelector('button')?.getAttribute('aria-label')).toEqual( + String(testMessage.value) + ', Close', + ); - expect(linkElement?.getAttribute('ng-reflect-initial-focus')).toEqual('true'); + // Test @Input() htmlId + expect(element.getAttribute('id')).toEqual(testHtmlId); - expect(anchorElement?.getAttribute('ng-reflect-router-link')).toEqual(testRouterLinkUrl); + // Test @Input() buttonId + expect(element.querySelector('button')?.getAttribute('id')).toEqual(testButtonId); - expect(anchorElement?.getAttribute('href')).toEqual(testRouterLinkUrl); + // Test @Input() message + expect(element.querySelector('p')?.textContent).toContain(String(testMessage.value)); - expect(linkElement?.getAttribute('ng-reflect-title')).toEqual(testLinkTitle); + // Test that link does not exist by default + expect(element.querySelector('fudis-link')).toBeFalsy(); + }); }); }); - describe('CSS classes', () => { - it('should create alert variations with proper CSS classes', () => { - const element = getElement(fixture, '.fudis-alert'); - - expect(sortClasses(element.className)).toEqual(sortClasses('fudis-alert fudis-alert__info')); - - component.variant = 'danger'; - - fixture.detectChanges(); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-alert fudis-alert__danger'), - ); - - component.variant = 'success'; - - fixture.detectChanges(); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-alert fudis-alert__success'), - ); + describe('CSS classes and HTML role', () => { + it('should create alert variations with proper variant classes', () => { + const alert = getElement(fixture, '.fudis-alert'); - component.variant = 'warning'; + fudisNotificationVariantArray.forEach((variant) => { + fixture.componentRef.setInput('variant', `${variant}`); + fixture.detectChanges(); - fixture.detectChanges(); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-alert fudis-alert__warning'), - ); + expect(sortClasses(alert.className)).toEqual( + sortClasses(`fudis-alert fudis-alert__${variant}`), + ); + }); + }); - component.variant = 'info'; + it('should have assigned to correct role depending on the variant', () => { + const alertText = getElement(fixture, '.fudis-alert__text'); - fixture.detectChanges(); + fudisNotificationVariantArray.forEach((variant) => { + fixture.componentRef.setInput('variant', `${variant}`); + fixture.detectChanges(); - expect(sortClasses(element.className)).toEqual(sortClasses('fudis-alert fudis-alert__info')); + if (variant === 'info' || variant === 'success') { + expect(alertText.getAttribute('role')).toEqual('status'); + } else { + expect(alertText.getAttribute('role')).toEqual('alert'); + } + }); }); }); - describe('close button interaction', () => { + describe('Close button interaction', () => { beforeEach(() => { alertService = TestBed.inject(FudisAlertService); jest.spyOn(alertService, 'dismissAlertFromButton').mockImplementation(); @@ -201,74 +176,44 @@ describe.skip('AlertComponent', () => { fixture.detectChanges(); const closeButton = getElement(fixture, '.fudis-alert .fudis-alert__close'); - closeButton.click(); expect(clicked).toEqual(true); - fixture.detectChanges(); - expect(alertService.dismissAlertFromButton).toHaveBeenCalledWith(testButtonId); }); }); describe('Focus and blur events', () => { beforeEach(() => { - fixture = TestBed.createComponent(MockAlertComponent); focusService = TestBed.inject(FudisFocusService); jest.spyOn(focusService, 'setFocusTarget').mockImplementation(); jest.spyOn(focusService, 'getFocusTarget').mockImplementation(); + alertService = TestBed.inject(FudisAlertService); jest.spyOn(alertService, 'addAlert').mockImplementation(() => {}); jest.spyOn(alertService, 'updateAlertLinkFocusState').mockImplementation(); - fixture.detectChanges(); - }); - - // TODO: fix tests - - // it('should emit info to focusService when focusing on closeButton', () => { - // const button = getElement(fixture, '.fudis-button'); - - // button.focus(); - - // const closeButton = getElement(fixture, '.fudis-alert .fudis-alert__close'); - - // closeButton.focus(); - - // expect(focusService.setFocusTarget).toHaveBeenCalledWith(button); - // }); - // it('should emit info to focusService when focusing on link in alert', () => { - // const button = getElement(fixture, '.fudis-button'); - - // button.focus(); - - // const linkInAlert = getElement(fixture, '.fudis-link'); - - // linkInAlert.focus(); - - // expect(focusService.setFocusTarget).toHaveBeenCalledWith(button); - // }); - - it('should not emit info to focusService when focusing from link to alert close and vice versa', () => { - const linkInAlert = getElement(fixture, '.fudis-link'); + mockFixture.detectChanges(); + }); - linkInAlert.focus(); + it('should create mock', () => { + expect(mockComponent).toBeTruthy(); + }); - const closeButton = getElement(fixture, '.fudis-alert__close'); + it('should emit info to focusService when focusing on closeButton', () => { + const button = getElement(mockFixture, '.fudis-button'); + button.focus(); + const closeButton = getElement(mockFixture, '.fudis-alert .fudis-alert__close'); closeButton.focus(); - linkInAlert.focus(); - - expect(focusService.setFocusTarget).not.toHaveBeenCalledWith(closeButton); - - expect(focusService.setFocusTarget).not.toHaveBeenCalledWith(linkInAlert); + expect(focusService.setFocusTarget).toHaveBeenCalledWith(button); }); it('should set focus to close button, if another close is clicked', () => { - const firstClose = getElement(fixture, '#fudis-alert-1 .fudis-alert__close'); - const secondClose = getElement(fixture, '#fudis-alert-2 .fudis-alert__close'); + const firstClose = getElement(mockFixture, '#fudis-alert-1 .fudis-alert__close'); + const secondClose = getElement(mockFixture, '#fudis-alert-2 .fudis-alert__close'); jest.spyOn(secondClose, 'focus').mockImplementation(() => {}); @@ -279,13 +224,11 @@ describe.skip('AlertComponent', () => { }); it('should call getFocusTarget, when all alerts are closed', () => { - const uiButton = getElement(fixture, '.fudis-button'); - + const uiButton = getElement(mockFixture, '.fudis-button'); uiButton.focus(); - const firstClose = getElement(fixture, '#fudis-alert-1 .fudis-alert__close'); - - const secondClose = getElement(fixture, '#fudis-alert-2 .fudis-alert__close'); + const firstClose = getElement(mockFixture, '#fudis-alert-1 .fudis-alert__close'); + const secondClose = getElement(mockFixture, '#fudis-alert-2 .fudis-alert__close'); firstClose.click(); secondClose.click(); @@ -294,27 +237,13 @@ describe.skip('AlertComponent', () => { }); it('should not call getFocusTarget, if only one is closed', () => { - const uiButton = getElement(fixture, '.fudis-button'); - + const uiButton = getElement(mockFixture, '.fudis-button'); uiButton.focus(); - const firstClose = getElement(fixture, '#fudis-alert-1 .fudis-alert__close'); - + const firstClose = getElement(mockFixture, '#fudis-alert-1 .fudis-alert__close'); firstClose.click(); expect(focusService.getFocusTarget).not.toHaveBeenCalledWith(); }); - - // it('should update initialFocus, when blurring from link', () => { - // const alertLink = getElement(fixture, '#fudis-alert-2 .fudis-link'); - - // const secondClose = getElement(fixture, '#fudis-alert-2 .fudis-alert__close'); - - // alertLink.focus(); - - // secondClose.focus(); - - // expect(alertService.updateAlertLinkFocusState).toHaveBeenCalledWith('fudis-alert-2'); - // }); }); }); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.ts index 49ce07163..844c58227 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/alert/alert.component.ts @@ -1,5 +1,14 @@ -import { Component, EventEmitter, Inject, Input, Output, effect } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Inject, + Input, + Output, + effect, +} from '@angular/core'; import { DOCUMENT } from '@angular/common'; +import { Observable, Subject } from 'rxjs'; import { FudisNotification } from '../../../types/miscellaneous'; import { FudisFocusService } from '../../../services/focus/focus.service'; import { FudisTranslationService } from '../../../services/translation/translation.service'; @@ -10,6 +19,7 @@ import { FudisDialogService } from '../../../services/dialog/dialog.service'; selector: 'fudis-alert', templateUrl: './alert.component.html', styleUrls: ['./alert.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class AlertComponent { constructor( @@ -20,16 +30,14 @@ export class AlertComponent { private _dialogService: FudisDialogService, ) { effect(() => { - // TODO: To Observable - this._closeLabel = this._translateService.getTranslations()().DIALOG.CLOSE; + this._closeLabel.next(this._translateService.getTranslations()().DIALOG.CLOSE); }); } /** - * Visible message + * Visible alert message */ - // TODO: add observable message - @Input({ required: true }) message: string; + @Input({ required: true }) message: Observable; /** * Id to be set on the whole alert element @@ -37,42 +45,24 @@ export class AlertComponent { @Input({ required: true }) htmlId: string; /** - * Id to be set on the close alert button + * Id to be set on the close button */ @Input({ required: true }) buttonId: string; /** - * Variant of alert. Same names and colors as in Notification component. + * Variant of Alert. Same names and colors as in Notification Component. */ @Input() variant: FudisNotification = 'info'; /** - * Conditional routerLink for Alert. If used, provide also linkTitle. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - @Input() link: string | any[] | null | undefined; - - /** - * Title of url used with routerLinkUrl - */ - @Input() linkTitle: string | undefined; - - /** - * Used with links in alert, to force focus to the link on the first load. - */ - @Input() initialFocus: boolean = false; - - /** - * Output for when Close button is clicked + * Output for close button click */ @Output() handleClose = new EventEmitter(); - // TODO: add output for possible callback event when clicking link in alert - /** - * Label for close button, fetched from FudisTranslationService + * Internal translated aria-label for close button */ - protected _closeLabel: string; + protected _closeLabel = new Subject(); /** * Handler for close button. Dismisses alert from service and sets focus to last alert in the list or to previously focused element stored with _handleFocus(). @@ -80,7 +70,7 @@ export class AlertComponent { protected _handleCloseClick(event: Event): void { this._alertService.dismissAlertFromButton(this.buttonId); - const alerts = this._alertService.getAlertsSignal()(); + const alerts = this._alertService.alerts.getValue(); if (alerts.length !== 0) { const targetId = alerts[alerts.length - 1].buttonId; @@ -94,12 +84,12 @@ export class AlertComponent { } /** - * Focus handler for both link and close button inside alert. Saves the element focus originated from to restore focus there when there are no alerts left. + * Focus handler for close Button inside alert. Saves the element focus originated from, to restore focus there when there are no alerts left. */ protected _handleFocus(focusEvent: FocusEvent): void { const relatedTarget = focusEvent?.relatedTarget as HTMLElement; - const isDialogOpen = this._dialogService.getDialogOpenSignal()(); + const isDialogOpen = this._dialogService.getDialogOpenStatus().value; /** * First if: when keyboard tabbing through ngMaterial dialog, focus goes through its hidden focus-trap helper element which focuses on either first alert on the list or dialog close. So we store the dialog close as focus target. @@ -121,20 +111,4 @@ export class AlertComponent { this._focusService.setFocusTarget(relatedTarget); } } - - /** - * When blurring from link inside alert, initialFocus is set to false. (So that in e. g. opening dialog doesn't re-focus to link). Because this makes alert to render again, the focus may be lost, so this blurring makes sure the next focus target is logical. - */ - protected _handleBlur(event: FocusEvent): void { - const nextElement = event.relatedTarget as HTMLElement; - - if ( - this.initialFocus && - nextElement?.classList?.contains('fudis-alert__close') && - nextElement?.id - ) { - this._alertService.updateAlertLinkFocusState(this.htmlId); - this._focusService.focusToElementById(nextElement.id); - } - } } diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/readme.hidden b/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/readme.hidden deleted file mode 100644 index 3e02aac7c..000000000 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/alert/readme.hidden +++ /dev/null @@ -1,108 +0,0 @@ -import { ArgTypes, Meta, Canvas } from "@storybook/blocks"; -import { AlertGroupComponent } from "./alert-group/alert-group.component.ts"; - - - -# Alert Group - -`Alert Group` component displays list of toaster-like `Alert` components with four variants: `success`, `info`, `warning` and `danger.` These variants are same ones as with `Notification` component. - -## Adding and dismissing Alerts - -`Alert Group` listens to `FudisAlertService` where application or UI sends information about adding or dismissing alerts. - -### Add new alert - -To add new alert, send an object of type `FudisAlert` to `FudisAlertService` using `addAlert()`. - -Note that `id` doesn't need to be unique for AlertService. - -``` -constructor( - private _alertService: FudisAlertService - ) { - const newAlert: FudisAlert = { - message: 'Well done, a new alert is displayed!', - type: 'success', - id: 'well-done-identifier', - }; - - _alertService.addAlert(newAlert); - } -``` - -### Add new alert with routerLink - -It has been discussed with designers, that using alerts with links should be avoided, as it can cause confusion for users, as they need to focus on multiple interactive areas in the UI. But if a link is needed, both `routerLinkUrl` and `linkTitle` must be provided. - -``` -const alertWithLink: FudisAlert = { - message: 'You need to adjust your settings.', - type: 'warning', - routerLinkUrl: '/your-routerlink-url/to/destination' - linkTitle: 'Move to your settings.' - id: 'move-to-settings-warning-identifier', - }; - - _alertService.addAlert(alertWithLink); -``` - -### Dismiss alert - -#### Dismiss by id - -Previously sent alert can be dismissed with `dismissAlert()`. It will dismiss all alerts with `id` provided. So if for example you have sent multiple alerts with same `id`, all of them will be dismissed. - -``` -_alertService.dismissAlert('well-done-identifier'); -``` - -#### Dismiss all - -All alerts can be dismissed by calling `dismissAll()`. - -``` -_alertService.dismissAll(); -``` - -#### Dismiss from UI - -When user click alert's close button, it will dismiss that single alert from the service. But it will still leave all other alerts with the same `id`. - -## Positioning and with Fudis Dialog - -Usually Alert Group is positioned to the top of the screen right after navigation. - -CSS `position` of Alert Group can be set using `position` attribute with three currently provided options. By default it is `fixed` but can be set to `static` or `absolute` as well. - -Fudis automatically embeddes Alert Group inside every Dialog, so that new and existing alerts are visible and available for keyboard users when Dialog is open. - -## Accessibility and keyboard behavior - -### Alerts without a link - -Alerts without links and with variant `danger` or `warning` are communicated to screen readers, as they have html attribute of `role='alert'`. Other variants have `role='status'`. Focus does not change even if new alert appears. - -### With links - -With links, the focus will move to that link newly added alert, so that keyboard users can more easily react to it if needed. - -### When closing alert - -When user closes an alert, focus will move automatically to the last alert in the list. If there are no alerts left, Alert Group tries to return focus in that element of the UI the focus was before focusing on the alert. - -### With Fudis Dialog open - -Fudis Dialog has a focus trap, so that keyboard are not able to navigate outside dialog until Dialog is closed. If there are alerts visible, those are included as focusable elements inside Dialog's focus trap. If user closes all the alerts when dialog is open, focus returns to Dialog's close button. - -### Other mentionable accessibility details - -- Focusable elements inside alert have visible focus state. -- `Danger` and `warning` alerts without links are communicated to user using `role='alert'` attribute. -- Contrast between background colors and text meet accessibility criteria. -- Alert Group is wrapped inside a `section` element which has an automatic `aria-label` describing its content. E. g. 'Notifications - Number of notifications: 5' -- Elements used in Alert Group have proper labels as needed for screen reader users. - -## Properties - - diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.html b/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.html index e33406265..61d94723c 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.html +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.html @@ -1,4 +1,4 @@ - +
{ }); it('should call open signal on initialisation', () => { - const dialogSpy = jest.spyOn(dialogService, 'setDialogOpenSignal'); + const dialogSpy = jest.spyOn(dialogService, 'setDialogOpenStatus'); initDialogComponent(); expect(dialogSpy).toHaveBeenCalledWith(true); }); it('should call open signal on destroy', () => { - const dialogSpy = jest.spyOn(dialogService, 'setDialogOpenSignal'); + const dialogSpy = jest.spyOn(dialogService, 'setDialogOpenStatus'); initDialogComponent(); component.ngOnDestroy(); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.ts index 6d787eb40..019abc5d2 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/dialog.component.ts @@ -21,7 +21,7 @@ export class DialogComponent implements OnDestroy, OnInit { }); this._id = _idService.getNewId('dialog'); - _dialogService.setDialogOpenSignal(true); + _dialogService.setDialogOpenStatus(true); } /** @@ -55,7 +55,7 @@ export class DialogComponent implements OnDestroy, OnInit { ngOnDestroy(): void { if (this._orderNumber === 1 || this._orderNumber === 0) { - this._dialogService.setDialogOpenSignal(false); + this._dialogService.setDialogOpenStatus(false); } } diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/ngMaterial-theme.scss b/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/ngMaterial-theme.scss index 028106845..f9ab10515 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/ngMaterial-theme.scss +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/dialog/ngMaterial-theme.scss @@ -16,6 +16,16 @@ @use "../../foundations/breakpoints/mixins.scss" as breakpoints; @use "../../foundations/utilities/mixins.scss" as utilities; @use "../../foundations/spacing/tokens.scss" as spacing; +@use "../../foundations/typography/mixins.scss" as typography; + +.fudis-dialog { + /* Body Text inside Dialog should be md-light by default but can be altered by the App */ + & fudis-body-text { + & .fudis-body-text__default { + @include typography.body-text-md-light; + } + } +} .cdk-overlay-container { & .cdk-global-overlay-wrapper { diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/link/link.component.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/link/link.component.ts index c4828282e..0ef7246f9 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/link/link.component.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/link/link.component.ts @@ -15,7 +15,6 @@ import { BehaviorSubject } from 'rxjs'; @Component({ selector: 'fudis-link', templateUrl: './link.component.html', - styleUrls: ['./link.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class LinkComponent extends LinkApiDirective implements OnChanges { @@ -128,7 +127,7 @@ export class LinkComponent extends LinkApiDirective implements OnChanges { } /** - * Handle Link Component blur event + * Handle Link Component click event */ protected _handleClick(event: Event): void { this.handleClick.emit(event); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text-theme.scss b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text-theme.scss index 2f5eacc16..fd6d5a1c6 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text-theme.scss +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text-theme.scss @@ -1,4 +1,5 @@ @use "../../../foundations/spacing/tokens.scss" as spacing; +@use "../../../foundations/colors/tokens.scss" as colors; fudis-body-text { /** @@ -11,11 +12,26 @@ fudis-body-text { } /** -* Remove spacing between multiple following Body Text elements directly under Grid element. -* Grid already defines a gap between its children elements. -*/ + * Remove spacing between multiple following Body Text elements directly under Grid element. + * Grid already defines a gap between its children elements. + */ .fudis-grid { > fudis-body-text:has(+ fudis-body-text) { margin-bottom: spacing.$spacing-none; } } + +/** + * Body Texts inside three Alert variants should be white instead of default dark-gray. + * Body Text Component does not support other than dark-gray color, we need to use important! here + */ +.fudis-alert { + &__info, + &__success, + &__danger { + fudis-body-text p { + /* stylelint-disable-next-line property-disallowed-list, declaration-no-important */ + color: colors.$color-white !important; + } + } +} diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.html b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.html index 41f725070..c2bf22d37 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.html +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.html @@ -1,3 +1,6 @@ -

diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.spec.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.spec.ts index 6def13ef5..2d1e0ca3b 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.spec.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ChangeDetectionStrategy } from '@angular/core'; import { BodyTextComponent } from './body-text.component'; import { getElement, sortClasses } from '../../../utilities/tests/utilities'; -import { fudisBodyTextArray } from '../../../types/typography'; +import { fudisBodyTextArray, fudisTextAlignArray } from '../../../types/typography'; describe('BodyTextComponent', () => { let component: BodyTextComponent; @@ -26,7 +26,7 @@ describe('BodyTextComponent', () => { //TODO: Write test for host class describe('CSS classes', () => { - it('should change CSS classes according to the given body-text variant', () => { + it('should change CSS classes according to the given body-text variant input', () => { fudisBodyTextArray.forEach((variant) => { component.variant = variant; fixture.detectChanges(); @@ -34,32 +34,26 @@ describe('BodyTextComponent', () => { const element = getElement(fixture, '.fudis-body-text'); expect(sortClasses(element.className)).toEqual( - sortClasses(`fudis-body-text fudis-body-text__left fudis-body-text__${variant}`), + sortClasses( + `fudis-body-text fudis-body-text__default fudis-body-text__left fudis-body-text__${variant}`, + ), ); }); }); - // TODO: Refactor to use helper function to test align input - it('should change CSS classes according to given body-text align', () => { - const element = getElement(fixture, '.fudis-body-text'); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-body-text fudis-body-text__left fudis-body-text__md-regular'), - ); - - component.align = 'center'; - fixture.detectChanges(); - - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-body-text fudis-body-text__center fudis-body-text__md-regular'), - ); + it('should change CSS classes according to the given body-text align input', () => { + fudisTextAlignArray.forEach((align) => { + component.align = align; + fixture.detectChanges(); - component.align = 'right'; - fixture.detectChanges(); + const element = getElement(fixture, '.fudis-body-text'); - expect(sortClasses(element.className)).toEqual( - sortClasses('fudis-body-text fudis-body-text__right fudis-body-text__md-regular'), - ); + expect(sortClasses(element.className)).toEqual( + sortClasses( + `fudis-body-text fudis-body-text__default fudis-body-text__${align} fudis-body-text__md-regular`, + ), + ); + }); }); }); }); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.ts b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.ts index c5cab5e4d..3a3e6f47e 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/components/typography/body-text/body-text.component.ts @@ -1,14 +1,8 @@ -import { - Component, - Input, - HostBinding, - ChangeDetectionStrategy, - Optional, - Host, -} from '@angular/core'; +import { Component, Input, HostBinding, ChangeDetectionStrategy, OnChanges } from '@angular/core'; import { FudisBodyText, FudisTextAlign } from '../../../types/typography'; -import { DialogComponent } from '../../dialog/dialog.component'; import { FudisIdService } from '../../../services/id/id.service'; +import { FudisComponentChanges } from '../../../types/miscellaneous'; +import { BehaviorSubject } from 'rxjs'; @Component({ selector: 'fudis-body-text', @@ -16,16 +10,9 @@ import { FudisIdService } from '../../../services/id/id.service'; styleUrls: ['./body-text.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class BodyTextComponent { - constructor( - @Host() @Optional() private _parentDialog: DialogComponent, - private _idService: FudisIdService, - ) { +export class BodyTextComponent implements OnChanges { + constructor(private _idService: FudisIdService) { this._id = _idService.getNewId('body-text'); - - if (_parentDialog) { - this.variant = 'md-light'; - } } /** @@ -48,5 +35,18 @@ export class BodyTextComponent { */ protected _id: string; - // TODO: Enable Input spacing for marginBottom + /** + * To add default CSS class if app hasn't provided any variant + */ + protected _defaultClass = new BehaviorSubject(true); + + ngOnChanges(changes: FudisComponentChanges): void { + if (changes.variant?.currentValue !== changes.variant?.previousValue) { + if (!changes.variant?.currentValue) { + this._defaultClass.next(true); + } else { + this._defaultClass.next(false); + } + } + } } diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts b/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts index 52a99ad38..f55ba4bd9 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/ngx-fudis.module.ts @@ -224,9 +224,9 @@ import { */ exports: [ ActionsDirective, + AlertComponent, + AlertGroupComponent, AutocompleteComponent, - // AlertComponent, - // AlertGroupComponent, BadgeComponent, BodyTextComponent, BreadcrumbsComponent, diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.spec.ts b/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.spec.ts index c29678775..43cb3c508 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.spec.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.spec.ts @@ -1,19 +1,21 @@ import { TestBed } from '@angular/core/testing'; - import { FudisAlertService } from './alert.service'; import { FudisAlert, FudisAlertElement } from '../../types/miscellaneous'; +import { BehaviorSubject } from 'rxjs'; describe('FudisAlertService', () => { let service: FudisAlertService; - const firstAlert: FudisAlert = { message: 'First message', type: 'danger', id: 'test-id-1' }; + const firstAlert: FudisAlert = { + message: new BehaviorSubject('First message'), + type: 'danger', + id: 'test-id-1', + }; const secondAlert: FudisAlert = { - message: 'Second message', + message: new BehaviorSubject('Second message'), type: 'warning', id: 'test-id-2', - linkTitle: 'Test link title', - routerLinkUrl: 'test/url/here', }; const firstAlertFromService: FudisAlertElement = { @@ -39,19 +41,19 @@ describe('FudisAlertService', () => { expect(service).toBeTruthy(); }); - it('should be return an empty array initially', () => { - const alerts = service.getAlertsSignal(); + it('should return an empty array initially', () => { + const alerts = service.alerts.getValue(); - expect(alerts()).toEqual([]); + expect(alerts).toEqual([]); }); it('should return two alerts after adding two', () => { service.addAlert(firstAlert); service.addAlert(secondAlert); - const alerts = service.getAlertsSignal(); + const alerts = service.alerts.getValue(); - expect(alerts()).toEqual([firstAlertFromService, secondAlertFromService]); + expect(alerts).toEqual([firstAlertFromService, secondAlertFromService]); }); it('should return second alert after dismissing first by id', () => { @@ -60,9 +62,9 @@ describe('FudisAlertService', () => { service.dismissAlert('test-id-1'); - const alerts = service.getAlertsSignal(); + const alerts = service.alerts.getValue(); - expect(alerts()).toEqual([secondAlertFromService]); + expect(alerts).toEqual([secondAlertFromService]); }); it('should return empty array after dismissing all', () => { @@ -71,9 +73,9 @@ describe('FudisAlertService', () => { service.dismissAll(); - const alerts = service.getAlertsSignal(); + const alerts = service.alerts.getValue(); - expect(alerts()).toEqual([]); + expect(alerts).toEqual([]); }); it('should return only second alert after first one is dismissed by button', () => { @@ -82,9 +84,9 @@ describe('FudisAlertService', () => { service.dismissAlertFromButton('fudis-alert-1-button'); - const alerts = service.getAlertsSignal(); + const alerts = service.alerts.getValue(); - expect(alerts()).toEqual([secondAlertFromService]); + expect(alerts).toEqual([secondAlertFromService]); }); it('should update initialFocus status of second alert', () => { @@ -93,11 +95,8 @@ describe('FudisAlertService', () => { service.updateAlertLinkFocusState('fudis-alert-2'); - const alerts = service.getAlertsSignal(); + const alerts = service.alerts.getValue(); - expect(alerts()).toEqual([ - firstAlertFromService, - { ...secondAlertFromService, initialFocus: false }, - ]); + expect(alerts[1].initialFocus).toEqual(false); }); }); diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.ts b/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.ts index 2e28f4075..21d1e99cc 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/services/alert/alert.service.ts @@ -1,79 +1,80 @@ -import { Injectable, Signal, signal } from '@angular/core'; +import { Injectable } from '@angular/core'; import { FudisIdService } from '../id/id.service'; import { FudisAlert, FudisAlertElement } from '../../types/miscellaneous'; +import { BehaviorSubject } from 'rxjs'; -// TODO: discuss how much this needs to be improved to be usable for client application @Injectable({ providedIn: 'root', }) export class FudisAlertService { constructor(private _idService: FudisIdService) {} - private _alerts = signal([]); + /** + * Current alerts + */ + private _alerts = new BehaviorSubject([]); /** - * To add new alert, which will be rendered then by Alert Group component. Note 'id' doesn't necessarily need to be unique. It could also e. g. 'error-in-form-add-new-user', so all these error could be dismissed with dismissAlert() using this 'id'. + * To add new alert, which will be rendered by Alert Group component. + * Note that 'id' doesn't necessarily need to be unique. All errors with the same 'id' can be dismissed with dismissAlert() using that particular 'id'. */ public addAlert(newAlert: FudisAlert): void { - const alertToAdd = newAlert; - const htmlId = this._idService.getNewId('alert'); + const currentAlerts = this._alerts.getValue(); - const currentAlerts: FudisAlertElement[] = this._alerts(); - - currentAlerts.push({ ...alertToAdd, htmlId, buttonId: `${htmlId}-button`, initialFocus: true }); - - this._alerts.set(currentAlerts); + this._alerts.next([ + ...currentAlerts, + { ...newAlert, htmlId, buttonId: `${htmlId}-button`, initialFocus: true }, + ]); } /** * To dismiss alert by 'id' provided in 'addAlert()'. It will dismiss all alerts matching the 'id'. */ public dismissAlert(id: string): void { - const currentAlerts: FudisAlertElement[] = this._alerts(); - - const filteredAlerts = currentAlerts.filter((alert) => alert.id !== id); - - this._alerts.set(filteredAlerts); + const filteredAlerts = this._alerts.getValue().filter((alert) => alert.id !== id); + this._alerts.next(filteredAlerts); } /** * Dismisses only one alert from alert's close button click in the UI */ public dismissAlertFromButton(id: string): void { - const currentAlerts: FudisAlertElement[] = this._alerts(); - - const filteredAlerts = currentAlerts.filter((alert) => alert.buttonId !== id); - - this._alerts.set(filteredAlerts); + const filteredAlerts = this._alerts.getValue().filter((alert) => alert.buttonId !== id); + this._alerts.next(filteredAlerts); } /** * Dismiss all alerts */ public dismissAll(): void { - this._alerts.set([]); + this._alerts.next([]); } /** - * Get Signal containing alerts array + * Get alerts as an Observable */ - public getAlertsSignal(): Signal { - return this._alerts.asReadonly(); + get alerts(): BehaviorSubject { + return this._alerts; } /** * Set 'initialFocus' attribute of alert to false, so that if same alert with link is rendered again, the initial focus will not jump there anymore. */ public updateAlertLinkFocusState(htmlId: string): void { - const currentAlerts: FudisAlertElement[] = this._alerts(); + let alertIndex: number; + const tempAlerts = this._alerts.getValue(); - const index = currentAlerts.findIndex((alert) => alert.htmlId === htmlId); + tempAlerts.forEach((value, index) => { + if (value.htmlId === htmlId) { + alertIndex = index; + } - if (index !== -1) { - currentAlerts[index] = { ...currentAlerts[index], initialFocus: false }; + if (alertIndex !== -1) { + tempAlerts[index].initialFocus = false; - this._alerts.set(currentAlerts); - } + this._alerts.next(tempAlerts); + } + }); } } diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/services/dialog/dialog.service.ts b/ngx-fudis/projects/ngx-fudis/src/lib/services/dialog/dialog.service.ts index ad7829055..40a2ba63d 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/services/dialog/dialog.service.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/services/dialog/dialog.service.ts @@ -1,13 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentType } from '@angular/cdk/portal'; -import { Injectable, Signal, TemplateRef, signal } from '@angular/core'; +import { Injectable, TemplateRef } from '@angular/core'; import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; +import { BehaviorSubject } from 'rxjs'; @Injectable() export class FudisDialogService { constructor(public ngMaterialDialog: MatDialog) {} - private _dialogOpen = signal(false); + private _dialogOpen = new BehaviorSubject(false); private _dialogRefs: MatDialogRef[] = []; @@ -19,7 +20,6 @@ export class FudisDialogService { */ public open( component: ComponentType | TemplateRef, - // eslint-disable-next-line @typescript-eslint/no-explicit-any config?: MatDialogConfig, ): MatDialogRef { const newDialog = this.ngMaterialDialog.open( @@ -54,15 +54,15 @@ export class FudisDialogService { /** * Get dialog open status */ - public getDialogOpenSignal(): Signal { - return this._dialogOpen.asReadonly(); + public getDialogOpenStatus(): BehaviorSubject { + return this._dialogOpen; } /** * Set dialog open */ - public setDialogOpenSignal(value: boolean): void { - this._dialogOpen.set(value); + public setDialogOpenStatus(value: boolean): void { + this._dialogOpen.next(value); } /** diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/types/miscellaneous.ts b/ngx-fudis/projects/ngx-fudis/src/lib/types/miscellaneous.ts index bff4dea24..67df01f92 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/types/miscellaneous.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/types/miscellaneous.ts @@ -1,14 +1,15 @@ import { SimpleChange } from '@angular/core'; +import { Observable } from 'rxjs'; /** * Alert */ +export const fudisAlertPositionArray = ['static', 'absolute', 'fixed'] as const; +export type FudisAlertPosition = (typeof fudisAlertPositionArray)[number]; + export interface FudisAlert { - message: string; + message: Observable; type: FudisNotification; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - routerLinkUrl?: string | any[] | null; - linkTitle?: string; id: string; } @@ -68,7 +69,8 @@ export type FudisLanguageAbbr = 'fi' | 'sv' | 'en'; /** * Notification */ -export type FudisNotification = 'warning' | 'danger' | 'success' | 'info'; +export const fudisNotificationVariantArray = ['warning', 'danger', 'success', 'info'] as const; +export type FudisNotification = (typeof fudisNotificationVariantArray)[number]; /** * Tooltip diff --git a/ngx-fudis/projects/ngx-fudis/src/lib/utilities/storybook.ts b/ngx-fudis/projects/ngx-fudis/src/lib/utilities/storybook.ts index 10473267d..6dae26102 100644 --- a/ngx-fudis/projects/ngx-fudis/src/lib/utilities/storybook.ts +++ b/ngx-fudis/projects/ngx-fudis/src/lib/utilities/storybook.ts @@ -30,6 +30,11 @@ export const excludeEverythingExceptRegex = (array?: string[]): RegExp => { export const excludeAllRegex: RegExp = /.*/; +/** + * Alert + */ +export const alertGroupExclude: RegExp = excludeRegex(['insideDialog', 'getVisibleStatus']); + /** * Button */ diff --git a/ngx-fudis/projects/ngx-fudis/src/public-api.ts b/ngx-fudis/projects/ngx-fudis/src/public-api.ts index 97f973015..fb847f0a2 100644 --- a/ngx-fudis/projects/ngx-fudis/src/public-api.ts +++ b/ngx-fudis/projects/ngx-fudis/src/public-api.ts @@ -6,8 +6,8 @@ export * from './lib/ngx-fudis.module'; export { ActionsDirective } from './lib/directives/content-projection/actions/actions.directive'; -// export { AlertComponent } from './lib/components/alert/alert/alert.component'; -// export { AlertGroupComponent } from './lib/components/alert/alert-group/alert-group.component'; +export { AlertComponent } from './lib/components/alert/alert/alert.component'; +export { AlertGroupComponent } from './lib/components/alert/alert-group/alert-group.component'; export { AutocompleteComponent } from './lib/components/form/autocomplete/autocomplete.component'; export { BadgeComponent } from './lib/components/badge/badge.component'; export { BodyTextComponent } from './lib/components/typography/body-text/body-text.component'; @@ -38,7 +38,7 @@ export { DescriptionListComponent } from './lib/components/description-list/desc export { DescriptionListItemComponent } from './lib/components/description-list/description-list-item/description-list-item.component'; export { DescriptionListItemDetailsComponent } from './lib/components/description-list/description-list-item/description-list-item-details/description-list-item-details.component'; export { DescriptionListItemTermComponent } from './lib/components/description-list/description-list-item/description-list-item-term/description-list-item-term.component'; -// export { FudisAlertService } from './lib/services/alert/alert.service'; +export { FudisAlertService } from './lib/services/alert/alert.service'; export { FudisBreakpointService } from './lib/services/breakpoint/breakpoint.service'; export { FudisDialogService } from './lib/services/dialog/dialog.service'; export { FudisGridService } from './lib/services/grid/grid.service'; diff --git a/test/visual-regression/alert-group.spec.ts b/test/visual-regression/alert-group.spec.ts new file mode 100644 index 000000000..d644ed4d0 --- /dev/null +++ b/test/visual-regression/alert-group.spec.ts @@ -0,0 +1,30 @@ +import test, { expect } from "@playwright/test"; + +test("alert group default", async ({ page }) => { + await page.goto( + "/iframe.html?args=position:absolute&id=components-alert-group--example&viewMode=story", + ); + await expect(page).toHaveScreenshot("1-init.png"); + + // Add two more alerts + await page.getByTestId("fudis-button-1").click(); + await page.getByTestId("fudis-button-2").click(); + + // Close two previous alerts + await page.getByTestId("fudis-alert-1-button").click(); + await page.getByTestId("fudis-alert-2-button").click(); + await expect(page).toHaveScreenshot("2-add-and-remove.png"); + + // Open dialog + await page.getByTestId("fudis-button-7").click(); + await page.getByTestId("fudis-alert-3-button").click(); // Dismiss one Alert + await page.keyboard.press("Tab"); // Tab away from the last Alert + await page.keyboard.press("Tab"); // Tab to Dialog OK button + await page.keyboard.press("Tab"); // Tab to the first Alert since we are inside focus trap + await expect(page).toHaveScreenshot("3-open-dialog-and-dismiss-alert.png"); + await page.keyboard.press("Escape"); // Close dialog + + // Dismiss all alerts + await page.getByTestId("fudis-button-6").click(); + await expect(page).toHaveScreenshot("4-dismiss-all.png"); +}); diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Chrome-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Chrome-Big-Landscape-linux.png new file mode 100644 index 000000000..b46f99f7f Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Chrome-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Chrome-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Chrome-linux.png new file mode 100644 index 000000000..9e97001ea Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Chrome-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Safari-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Safari-Big-Landscape-linux.png new file mode 100644 index 000000000..25a2d1785 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Safari-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Safari-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Safari-linux.png new file mode 100644 index 000000000..6fe4f209f Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-Mobile-Safari-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-chromium-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-chromium-linux.png new file mode 100644 index 000000000..dcaa4a432 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-chromium-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-firefox-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-firefox-linux.png new file mode 100644 index 000000000..a5a3dd606 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-firefox-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/1-init-webkit-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-webkit-linux.png new file mode 100644 index 000000000..462caec08 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/1-init-webkit-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Chrome-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Chrome-Big-Landscape-linux.png new file mode 100644 index 000000000..9650f105b Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Chrome-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Chrome-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Chrome-linux.png new file mode 100644 index 000000000..980a52a7c Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Chrome-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Safari-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Safari-Big-Landscape-linux.png new file mode 100644 index 000000000..c35802ff4 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Safari-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Safari-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Safari-linux.png new file mode 100644 index 000000000..0381502ce Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-Mobile-Safari-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-chromium-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-chromium-linux.png new file mode 100644 index 000000000..949153e07 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-chromium-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-firefox-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-firefox-linux.png new file mode 100644 index 000000000..cfe57218b Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-firefox-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-webkit-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-webkit-linux.png new file mode 100644 index 000000000..c2427813b Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/2-add-and-remove-webkit-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Chrome-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Chrome-Big-Landscape-linux.png new file mode 100644 index 000000000..9c8efde72 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Chrome-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Chrome-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Chrome-linux.png new file mode 100644 index 000000000..1f0a77f85 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Chrome-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Safari-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Safari-Big-Landscape-linux.png new file mode 100644 index 000000000..fe5ae5754 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Safari-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Safari-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Safari-linux.png new file mode 100644 index 000000000..c7b996af5 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-Mobile-Safari-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-chromium-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-chromium-linux.png new file mode 100644 index 000000000..072ef4d4a Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-chromium-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-firefox-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-firefox-linux.png new file mode 100644 index 000000000..c3443ae88 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-firefox-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-webkit-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-webkit-linux.png new file mode 100644 index 000000000..3bb39320a Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/3-open-dialog-and-dismiss-alert-webkit-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Chrome-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Chrome-Big-Landscape-linux.png new file mode 100644 index 000000000..cc38dd128 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Chrome-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Chrome-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Chrome-linux.png new file mode 100644 index 000000000..17f23e93a Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Chrome-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Safari-Big-Landscape-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Safari-Big-Landscape-linux.png new file mode 100644 index 000000000..8c960a074 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Safari-Big-Landscape-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Safari-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Safari-linux.png new file mode 100644 index 000000000..472bfbf12 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-Mobile-Safari-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-chromium-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-chromium-linux.png new file mode 100644 index 000000000..4e015faa3 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-chromium-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-firefox-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-firefox-linux.png new file mode 100644 index 000000000..4a24cff7d Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-firefox-linux.png differ diff --git a/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-webkit-linux.png b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-webkit-linux.png new file mode 100644 index 000000000..5b1846cf2 Binary files /dev/null and b/test/visual-regression/alert-group.spec.ts-snapshots/4-dismiss-all-webkit-linux.png differ