From ea3b525d17face29007782e110aecd12a6f18ded Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 15:38:20 +0900 Subject: [PATCH 01/23] Create a secondary entry for the experimental alert --- libs/designsystem/alert-experimental/index.ts | 1 + .../alert-experimental/ng-packge.json | 1 + .../src/alert.component.html | 29 +++++ .../src/alert.component.scss | 21 ++++ .../src/alert.component.spec.ts | 100 ++++++++++++++++++ .../alert-experimental/src/alert.component.ts | 80 ++++++++++++++ .../src/config/alert-config.ts | 19 ++++ .../alert-experimental/src/index.ts | 1 + .../alert-experimental/src/public_api.ts | 1 + .../src/services/alert.controller.spec.ts | 89 ++++++++++++++++ .../src/services/alert.controller.ts | 56 ++++++++++ libs/designsystem/src/lib/index.ts | 1 + 12 files changed, 399 insertions(+) create mode 100644 libs/designsystem/alert-experimental/index.ts create mode 100644 libs/designsystem/alert-experimental/ng-packge.json create mode 100644 libs/designsystem/alert-experimental/src/alert.component.html create mode 100644 libs/designsystem/alert-experimental/src/alert.component.scss create mode 100644 libs/designsystem/alert-experimental/src/alert.component.spec.ts create mode 100644 libs/designsystem/alert-experimental/src/alert.component.ts create mode 100644 libs/designsystem/alert-experimental/src/config/alert-config.ts create mode 100644 libs/designsystem/alert-experimental/src/index.ts create mode 100644 libs/designsystem/alert-experimental/src/public_api.ts create mode 100644 libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts create mode 100644 libs/designsystem/alert-experimental/src/services/alert.controller.ts diff --git a/libs/designsystem/alert-experimental/index.ts b/libs/designsystem/alert-experimental/index.ts new file mode 100644 index 0000000000..cba1843545 --- /dev/null +++ b/libs/designsystem/alert-experimental/index.ts @@ -0,0 +1 @@ +export * from './src/index'; diff --git a/libs/designsystem/alert-experimental/ng-packge.json b/libs/designsystem/alert-experimental/ng-packge.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/libs/designsystem/alert-experimental/ng-packge.json @@ -0,0 +1 @@ +{} diff --git a/libs/designsystem/alert-experimental/src/alert.component.html b/libs/designsystem/alert-experimental/src/alert.component.html new file mode 100644 index 0000000000..536f29b035 --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.component.html @@ -0,0 +1,29 @@ +
+ +
+ + +
+
diff --git a/libs/designsystem/alert-experimental/src/alert.component.scss b/libs/designsystem/alert-experimental/src/alert.component.scss new file mode 100644 index 0000000000..fae2f3123e --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.component.scss @@ -0,0 +1,21 @@ +@use '@kirbydesign/core/src/scss/utils'; + +article { + overflow: hidden; + padding: utils.size('s'); + padding-top: utils.size('m'); + @include utils.media(' { + let spectator: SpectatorHost; + + const createHost = createHostFactory({ + component: AlertComponent, + }); + + beforeEach(() => { + spectator = createHost(` + + + `); + }); + + it('should create', () => { + expect(spectator.component).toBeTruthy(); + }); + + describe('ok button', () => { + let okButton: HTMLElement; + + beforeEach(() => { + okButton = spectator.query('.ok-btn'); + }); + it('should render', () => { + const expected = 'Test OK Button Text'; + + expect(spectator.component.okBtn).toEqual(expected); + expect(okButton).toHaveText(expected); + }); + + it('should support isDestructive', () => { + spectator.setInput({ okBtnIsDestructive: true }); + + expect(okButton).toBeDefined(); + expect(okButton).toHaveClass('destructive'); + }); + + it('should default to not being destructive', () => { + expect(okButton).toBeDefined(); + expect(okButton).not.toHaveClass('destructive'); + }); + + it('should have large ok button when no cancel button', () => { + spectator.setInput({ cancelBtn: null }); + + expect(okButton).toHaveClass('lg'); + }); + + it('should have success colors on button', () => { + expect(okButton).toHaveComputedStyle({ + 'background-color': getColor('success'), + color: getColor('success', 'contrast'), + }); + }); + + it('should have default size when cancel button', () => { + expect(okButton.attributes['ng-reflect-size']).toBeUndefined(); + }); + }); + + describe('cancel button', () => { + let cancelButton: HTMLElement; + + beforeEach(() => { + cancelButton = spectator.query('.cancel-btn'); + }); + it('should render', () => { + const expected = 'Test Cancel Button Text'; + + expect(spectator.component.cancelBtn).toEqual(expected); + expect(cancelButton).toHaveText(expected); + }); + + it('should not render when cancelBtn not set', () => { + spectator.setInput({ cancelBtn: null }); + + expect(spectator.query('.cancel-btn')).toBeNull(); + }); + }); + describe('icon', () => { + it('should render', () => { + spectator.setInput({ iconName: 'warning' }); + const icon: HTMLElement = spectator.query('.icon-outline'); + + expect(icon).not.toBeNull(); + }); + }); +}); diff --git a/libs/designsystem/alert-experimental/src/alert.component.ts b/libs/designsystem/alert-experimental/src/alert.component.ts new file mode 100644 index 0000000000..b8439a7a95 --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.component.ts @@ -0,0 +1,80 @@ +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + ViewChild, +} from '@angular/core'; +import { WindowRef } from '@kirbydesign/designsystem/types'; +import { Observable, of } from 'rxjs'; +import { IconModule } from '@kirbydesign/designsystem/icon'; +import { ThemeColorDirective } from '@kirbydesign/designsystem/shared'; +import { EmptyStateModule } from '@kirbydesign/designsystem/empty-state'; +import { ButtonComponent } from '@kirbydesign/designsystem/button'; + +@Component({ + standalone: true, + imports: [ + IconModule, + EmptyStateModule, + ButtonComponent, + CommonModule, + EmptyStateModule, + ThemeColorDirective, + ], + selector: 'kirby-alert-experimental', + templateUrl: './alert.component.html', + styleUrls: ['./alert.component.scss'], + // eslint-disable-next-line @angular-eslint/no-host-metadata-property + host: { '[class.ion-page]': 'false' }, //Ensure ion-page class doesn't get applied by Ionic Modal Controller + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AlertExperimentalComponent implements AfterViewInit { + readonly BLUR_WRAPPER_DELAY_IN_MS = 50; + @ViewChild('alertWrapper', { static: true }) private alertWrapper: ElementRef; + private scrollY: number = Math.abs(this.windowRef.nativeWindow.scrollY); + + title$: Observable; + @Input() + set title(title: string | Observable) { + this.title$ = typeof title === 'string' ? of(title) : title; + } + + message$: Observable; + @Input() + set message(message: string & Observable) { + this.message$ = typeof message === 'string' ? of(message) : message; + } + + @Input() iconName: string; + @Input() iconThemeColor: string; + @Input() okBtn: string; + @Input() okBtnIsDestructive: boolean; + @Input() cancelBtn: string; + + constructor(private elementRef: ElementRef, private windowRef: WindowRef) {} + + ngAfterViewInit(): void { + setTimeout(() => { + this.alertWrapper.nativeElement.focus(); + this.alertWrapper.nativeElement.blur(); + }, this.BLUR_WRAPPER_DELAY_IN_MS); + } + + onFocusChange() { + // This fixes an undesired scroll behaviour occurring on keyboard-tabbing + this.windowRef.nativeWindow.scrollTo({ top: this.scrollY }); + } + + onCancel() { + const ionModalElement = this.elementRef.nativeElement.closest('ion-modal'); + ionModalElement && ionModalElement.dismiss(false); + } + + onOk() { + const ionModalElement = this.elementRef.nativeElement.closest('ion-modal'); + ionModalElement && ionModalElement.dismiss(true); + } +} diff --git a/libs/designsystem/alert-experimental/src/config/alert-config.ts b/libs/designsystem/alert-experimental/src/config/alert-config.ts new file mode 100644 index 0000000000..3883cf07eb --- /dev/null +++ b/libs/designsystem/alert-experimental/src/config/alert-config.ts @@ -0,0 +1,19 @@ +import { Observable } from 'rxjs'; + +export interface AlertConfig { + title: string | Observable; + message?: string | Observable; + cancelBtn?: string; + + icon?: { + name: string; + themeColor?: string; + }; + + okBtn?: + | string + | { + text: string; + isDestructive: boolean; + }; +} diff --git a/libs/designsystem/alert-experimental/src/index.ts b/libs/designsystem/alert-experimental/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/designsystem/alert-experimental/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts new file mode 100644 index 0000000000..6b6f3b9b1e --- /dev/null +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -0,0 +1 @@ +export { AlertController } from './services/alert.controller'; diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts new file mode 100644 index 0000000000..895b521db2 --- /dev/null +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; +import { ModalController as IonicModalController } from '@ionic/angular'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; + +import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; + +import { WindowRef } from '@kirbydesign/designsystem/types'; +import { TestHelper } from '@kirbydesign/designsystem/testing'; + +import { Overlay } from '../../modal.interfaces'; +import { AlertHelper } from './alert.helper'; + +@Component({ + template: ` +

Dummy Component

+ `, +}) +class EmbeddedDummyComponent {} + +describe('AlertHelper', () => { + let spectator: SpectatorService; + let alertHelper: AlertHelper; + const backdropOpacity = '0.4'; + + const createService = createServiceFactory({ + service: AlertHelper, + imports: [TestHelper.ionicModuleForTest], + providers: [ + { + provide: WindowRef, + useValue: { nativeWindow: window }, + }, + ], + }); + + beforeEach(() => { + spectator = createService(); + alertHelper = spectator.service; + }); + + describe('showAlert', () => { + let overlay: Overlay; + let ionModal: HTMLIonModalElement; + let backdrop: HTMLIonBackdropElement; + let ionModalController: IonicModalController; + + beforeEach(async () => { + ionModalController = spectator.inject(IonicModalController); + overlay = await alertHelper.showAlert({ title: 'Alert' }); + ionModal = await ionModalController.getTop(); + expect(ionModal).toBeTruthy(); + backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); + expect(backdrop).toBeTruthy(); + }); + + afterEach(async () => { + await overlay.dismiss(); + }); + + it('alert should have correct backdrop style', () => { + expect(ionModal).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); + }); + + it('modal wrapper should have correct max width', () => { + const modalWrapper = ionModal.shadowRoot.querySelector('.modal-wrapper'); + expect(modalWrapper).toHaveComputedStyle({ + 'max-width': DesignTokenHelper.compactModalMaxWidth(), + }); + }); + + it('alert should have correct backdrop style when opened on top of a modal', async () => { + await overlay.dismiss(); + const ionModalElement = await ionModalController.create({ + component: EmbeddedDummyComponent, + }); + await ionModalElement.present(); + const modalIonModal = await ionModalController.getTop(); + expect(modalIonModal).toBeTruthy(); + + overlay = await alertHelper.showAlert({ title: 'Alert on top of modal' }); + + ionModal = await ionModalController.getTop(); + expect(ionModal).toBeTruthy(); + backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); + expect(backdrop).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); + await ionModalElement.dismiss(); + }); + }); +}); diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts new file mode 100644 index 0000000000..d6f9c9bfcd --- /dev/null +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { Overlay } from '../../modal.interfaces'; + +import { AlertComponent } from '../alert/alert.component'; +import { AlertConfig } from '../alert/config/alert-config'; + +@Injectable() +export class AlertController { + constructor(private ionicModalController: ModalController) {} + + public async showAlert(config: AlertConfig): Promise { + const ionModal = await this.ionicModalController.create({ + component: AlertComponent, + componentProps: this.getComponentProps(config), + cssClass: ['kirby-overlay', 'kirby-alert'], + mode: 'ios', + backdropDismiss: false, + }); + + await ionModal.present(); + return { + dismiss: ionModal.dismiss.bind(ionModal), + onWillDismiss: ionModal.onWillDismiss(), + onDidDismiss: ionModal.onDidDismiss(), + }; + } + + private getComponentProps(config: AlertConfig) { + return { + ...config, + okBtn: this.getOkBtn(config), + cancelBtn: config.cancelBtn, + okBtnIsDestructive: this.getOkBtnIsDestructive(config), + iconName: config.icon && config.icon.name, + iconThemeColor: config.icon && config.icon.themeColor, + }; + } + + private getOkBtn(config: AlertConfig) { + let text: string; + + if (config.okBtn) { + if (typeof config.okBtn === 'string') { + text = config.okBtn; + } else { + text = config.okBtn.text; + } + } + return text; + } + + getOkBtnIsDestructive(config) { + return typeof config.okBtn === 'object' ? config.okBtn.isDestructive : undefined; + } +} diff --git a/libs/designsystem/src/lib/index.ts b/libs/designsystem/src/lib/index.ts index 1ffb11f176..455b2222c0 100644 --- a/libs/designsystem/src/lib/index.ts +++ b/libs/designsystem/src/lib/index.ts @@ -44,6 +44,7 @@ export * from '@kirbydesign/designsystem/toggle'; export * from '@kirbydesign/designsystem/toggle-button'; export * from '@kirbydesign/designsystem/types'; export * from '@kirbydesign/designsystem/chart'; +export * from '@kirbydesign/designsystem/alert-experimental'; export { KirbyModule } from './kirby.module'; export { KirbyExperimentalModule } from './kirby-experimental.module'; From 97df49e1ae46ea1ca15176e8a058726b36d32259 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 15:40:55 +0900 Subject: [PATCH 02/23] Add the alert path to tsconfig files --- apps/cookbook/tsconfig.json | 3 ++- apps/flows/tsconfig.json | 3 ++- libs/designsystem/tsconfig.json | 5 +++-- libs/designsystem/tsconfig.spec.json | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/cookbook/tsconfig.json b/apps/cookbook/tsconfig.json index 3435e6d4b5..76a7dd3737 100644 --- a/apps/cookbook/tsconfig.json +++ b/apps/cookbook/tsconfig.json @@ -56,7 +56,8 @@ "@kirbydesign/designsystem/data-table": ["libs/designsystem/data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], - "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] }, "target": "es2020" }, diff --git a/apps/flows/tsconfig.json b/apps/flows/tsconfig.json index 6eb0531d1d..2daf1a5d17 100644 --- a/apps/flows/tsconfig.json +++ b/apps/flows/tsconfig.json @@ -74,7 +74,8 @@ "@kirbydesign/designsystem/data-table": ["libs/designsystem/data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], - "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] }, "target": "es2020" }, diff --git a/libs/designsystem/tsconfig.json b/libs/designsystem/tsconfig.json index 7da4094683..e28ed96dd3 100644 --- a/libs/designsystem/tsconfig.json +++ b/libs/designsystem/tsconfig.json @@ -14,7 +14,7 @@ "@kirbydesign/designsystem/types": ["types/index.ts"], "@kirbydesign/designsystem/flag": ["flag/index.ts"], "@kirbydesign/designsystem/form-field": ["form-field/index.ts"], - "@kirbydesign/designsystem/grid": ["grid/index.ts"], + "@kirbydesign/designsystem/grid": ["grid/index.ts"], "@kirbydesign/designsystem/kirby-ionic-module": ["kirby-ionic-module/index.ts"], "@kirbydesign/designsystem/spinner": ["spinner/index.ts"], "@kirbydesign/designsystem/item": ["item/index.ts"], @@ -47,7 +47,8 @@ "@kirbydesign/designsystem/fab-sheet": ["fab-sheet/index.ts"], "@kirbydesign/designsystem/data-table": ["data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["reorder-list/index.ts"], - "@kirbydesign/designsystem/toast": ["toast/index.ts"] + "@kirbydesign/designsystem/toast": ["toast/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] }, "esModuleInterop": true, "target": "es2020" diff --git a/libs/designsystem/tsconfig.spec.json b/libs/designsystem/tsconfig.spec.json index 6d6bb26994..e9b6ae859f 100644 --- a/libs/designsystem/tsconfig.spec.json +++ b/libs/designsystem/tsconfig.spec.json @@ -51,7 +51,8 @@ "@kirbydesign/designsystem/fab-sheet": ["fab-sheet/index.ts"], "@kirbydesign/designsystem/data-table": ["data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["reorder-list/index.ts"], - "@kirbydesign/designsystem/toast": ["toast/index.ts"] + "@kirbydesign/designsystem/toast": ["toast/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] } }, "files": ["test.ts"], From 4a231b626f243165cb2b3a122c8aad0b4c3b3a51 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 15:55:02 +0900 Subject: [PATCH 03/23] Refactor alertController from promises to observables --- .../src/services/alert.controller.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index d6f9c9bfcd..4b4985238f 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -1,28 +1,52 @@ import { Injectable } from '@angular/core'; import { ModalController } from '@ionic/angular'; -import { Overlay } from '../../modal.interfaces'; +import { from, Observable, Subject, switchMap, tap } from 'rxjs'; +import { OverlayEventDetail } from '@ionic/core/components'; +import { AlertExperimentalComponent } from '../alert.component'; +import { AlertConfig } from '../config/alert-config'; -import { AlertComponent } from '../alert/alert.component'; -import { AlertConfig } from '../alert/config/alert-config'; +type AlertDismissObservables = { + onWillDismiss: Observable; + onDidDismiss: Observable; +}; @Injectable() export class AlertController { constructor(private ionicModalController: ModalController) {} - public async showAlert(config: AlertConfig): Promise { - const ionModal = await this.ionicModalController.create({ - component: AlertComponent, - componentProps: this.getComponentProps(config), - cssClass: ['kirby-overlay', 'kirby-alert'], - mode: 'ios', - backdropDismiss: false, - }); + public showAlert(config: AlertConfig): AlertDismissObservables { + const $onWillDismiss = new Subject(); + const onWillDismiss$ = $onWillDismiss.asObservable(); + + const $onDidDismiss = new Subject(); + const onDidDismiss$ = $onDidDismiss.asObservable(); + + const modal$ = from( + this.ionicModalController.create({ + component: AlertExperimentalComponent, + componentProps: this.getComponentProps(config), + cssClass: ['kirby-overlay', 'kirby-alert'], + mode: 'ios', + backdropDismiss: false, + }) + ); + + modal$ + .pipe( + tap((modal) => from(modal.present())), + switchMap((modal) => modal.onWillDismiss()) + ) + .subscribe((res) => { + $onWillDismiss.next(res); + $onWillDismiss.complete(); + + $onDidDismiss.next(res); + $onDidDismiss.complete(); + }); - await ionModal.present(); return { - dismiss: ionModal.dismiss.bind(ionModal), - onWillDismiss: ionModal.onWillDismiss(), - onDidDismiss: ionModal.onDidDismiss(), + onWillDismiss: onWillDismiss$, + onDidDismiss: onDidDismiss$, }; } From b1b64c9fb2edf4014881468d44d9274489b421cd Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 16:48:44 +0900 Subject: [PATCH 04/23] Fix broken tests --- .../src/alert.component.spec.ts | 12 +++--- .../src/services/alert.controller.spec.ts | 39 +++++-------------- .../button/src/button.component.scss | 3 +- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/alert.component.spec.ts b/libs/designsystem/alert-experimental/src/alert.component.spec.ts index ebfa413e9d..f2dbad6e3a 100644 --- a/libs/designsystem/alert-experimental/src/alert.component.spec.ts +++ b/libs/designsystem/alert-experimental/src/alert.component.spec.ts @@ -2,24 +2,24 @@ import { createHostFactory, SpectatorHost } from '@ngneat/spectator'; import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; -import { AlertComponent } from './alert.component'; +import { AlertExperimentalComponent } from './alert.component'; const getColor = DesignTokenHelper.getColor; -describe('AlertComponent', () => { - let spectator: SpectatorHost; +describe('AlertExperimentalComponent', () => { + let spectator: SpectatorHost; const createHost = createHostFactory({ - component: AlertComponent, + component: AlertExperimentalComponent, }); beforeEach(() => { spectator = createHost(` - - + `); }); diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts index 895b521db2..d0702a771c 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -7,8 +7,7 @@ import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; import { WindowRef } from '@kirbydesign/designsystem/types'; import { TestHelper } from '@kirbydesign/designsystem/testing'; -import { Overlay } from '../../modal.interfaces'; -import { AlertHelper } from './alert.helper'; +import { AlertController } from './alert.controller'; @Component({ template: ` @@ -17,13 +16,13 @@ import { AlertHelper } from './alert.helper'; }) class EmbeddedDummyComponent {} -describe('AlertHelper', () => { - let spectator: SpectatorService; - let alertHelper: AlertHelper; +describe('AlertExperimentalController', () => { + let spectator: SpectatorService; + let alertController: AlertController; const backdropOpacity = '0.4'; const createService = createServiceFactory({ - service: AlertHelper, + service: AlertController, imports: [TestHelper.ionicModuleForTest], providers: [ { @@ -35,18 +34,18 @@ describe('AlertHelper', () => { beforeEach(() => { spectator = createService(); - alertHelper = spectator.service; + alertController = spectator.service; }); describe('showAlert', () => { - let overlay: Overlay; let ionModal: HTMLIonModalElement; let backdrop: HTMLIonBackdropElement; let ionModalController: IonicModalController; beforeEach(async () => { ionModalController = spectator.inject(IonicModalController); - overlay = await alertHelper.showAlert({ title: 'Alert' }); + await alertController.showAlert({ title: 'Alert' }); + await TestHelper.waitForTimeout(50); ionModal = await ionModalController.getTop(); expect(ionModal).toBeTruthy(); backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); @@ -54,10 +53,10 @@ describe('AlertHelper', () => { }); afterEach(async () => { - await overlay.dismiss(); + await ionModal.dismiss(); }); - it('alert should have correct backdrop style', () => { + it('alert should have correct backdrop style', async () => { expect(ionModal).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); }); @@ -67,23 +66,5 @@ describe('AlertHelper', () => { 'max-width': DesignTokenHelper.compactModalMaxWidth(), }); }); - - it('alert should have correct backdrop style when opened on top of a modal', async () => { - await overlay.dismiss(); - const ionModalElement = await ionModalController.create({ - component: EmbeddedDummyComponent, - }); - await ionModalElement.present(); - const modalIonModal = await ionModalController.getTop(); - expect(modalIonModal).toBeTruthy(); - - overlay = await alertHelper.showAlert({ title: 'Alert on top of modal' }); - - ionModal = await ionModalController.getTop(); - expect(ionModal).toBeTruthy(); - backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); - expect(backdrop).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); - await ionModalElement.dismiss(); - }); }); }); diff --git a/libs/designsystem/button/src/button.component.scss b/libs/designsystem/button/src/button.component.scss index b715cd4e97..1c3bf9a496 100644 --- a/libs/designsystem/button/src/button.component.scss +++ b/libs/designsystem/button/src/button.component.scss @@ -244,7 +244,8 @@ $button-width: ( margin-inline-start: utils.size('s'); } -:host-context(kirby-alert).ok-btn { +:host-context(kirby-alert).ok-btn, +:host-context(kirby-alert-experimental).ok-btn { --kirby-button-background-color: #{utils.get-color('success')}; --kirby-button-color: #{utils.get-color('success-contrast')}; } From 84695fb08954bf44eab8a2e448dea98fd3c04188 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 17:17:15 +0900 Subject: [PATCH 05/23] Create showcase component --- ...alert-experimental-showcase.component.html | 44 +++++++++++++++++++ .../alert-experimental-showcase.component.ts | 44 +++++++++++++++++++ .../src/app/showcase/showcase.routes.ts | 5 +++ 3 files changed, 93 insertions(+) create mode 100644 apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html create mode 100644 apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html new file mode 100644 index 0000000000..7d9f1f0ebb --- /dev/null +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html @@ -0,0 +1,44 @@ +
+ +

 

+

+ To show an alert, inject the Kirby + modalController + in your constructor, create an + AlertConfig + and pass it to + modalController.showAlert + . + +

+

+ title + and + message + properties in + AlertConfig + also accept an Observable for dynamic values: +
+ + (see "Example on github" for implementation details of the + remainingSeconds$ + timer) + + : + +

+

+ [Optional] + If you need to obtain data back from the alert, you can pass a callback function: + + this.modalController.showAlert(config, onAlertClose); + +private onAlertClose(result: boolean) {{ '{' }} + ... +{{ '}' }} +

+

Alert config properties:

+ +
diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts new file mode 100644 index 0000000000..417b75f50a --- /dev/null +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { AlertExampleComponent } from '~/app/examples/alert-example/alert-example.component'; +import { ApiDescriptionProperty } from '~/app/shared/api-description/api-description-properties/api-description-properties.component'; + +@Component({ + selector: 'cookbook-alert-showcase', + templateUrl: './alert-experimental-showcase.component.html', + preserveWhitespaces: true, +}) +export class AlertExperimentalShowcaseComponent { + alertConfigWithIcon: string = AlertExampleComponent.alertConfigWithIcon; + alertConfigWithDynamicValues: string = AlertExampleComponent.alertConfigWithDynamicValues; + properties: ApiDescriptionProperty[] = [ + { + name: 'title', + description: 'The title of the alert', + type: ['string | Observable'], + }, + { + name: 'message', + description: + "(Optional) The message shown under the title (or icon if specified). Use '\\n' for newline.", + type: ['string | Observable'], + }, + { + name: 'icon', + description: '(Optional) Icon to be shown below the title', + type: ['{ name: string }', '{ name: string, themeColor: string }'], + }, + { + name: 'okBtn', + description: + '(Optional) Defines the text that will appear on the OK button and if it should be destructive', + defaultValue: 'OK', + type: ['string', '{ text: string, isDestructive: boolean }'], + }, + { + name: 'cancelBtn', + description: + '(Optional) The text that will appear on the cancel button. If not defined the cancel button will not be shown.', + type: ['string'], + }, + ]; +} diff --git a/apps/cookbook/src/app/showcase/showcase.routes.ts b/apps/cookbook/src/app/showcase/showcase.routes.ts index b79b83c710..3bee563519 100644 --- a/apps/cookbook/src/app/showcase/showcase.routes.ts +++ b/apps/cookbook/src/app/showcase/showcase.routes.ts @@ -10,6 +10,7 @@ import { ModalRoutingExperimentalExamplePage2Component } from '../examples/modal import { AccordionShowcaseComponent } from './accordion-showcase/accordion-showcase.component'; import { ActionSheetShowcaseComponent } from './action-sheet-showcase/action-sheet-showcase.component'; import { AlertShowcaseComponent } from './alert-showcase/alert-showcase.component'; +import { AlertExperimentalShowcaseComponent } from './alert-experimental-showcase/alert-experimental-showcase.component'; import { AvatarShowcaseComponent } from './avatar-showcase/avatar-showcase.component'; import { BadgeShowcaseComponent } from './badge-showcase/badge-showcase.component'; import { ButtonShowcaseComponent } from './button-showcase/button-showcase.component'; @@ -227,6 +228,10 @@ export const routes: Routes = [ path: 'alert', component: AlertShowcaseComponent, }, + { + path: 'alert-experimental', + component: AlertExperimentalShowcaseComponent, + }, { path: 'badge', component: BadgeShowcaseComponent, From 506dcf465a52b2acb47e230d66fd8c54664bb36b Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 17:18:06 +0900 Subject: [PATCH 06/23] Rename the controller to include 'Experimental' --- .../src/services/alert.controller.spec.ts | 8 ++++---- .../alert-experimental/src/services/alert.controller.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts index d0702a771c..eef396f350 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -7,7 +7,7 @@ import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; import { WindowRef } from '@kirbydesign/designsystem/types'; import { TestHelper } from '@kirbydesign/designsystem/testing'; -import { AlertController } from './alert.controller'; +import { AlertExperimentalController } from './alert.controller'; @Component({ template: ` @@ -17,12 +17,12 @@ import { AlertController } from './alert.controller'; class EmbeddedDummyComponent {} describe('AlertExperimentalController', () => { - let spectator: SpectatorService; - let alertController: AlertController; + let spectator: SpectatorService; + let alertController: AlertExperimentalController; const backdropOpacity = '0.4'; const createService = createServiceFactory({ - service: AlertController, + service: AlertExperimentalController, imports: [TestHelper.ionicModuleForTest], providers: [ { diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index 4b4985238f..85ca47a8f1 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -11,7 +11,7 @@ type AlertDismissObservables = { }; @Injectable() -export class AlertController { +export class AlertExperimentalController { constructor(private ionicModalController: ModalController) {} public showAlert(config: AlertConfig): AlertDismissObservables { From d31c9a28a03267df2d917e65db287978360e4fcd Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 17:28:08 +0900 Subject: [PATCH 07/23] Fix bug that prevents the bookcook from running --- apps/cookbook/tsconfig.json | 4 +++- apps/flows/tsconfig.json | 4 +++- .../alert-experimental/{ng-packge.json => ng-package.json} | 0 libs/designsystem/alert-experimental/src/public_api.ts | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) rename libs/designsystem/alert-experimental/{ng-packge.json => ng-package.json} (100%) diff --git a/apps/cookbook/tsconfig.json b/apps/cookbook/tsconfig.json index 76a7dd3737..58789a711c 100644 --- a/apps/cookbook/tsconfig.json +++ b/apps/cookbook/tsconfig.json @@ -57,7 +57,9 @@ "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], - "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] + "@kirbydesign/designsystem/alert-experimental": [ + "libs/designsystem/alert-experimental/index.ts" + ] }, "target": "es2020" }, diff --git a/apps/flows/tsconfig.json b/apps/flows/tsconfig.json index 2daf1a5d17..93dd760165 100644 --- a/apps/flows/tsconfig.json +++ b/apps/flows/tsconfig.json @@ -75,7 +75,9 @@ "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], - "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] + "@kirbydesign/designsystem/alert-experimental": [ + "libs/designsystem/alert-experimental/index.ts" + ] }, "target": "es2020" }, diff --git a/libs/designsystem/alert-experimental/ng-packge.json b/libs/designsystem/alert-experimental/ng-package.json similarity index 100% rename from libs/designsystem/alert-experimental/ng-packge.json rename to libs/designsystem/alert-experimental/ng-package.json diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts index 6b6f3b9b1e..8d1def3ca0 100644 --- a/libs/designsystem/alert-experimental/src/public_api.ts +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -1 +1,2 @@ -export { AlertController } from './services/alert.controller'; +export { AlertExperimentalController } from './services/alert.controller'; +export { AlertExperimentalComponent } from './alert.component'; From 50d3450fd4fa8370c4c051f4496ea835787e3861 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 18:57:02 +0900 Subject: [PATCH 08/23] WIP: update cookbook example --- .../alert-experimental-example.component.html | 8 + .../alert-experimental-example.component.ts | 193 ++++++++++++++++++ .../src/app/examples/examples.common.ts | 2 + ...alert-experimental-showcase.component.html | 2 +- .../src/app/showcase/showcase.common.ts | 2 + .../src/config/alert-config.ts | 2 +- .../alert-experimental/src/public_api.ts | 1 + .../src/services/alert.controller.ts | 8 +- 8 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html create mode 100644 apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html new file mode 100644 index 0000000000..41d8ea934e --- /dev/null +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts new file mode 100644 index 0000000000..79b0081ea0 --- /dev/null +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts @@ -0,0 +1,193 @@ +import { Component } from '@angular/core'; +import { of, Subject, timer } from 'rxjs'; +import { map, takeUntil, takeWhile } from 'rxjs/operators'; + +import { + AlertExperimentalConfig, + AlertExperimentalController, +} from '@kirbydesign/designsystem/alert-experimental'; +import { ToastConfig, ToastController } from '@kirbydesign/designsystem'; + +const alertConfigWithIcon = { + title: 'Alert With Icon', + message: 'This message can have more than 1 line.', + okBtn: 'I agree', + cancelBtn: 'Take me back', + icon: { name: 'warning', themeColor: 'warning' }, +}; + +export const observableCodeSnippet = `showAlert() { + const alert = this.alertController.showAlert(config); + alert?.onWillDismiss.subscribe((response) => { + const { role, data } = response; + ... + }); + alert?.onDidDismiss.subscribe((response) => { + const { role, data } = response; + ... + }); +}`; + +@Component({ + selector: 'cookbook-alert-experimental-example', + templateUrl: './alert-experimental-example.component.html', + styles: [':host { display: block; }'], +}) +export class AlertExperimentalExampleComponent { + static readonly alertConfigWithIcon = `const config: AlertConfig = ${AlertExperimentalExampleComponent.stringify( + alertConfigWithIcon + )} + +this.alertController.showAlert(config);`; + + private static stringify(value: any): string { + return JSON.stringify(value, null, '\t') + .replace(/"(\w+)\":/g, '$1:') + .replace(/"/g, "'"); + } + + private alertClose$: Subject = new Subject(); + + static readonly alertConfigWithDynamicValues = `const title$ = of('Need more time?'); + const message$ = remainingSeconds$.pipe( + map((remainingSeconds) => \`Time remaining: \${remainingSeconds}\`) + ); + const config: AlertConfig = { + title: title$, + icon: { + name: 'clock', + themeColor: 'warning', + }, + message: message$, + okBtn: 'Logout', + cancelBtn: 'Take me back', + }; + + this.alertController.showAlert(config);`; + constructor( + private alertController: AlertExperimentalController, + private toastController: ToastController + ) {} + + showAlert() { + const config: AlertExperimentalConfig = { + title: 'Default Alert', + message: 'The default alert is just a title, a message, an OK and (optional) cancel button', + okBtn: 'I agree', + cancelBtn: 'Take me back', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showAlertWithIcon() { + const alert = this.alertController.showAlert(alertConfigWithIcon); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showAlertWithoutCancel() { + const config: AlertExperimentalConfig = { + title: 'Alert Without Cancel', + message: 'This is an alert that can only be acknowledged (no cancel option)', + okBtn: 'I understand', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showDestructiveAlert() { + const config: AlertExperimentalConfig = { + title: 'Desctructive Alert', + message: + 'This is to indicate that something destructive will happen when clicking the OK button', + cancelBtn: 'Get me out of here', + okBtn: { text: 'Confirm', isDestructive: true }, + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertDestructiveClosed(result.data); + }); + } + + showAlertWithNewline() { + const config: AlertExperimentalConfig = { + title: 'Alert with newline', + message: 'This is message one.\n\nThis is message two.', + okBtn: 'I agree', + cancelBtn: 'Take me back', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showAlertWithDynamicValues() { + const countdownTimeInSeconds = 60; + const countdownTimeInMs = countdownTimeInSeconds * 1000; + const intervalInMs = 1000; + const toRemainingTimeInMs = (count: number) => countdownTimeInMs - count * intervalInMs; + const toSeconds = (timeInMs: number) => Math.ceil(timeInMs / 1000); + + const remainingTime$ = timer(0, intervalInMs).pipe( + map(toRemainingTimeInMs), + takeUntil(this.alertClose$), + takeWhile((countdownTimeInMs) => countdownTimeInMs >= 0) + ); + + const title$ = of('Need more time?'); + const message$ = remainingTime$.pipe( + map((remainingTimeInMs) => `Time remaining: ${toSeconds(remainingTimeInMs)}`) + ); + const config: AlertExperimentalConfig = { + title: title$, + icon: { + name: 'clock', + themeColor: 'warning', + }, + message: message$, + okBtn: 'Logout', + cancelBtn: 'Take me back', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + private onAlertClosed(result?: boolean) { + const config: ToastConfig = { + message: `Alert selection: ${result}`, + messageType: result ? 'success' : 'warning', + durationInMs: 1500, + }; + this.toastController.showToast(config); + this.alertClose$.next(); + } + + private onAlertDestructiveClosed(result?: boolean) { + const config: ToastConfig = { + message: result ? 'Message deleted' : 'Nothing happened', + messageType: result ? 'warning' : 'success', + durationInMs: 1500, + }; + this.toastController.showToast(config); + } +} diff --git a/apps/cookbook/src/app/examples/examples.common.ts b/apps/cookbook/src/app/examples/examples.common.ts index ad96d7f92d..41fdf5e7fe 100644 --- a/apps/cookbook/src/app/examples/examples.common.ts +++ b/apps/cookbook/src/app/examples/examples.common.ts @@ -1,6 +1,7 @@ import { AccordionExampleComponent } from './accordion-example/accordion-example.component'; import { ActionSheetExampleComponent } from './action-sheet-example/action-sheet-example.component'; import { AlertExampleComponent } from './alert-example/alert-example.component'; +import { AlertExperimentalExampleComponent } from './alert-experimental-example/alert-experimental-example.component'; import { AvatarExampleComponent } from './avatar-example/avatar-example.component'; import { ButtonExampleComponent } from './button-example/button-example.component'; import { CalendarCardExampleComponent } from './calendar-example/calendar-card-example.component'; @@ -69,6 +70,7 @@ export const COMPONENT_DECLARATIONS: any[] = [ ActionSheetExampleComponent, CheckboxExampleComponent, AlertExampleComponent, + AlertExperimentalExampleComponent, ToastExampleComponent, ToggleExampleComponent, EmptyStateExampleComponent, diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html index 7d9f1f0ebb..d1034dcfc2 100644 --- a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html @@ -1,5 +1,5 @@
- +

 

To show an alert, inject the Kirby diff --git a/apps/cookbook/src/app/showcase/showcase.common.ts b/apps/cookbook/src/app/showcase/showcase.common.ts index 2ac1b10d1d..5d4431e830 100644 --- a/apps/cookbook/src/app/showcase/showcase.common.ts +++ b/apps/cookbook/src/app/showcase/showcase.common.ts @@ -8,6 +8,7 @@ import { ChartExampleConfigBaseBarComponent } from '../examples/charts-example/e import { AccordionShowcaseComponent } from './accordion-showcase/accordion-showcase.component'; import { ActionSheetShowcaseComponent } from './action-sheet-showcase/action-sheet-showcase.component'; import { AlertShowcaseComponent } from './alert-showcase/alert-showcase.component'; +import { AlertExperimentalShowcaseComponent } from './alert-experimental-showcase/alert-experimental-showcase.component'; import { AvatarShowcaseComponent } from './avatar-showcase/avatar-showcase.component'; import { BadgeShowcaseComponent } from './badge-showcase/badge-showcase.component'; import { ButtonShowcaseComponent } from './button-showcase/button-showcase.component'; @@ -83,6 +84,7 @@ export const COMPONENT_EXPORTS: any[] = [ ActionSheetShowcaseComponent, CheckboxShowcaseComponent, AlertShowcaseComponent, + AlertExperimentalShowcaseComponent, ToastShowcaseComponent, ToggleShowcaseComponent, ToggleButtonShowcaseComponent, diff --git a/libs/designsystem/alert-experimental/src/config/alert-config.ts b/libs/designsystem/alert-experimental/src/config/alert-config.ts index 3883cf07eb..cb3b3b84e3 100644 --- a/libs/designsystem/alert-experimental/src/config/alert-config.ts +++ b/libs/designsystem/alert-experimental/src/config/alert-config.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs'; -export interface AlertConfig { +export interface AlertExperimentalConfig { title: string | Observable; message?: string | Observable; cancelBtn?: string; diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts index 8d1def3ca0..2cd2f8f589 100644 --- a/libs/designsystem/alert-experimental/src/public_api.ts +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -1,2 +1,3 @@ export { AlertExperimentalController } from './services/alert.controller'; export { AlertExperimentalComponent } from './alert.component'; +export { AlertExperimentalConfig } from './config/alert-config'; diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index 85ca47a8f1..a9ee002efc 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -3,7 +3,7 @@ import { ModalController } from '@ionic/angular'; import { from, Observable, Subject, switchMap, tap } from 'rxjs'; import { OverlayEventDetail } from '@ionic/core/components'; import { AlertExperimentalComponent } from '../alert.component'; -import { AlertConfig } from '../config/alert-config'; +import { AlertExperimentalConfig } from '../config/alert-config'; type AlertDismissObservables = { onWillDismiss: Observable; @@ -14,7 +14,7 @@ type AlertDismissObservables = { export class AlertExperimentalController { constructor(private ionicModalController: ModalController) {} - public showAlert(config: AlertConfig): AlertDismissObservables { + public showAlert(config: AlertExperimentalConfig): AlertDismissObservables { const $onWillDismiss = new Subject(); const onWillDismiss$ = $onWillDismiss.asObservable(); @@ -50,7 +50,7 @@ export class AlertExperimentalController { }; } - private getComponentProps(config: AlertConfig) { + private getComponentProps(config: AlertExperimentalConfig) { return { ...config, okBtn: this.getOkBtn(config), @@ -61,7 +61,7 @@ export class AlertExperimentalController { }; } - private getOkBtn(config: AlertConfig) { + private getOkBtn(config: AlertExperimentalConfig) { let text: string; if (config.okBtn) { From f1f6bb247e93a4a660e88dcf2b4432c5bc2193d3 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 15:38:20 +0900 Subject: [PATCH 09/23] Create a secondary entry for the experimental alert --- libs/designsystem/alert-experimental/index.ts | 1 + .../alert-experimental/ng-packge.json | 1 + .../src/alert.component.html | 29 +++++ .../src/alert.component.scss | 21 ++++ .../src/alert.component.spec.ts | 100 ++++++++++++++++++ .../alert-experimental/src/alert.component.ts | 80 ++++++++++++++ .../src/config/alert-config.ts | 19 ++++ .../alert-experimental/src/index.ts | 1 + .../alert-experimental/src/public_api.ts | 1 + .../src/services/alert.controller.spec.ts | 89 ++++++++++++++++ .../src/services/alert.controller.ts | 56 ++++++++++ libs/designsystem/src/lib/index.ts | 1 + 12 files changed, 399 insertions(+) create mode 100644 libs/designsystem/alert-experimental/index.ts create mode 100644 libs/designsystem/alert-experimental/ng-packge.json create mode 100644 libs/designsystem/alert-experimental/src/alert.component.html create mode 100644 libs/designsystem/alert-experimental/src/alert.component.scss create mode 100644 libs/designsystem/alert-experimental/src/alert.component.spec.ts create mode 100644 libs/designsystem/alert-experimental/src/alert.component.ts create mode 100644 libs/designsystem/alert-experimental/src/config/alert-config.ts create mode 100644 libs/designsystem/alert-experimental/src/index.ts create mode 100644 libs/designsystem/alert-experimental/src/public_api.ts create mode 100644 libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts create mode 100644 libs/designsystem/alert-experimental/src/services/alert.controller.ts diff --git a/libs/designsystem/alert-experimental/index.ts b/libs/designsystem/alert-experimental/index.ts new file mode 100644 index 0000000000..cba1843545 --- /dev/null +++ b/libs/designsystem/alert-experimental/index.ts @@ -0,0 +1 @@ +export * from './src/index'; diff --git a/libs/designsystem/alert-experimental/ng-packge.json b/libs/designsystem/alert-experimental/ng-packge.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/libs/designsystem/alert-experimental/ng-packge.json @@ -0,0 +1 @@ +{} diff --git a/libs/designsystem/alert-experimental/src/alert.component.html b/libs/designsystem/alert-experimental/src/alert.component.html new file mode 100644 index 0000000000..536f29b035 --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.component.html @@ -0,0 +1,29 @@ +

+ +
+ + +
+
diff --git a/libs/designsystem/alert-experimental/src/alert.component.scss b/libs/designsystem/alert-experimental/src/alert.component.scss new file mode 100644 index 0000000000..fae2f3123e --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.component.scss @@ -0,0 +1,21 @@ +@use '@kirbydesign/core/src/scss/utils'; + +article { + overflow: hidden; + padding: utils.size('s'); + padding-top: utils.size('m'); + @include utils.media(' { + let spectator: SpectatorHost; + + const createHost = createHostFactory({ + component: AlertComponent, + }); + + beforeEach(() => { + spectator = createHost(` + + + `); + }); + + it('should create', () => { + expect(spectator.component).toBeTruthy(); + }); + + describe('ok button', () => { + let okButton: HTMLElement; + + beforeEach(() => { + okButton = spectator.query('.ok-btn'); + }); + it('should render', () => { + const expected = 'Test OK Button Text'; + + expect(spectator.component.okBtn).toEqual(expected); + expect(okButton).toHaveText(expected); + }); + + it('should support isDestructive', () => { + spectator.setInput({ okBtnIsDestructive: true }); + + expect(okButton).toBeDefined(); + expect(okButton).toHaveClass('destructive'); + }); + + it('should default to not being destructive', () => { + expect(okButton).toBeDefined(); + expect(okButton).not.toHaveClass('destructive'); + }); + + it('should have large ok button when no cancel button', () => { + spectator.setInput({ cancelBtn: null }); + + expect(okButton).toHaveClass('lg'); + }); + + it('should have success colors on button', () => { + expect(okButton).toHaveComputedStyle({ + 'background-color': getColor('success'), + color: getColor('success', 'contrast'), + }); + }); + + it('should have default size when cancel button', () => { + expect(okButton.attributes['ng-reflect-size']).toBeUndefined(); + }); + }); + + describe('cancel button', () => { + let cancelButton: HTMLElement; + + beforeEach(() => { + cancelButton = spectator.query('.cancel-btn'); + }); + it('should render', () => { + const expected = 'Test Cancel Button Text'; + + expect(spectator.component.cancelBtn).toEqual(expected); + expect(cancelButton).toHaveText(expected); + }); + + it('should not render when cancelBtn not set', () => { + spectator.setInput({ cancelBtn: null }); + + expect(spectator.query('.cancel-btn')).toBeNull(); + }); + }); + describe('icon', () => { + it('should render', () => { + spectator.setInput({ iconName: 'warning' }); + const icon: HTMLElement = spectator.query('.icon-outline'); + + expect(icon).not.toBeNull(); + }); + }); +}); diff --git a/libs/designsystem/alert-experimental/src/alert.component.ts b/libs/designsystem/alert-experimental/src/alert.component.ts new file mode 100644 index 0000000000..b8439a7a95 --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.component.ts @@ -0,0 +1,80 @@ +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + ViewChild, +} from '@angular/core'; +import { WindowRef } from '@kirbydesign/designsystem/types'; +import { Observable, of } from 'rxjs'; +import { IconModule } from '@kirbydesign/designsystem/icon'; +import { ThemeColorDirective } from '@kirbydesign/designsystem/shared'; +import { EmptyStateModule } from '@kirbydesign/designsystem/empty-state'; +import { ButtonComponent } from '@kirbydesign/designsystem/button'; + +@Component({ + standalone: true, + imports: [ + IconModule, + EmptyStateModule, + ButtonComponent, + CommonModule, + EmptyStateModule, + ThemeColorDirective, + ], + selector: 'kirby-alert-experimental', + templateUrl: './alert.component.html', + styleUrls: ['./alert.component.scss'], + // eslint-disable-next-line @angular-eslint/no-host-metadata-property + host: { '[class.ion-page]': 'false' }, //Ensure ion-page class doesn't get applied by Ionic Modal Controller + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AlertExperimentalComponent implements AfterViewInit { + readonly BLUR_WRAPPER_DELAY_IN_MS = 50; + @ViewChild('alertWrapper', { static: true }) private alertWrapper: ElementRef; + private scrollY: number = Math.abs(this.windowRef.nativeWindow.scrollY); + + title$: Observable; + @Input() + set title(title: string | Observable) { + this.title$ = typeof title === 'string' ? of(title) : title; + } + + message$: Observable; + @Input() + set message(message: string & Observable) { + this.message$ = typeof message === 'string' ? of(message) : message; + } + + @Input() iconName: string; + @Input() iconThemeColor: string; + @Input() okBtn: string; + @Input() okBtnIsDestructive: boolean; + @Input() cancelBtn: string; + + constructor(private elementRef: ElementRef, private windowRef: WindowRef) {} + + ngAfterViewInit(): void { + setTimeout(() => { + this.alertWrapper.nativeElement.focus(); + this.alertWrapper.nativeElement.blur(); + }, this.BLUR_WRAPPER_DELAY_IN_MS); + } + + onFocusChange() { + // This fixes an undesired scroll behaviour occurring on keyboard-tabbing + this.windowRef.nativeWindow.scrollTo({ top: this.scrollY }); + } + + onCancel() { + const ionModalElement = this.elementRef.nativeElement.closest('ion-modal'); + ionModalElement && ionModalElement.dismiss(false); + } + + onOk() { + const ionModalElement = this.elementRef.nativeElement.closest('ion-modal'); + ionModalElement && ionModalElement.dismiss(true); + } +} diff --git a/libs/designsystem/alert-experimental/src/config/alert-config.ts b/libs/designsystem/alert-experimental/src/config/alert-config.ts new file mode 100644 index 0000000000..3883cf07eb --- /dev/null +++ b/libs/designsystem/alert-experimental/src/config/alert-config.ts @@ -0,0 +1,19 @@ +import { Observable } from 'rxjs'; + +export interface AlertConfig { + title: string | Observable; + message?: string | Observable; + cancelBtn?: string; + + icon?: { + name: string; + themeColor?: string; + }; + + okBtn?: + | string + | { + text: string; + isDestructive: boolean; + }; +} diff --git a/libs/designsystem/alert-experimental/src/index.ts b/libs/designsystem/alert-experimental/src/index.ts new file mode 100644 index 0000000000..4aaf8f92ed --- /dev/null +++ b/libs/designsystem/alert-experimental/src/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts new file mode 100644 index 0000000000..6b6f3b9b1e --- /dev/null +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -0,0 +1 @@ +export { AlertController } from './services/alert.controller'; diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts new file mode 100644 index 0000000000..895b521db2 --- /dev/null +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -0,0 +1,89 @@ +import { Component } from '@angular/core'; +import { ModalController as IonicModalController } from '@ionic/angular'; +import { createServiceFactory, SpectatorService } from '@ngneat/spectator'; + +import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; + +import { WindowRef } from '@kirbydesign/designsystem/types'; +import { TestHelper } from '@kirbydesign/designsystem/testing'; + +import { Overlay } from '../../modal.interfaces'; +import { AlertHelper } from './alert.helper'; + +@Component({ + template: ` +

Dummy Component

+ `, +}) +class EmbeddedDummyComponent {} + +describe('AlertHelper', () => { + let spectator: SpectatorService; + let alertHelper: AlertHelper; + const backdropOpacity = '0.4'; + + const createService = createServiceFactory({ + service: AlertHelper, + imports: [TestHelper.ionicModuleForTest], + providers: [ + { + provide: WindowRef, + useValue: { nativeWindow: window }, + }, + ], + }); + + beforeEach(() => { + spectator = createService(); + alertHelper = spectator.service; + }); + + describe('showAlert', () => { + let overlay: Overlay; + let ionModal: HTMLIonModalElement; + let backdrop: HTMLIonBackdropElement; + let ionModalController: IonicModalController; + + beforeEach(async () => { + ionModalController = spectator.inject(IonicModalController); + overlay = await alertHelper.showAlert({ title: 'Alert' }); + ionModal = await ionModalController.getTop(); + expect(ionModal).toBeTruthy(); + backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); + expect(backdrop).toBeTruthy(); + }); + + afterEach(async () => { + await overlay.dismiss(); + }); + + it('alert should have correct backdrop style', () => { + expect(ionModal).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); + }); + + it('modal wrapper should have correct max width', () => { + const modalWrapper = ionModal.shadowRoot.querySelector('.modal-wrapper'); + expect(modalWrapper).toHaveComputedStyle({ + 'max-width': DesignTokenHelper.compactModalMaxWidth(), + }); + }); + + it('alert should have correct backdrop style when opened on top of a modal', async () => { + await overlay.dismiss(); + const ionModalElement = await ionModalController.create({ + component: EmbeddedDummyComponent, + }); + await ionModalElement.present(); + const modalIonModal = await ionModalController.getTop(); + expect(modalIonModal).toBeTruthy(); + + overlay = await alertHelper.showAlert({ title: 'Alert on top of modal' }); + + ionModal = await ionModalController.getTop(); + expect(ionModal).toBeTruthy(); + backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); + expect(backdrop).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); + await ionModalElement.dismiss(); + }); + }); +}); diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts new file mode 100644 index 0000000000..d6f9c9bfcd --- /dev/null +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { Overlay } from '../../modal.interfaces'; + +import { AlertComponent } from '../alert/alert.component'; +import { AlertConfig } from '../alert/config/alert-config'; + +@Injectable() +export class AlertController { + constructor(private ionicModalController: ModalController) {} + + public async showAlert(config: AlertConfig): Promise { + const ionModal = await this.ionicModalController.create({ + component: AlertComponent, + componentProps: this.getComponentProps(config), + cssClass: ['kirby-overlay', 'kirby-alert'], + mode: 'ios', + backdropDismiss: false, + }); + + await ionModal.present(); + return { + dismiss: ionModal.dismiss.bind(ionModal), + onWillDismiss: ionModal.onWillDismiss(), + onDidDismiss: ionModal.onDidDismiss(), + }; + } + + private getComponentProps(config: AlertConfig) { + return { + ...config, + okBtn: this.getOkBtn(config), + cancelBtn: config.cancelBtn, + okBtnIsDestructive: this.getOkBtnIsDestructive(config), + iconName: config.icon && config.icon.name, + iconThemeColor: config.icon && config.icon.themeColor, + }; + } + + private getOkBtn(config: AlertConfig) { + let text: string; + + if (config.okBtn) { + if (typeof config.okBtn === 'string') { + text = config.okBtn; + } else { + text = config.okBtn.text; + } + } + return text; + } + + getOkBtnIsDestructive(config) { + return typeof config.okBtn === 'object' ? config.okBtn.isDestructive : undefined; + } +} diff --git a/libs/designsystem/src/lib/index.ts b/libs/designsystem/src/lib/index.ts index 1ffb11f176..455b2222c0 100644 --- a/libs/designsystem/src/lib/index.ts +++ b/libs/designsystem/src/lib/index.ts @@ -44,6 +44,7 @@ export * from '@kirbydesign/designsystem/toggle'; export * from '@kirbydesign/designsystem/toggle-button'; export * from '@kirbydesign/designsystem/types'; export * from '@kirbydesign/designsystem/chart'; +export * from '@kirbydesign/designsystem/alert-experimental'; export { KirbyModule } from './kirby.module'; export { KirbyExperimentalModule } from './kirby-experimental.module'; From ce552a6264b263bf70e691a732119ef808eaa86a Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 15:40:55 +0900 Subject: [PATCH 10/23] Add the alert path to tsconfig files --- apps/cookbook/tsconfig.json | 3 ++- apps/flows/tsconfig.json | 3 ++- libs/designsystem/tsconfig.json | 5 +++-- libs/designsystem/tsconfig.spec.json | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/cookbook/tsconfig.json b/apps/cookbook/tsconfig.json index 3435e6d4b5..76a7dd3737 100644 --- a/apps/cookbook/tsconfig.json +++ b/apps/cookbook/tsconfig.json @@ -56,7 +56,8 @@ "@kirbydesign/designsystem/data-table": ["libs/designsystem/data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], - "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] }, "target": "es2020" }, diff --git a/apps/flows/tsconfig.json b/apps/flows/tsconfig.json index 6eb0531d1d..2daf1a5d17 100644 --- a/apps/flows/tsconfig.json +++ b/apps/flows/tsconfig.json @@ -74,7 +74,8 @@ "@kirbydesign/designsystem/data-table": ["libs/designsystem/data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], - "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] }, "target": "es2020" }, diff --git a/libs/designsystem/tsconfig.json b/libs/designsystem/tsconfig.json index 7da4094683..e28ed96dd3 100644 --- a/libs/designsystem/tsconfig.json +++ b/libs/designsystem/tsconfig.json @@ -14,7 +14,7 @@ "@kirbydesign/designsystem/types": ["types/index.ts"], "@kirbydesign/designsystem/flag": ["flag/index.ts"], "@kirbydesign/designsystem/form-field": ["form-field/index.ts"], - "@kirbydesign/designsystem/grid": ["grid/index.ts"], + "@kirbydesign/designsystem/grid": ["grid/index.ts"], "@kirbydesign/designsystem/kirby-ionic-module": ["kirby-ionic-module/index.ts"], "@kirbydesign/designsystem/spinner": ["spinner/index.ts"], "@kirbydesign/designsystem/item": ["item/index.ts"], @@ -47,7 +47,8 @@ "@kirbydesign/designsystem/fab-sheet": ["fab-sheet/index.ts"], "@kirbydesign/designsystem/data-table": ["data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["reorder-list/index.ts"], - "@kirbydesign/designsystem/toast": ["toast/index.ts"] + "@kirbydesign/designsystem/toast": ["toast/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] }, "esModuleInterop": true, "target": "es2020" diff --git a/libs/designsystem/tsconfig.spec.json b/libs/designsystem/tsconfig.spec.json index 6d6bb26994..e9b6ae859f 100644 --- a/libs/designsystem/tsconfig.spec.json +++ b/libs/designsystem/tsconfig.spec.json @@ -51,7 +51,8 @@ "@kirbydesign/designsystem/fab-sheet": ["fab-sheet/index.ts"], "@kirbydesign/designsystem/data-table": ["data-table/index.ts"], "@kirbydesign/designsystem/reorder-list": ["reorder-list/index.ts"], - "@kirbydesign/designsystem/toast": ["toast/index.ts"] + "@kirbydesign/designsystem/toast": ["toast/index.ts"], + "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] } }, "files": ["test.ts"], From df946c5a32907b7b9536dca45cb365db571466ef Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 15:55:02 +0900 Subject: [PATCH 11/23] Refactor alertController from promises to observables --- .../src/services/alert.controller.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index d6f9c9bfcd..4b4985238f 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -1,28 +1,52 @@ import { Injectable } from '@angular/core'; import { ModalController } from '@ionic/angular'; -import { Overlay } from '../../modal.interfaces'; +import { from, Observable, Subject, switchMap, tap } from 'rxjs'; +import { OverlayEventDetail } from '@ionic/core/components'; +import { AlertExperimentalComponent } from '../alert.component'; +import { AlertConfig } from '../config/alert-config'; -import { AlertComponent } from '../alert/alert.component'; -import { AlertConfig } from '../alert/config/alert-config'; +type AlertDismissObservables = { + onWillDismiss: Observable; + onDidDismiss: Observable; +}; @Injectable() export class AlertController { constructor(private ionicModalController: ModalController) {} - public async showAlert(config: AlertConfig): Promise { - const ionModal = await this.ionicModalController.create({ - component: AlertComponent, - componentProps: this.getComponentProps(config), - cssClass: ['kirby-overlay', 'kirby-alert'], - mode: 'ios', - backdropDismiss: false, - }); + public showAlert(config: AlertConfig): AlertDismissObservables { + const $onWillDismiss = new Subject(); + const onWillDismiss$ = $onWillDismiss.asObservable(); + + const $onDidDismiss = new Subject(); + const onDidDismiss$ = $onDidDismiss.asObservable(); + + const modal$ = from( + this.ionicModalController.create({ + component: AlertExperimentalComponent, + componentProps: this.getComponentProps(config), + cssClass: ['kirby-overlay', 'kirby-alert'], + mode: 'ios', + backdropDismiss: false, + }) + ); + + modal$ + .pipe( + tap((modal) => from(modal.present())), + switchMap((modal) => modal.onWillDismiss()) + ) + .subscribe((res) => { + $onWillDismiss.next(res); + $onWillDismiss.complete(); + + $onDidDismiss.next(res); + $onDidDismiss.complete(); + }); - await ionModal.present(); return { - dismiss: ionModal.dismiss.bind(ionModal), - onWillDismiss: ionModal.onWillDismiss(), - onDidDismiss: ionModal.onDidDismiss(), + onWillDismiss: onWillDismiss$, + onDidDismiss: onDidDismiss$, }; } From c32bafe3bb99809fba37f4e9a173f8f54c2645b7 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 16:48:44 +0900 Subject: [PATCH 12/23] Fix broken tests --- .../src/alert.component.spec.ts | 12 +++--- .../src/services/alert.controller.spec.ts | 39 +++++-------------- .../button/src/button.component.scss | 3 +- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/alert.component.spec.ts b/libs/designsystem/alert-experimental/src/alert.component.spec.ts index ebfa413e9d..f2dbad6e3a 100644 --- a/libs/designsystem/alert-experimental/src/alert.component.spec.ts +++ b/libs/designsystem/alert-experimental/src/alert.component.spec.ts @@ -2,24 +2,24 @@ import { createHostFactory, SpectatorHost } from '@ngneat/spectator'; import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; -import { AlertComponent } from './alert.component'; +import { AlertExperimentalComponent } from './alert.component'; const getColor = DesignTokenHelper.getColor; -describe('AlertComponent', () => { - let spectator: SpectatorHost; +describe('AlertExperimentalComponent', () => { + let spectator: SpectatorHost; const createHost = createHostFactory({ - component: AlertComponent, + component: AlertExperimentalComponent, }); beforeEach(() => { spectator = createHost(` - - + `); }); diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts index 895b521db2..d0702a771c 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -7,8 +7,7 @@ import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; import { WindowRef } from '@kirbydesign/designsystem/types'; import { TestHelper } from '@kirbydesign/designsystem/testing'; -import { Overlay } from '../../modal.interfaces'; -import { AlertHelper } from './alert.helper'; +import { AlertController } from './alert.controller'; @Component({ template: ` @@ -17,13 +16,13 @@ import { AlertHelper } from './alert.helper'; }) class EmbeddedDummyComponent {} -describe('AlertHelper', () => { - let spectator: SpectatorService; - let alertHelper: AlertHelper; +describe('AlertExperimentalController', () => { + let spectator: SpectatorService; + let alertController: AlertController; const backdropOpacity = '0.4'; const createService = createServiceFactory({ - service: AlertHelper, + service: AlertController, imports: [TestHelper.ionicModuleForTest], providers: [ { @@ -35,18 +34,18 @@ describe('AlertHelper', () => { beforeEach(() => { spectator = createService(); - alertHelper = spectator.service; + alertController = spectator.service; }); describe('showAlert', () => { - let overlay: Overlay; let ionModal: HTMLIonModalElement; let backdrop: HTMLIonBackdropElement; let ionModalController: IonicModalController; beforeEach(async () => { ionModalController = spectator.inject(IonicModalController); - overlay = await alertHelper.showAlert({ title: 'Alert' }); + await alertController.showAlert({ title: 'Alert' }); + await TestHelper.waitForTimeout(50); ionModal = await ionModalController.getTop(); expect(ionModal).toBeTruthy(); backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); @@ -54,10 +53,10 @@ describe('AlertHelper', () => { }); afterEach(async () => { - await overlay.dismiss(); + await ionModal.dismiss(); }); - it('alert should have correct backdrop style', () => { + it('alert should have correct backdrop style', async () => { expect(ionModal).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); }); @@ -67,23 +66,5 @@ describe('AlertHelper', () => { 'max-width': DesignTokenHelper.compactModalMaxWidth(), }); }); - - it('alert should have correct backdrop style when opened on top of a modal', async () => { - await overlay.dismiss(); - const ionModalElement = await ionModalController.create({ - component: EmbeddedDummyComponent, - }); - await ionModalElement.present(); - const modalIonModal = await ionModalController.getTop(); - expect(modalIonModal).toBeTruthy(); - - overlay = await alertHelper.showAlert({ title: 'Alert on top of modal' }); - - ionModal = await ionModalController.getTop(); - expect(ionModal).toBeTruthy(); - backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); - expect(backdrop).toHaveComputedStyle({ '--backdrop-opacity': backdropOpacity }); - await ionModalElement.dismiss(); - }); }); }); diff --git a/libs/designsystem/button/src/button.component.scss b/libs/designsystem/button/src/button.component.scss index b715cd4e97..1c3bf9a496 100644 --- a/libs/designsystem/button/src/button.component.scss +++ b/libs/designsystem/button/src/button.component.scss @@ -244,7 +244,8 @@ $button-width: ( margin-inline-start: utils.size('s'); } -:host-context(kirby-alert).ok-btn { +:host-context(kirby-alert).ok-btn, +:host-context(kirby-alert-experimental).ok-btn { --kirby-button-background-color: #{utils.get-color('success')}; --kirby-button-color: #{utils.get-color('success-contrast')}; } From a6681e402303a65afbb9d696c9ac733fa42077a0 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 17:17:15 +0900 Subject: [PATCH 13/23] Create showcase component --- ...alert-experimental-showcase.component.html | 44 +++++++++++++++++++ .../alert-experimental-showcase.component.ts | 44 +++++++++++++++++++ .../src/app/showcase/showcase.routes.ts | 5 +++ 3 files changed, 93 insertions(+) create mode 100644 apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html create mode 100644 apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html new file mode 100644 index 0000000000..7d9f1f0ebb --- /dev/null +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html @@ -0,0 +1,44 @@ +
+ +

 

+

+ To show an alert, inject the Kirby + modalController + in your constructor, create an + AlertConfig + and pass it to + modalController.showAlert + . + +

+

+ title + and + message + properties in + AlertConfig + also accept an Observable for dynamic values: +
+ + (see "Example on github" for implementation details of the + remainingSeconds$ + timer) + + : + +

+

+ [Optional] + If you need to obtain data back from the alert, you can pass a callback function: + + this.modalController.showAlert(config, onAlertClose); + +private onAlertClose(result: boolean) {{ '{' }} + ... +{{ '}' }} +

+

Alert config properties:

+ +
diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts new file mode 100644 index 0000000000..417b75f50a --- /dev/null +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; +import { AlertExampleComponent } from '~/app/examples/alert-example/alert-example.component'; +import { ApiDescriptionProperty } from '~/app/shared/api-description/api-description-properties/api-description-properties.component'; + +@Component({ + selector: 'cookbook-alert-showcase', + templateUrl: './alert-experimental-showcase.component.html', + preserveWhitespaces: true, +}) +export class AlertExperimentalShowcaseComponent { + alertConfigWithIcon: string = AlertExampleComponent.alertConfigWithIcon; + alertConfigWithDynamicValues: string = AlertExampleComponent.alertConfigWithDynamicValues; + properties: ApiDescriptionProperty[] = [ + { + name: 'title', + description: 'The title of the alert', + type: ['string | Observable'], + }, + { + name: 'message', + description: + "(Optional) The message shown under the title (or icon if specified). Use '\\n' for newline.", + type: ['string | Observable'], + }, + { + name: 'icon', + description: '(Optional) Icon to be shown below the title', + type: ['{ name: string }', '{ name: string, themeColor: string }'], + }, + { + name: 'okBtn', + description: + '(Optional) Defines the text that will appear on the OK button and if it should be destructive', + defaultValue: 'OK', + type: ['string', '{ text: string, isDestructive: boolean }'], + }, + { + name: 'cancelBtn', + description: + '(Optional) The text that will appear on the cancel button. If not defined the cancel button will not be shown.', + type: ['string'], + }, + ]; +} diff --git a/apps/cookbook/src/app/showcase/showcase.routes.ts b/apps/cookbook/src/app/showcase/showcase.routes.ts index b79b83c710..3bee563519 100644 --- a/apps/cookbook/src/app/showcase/showcase.routes.ts +++ b/apps/cookbook/src/app/showcase/showcase.routes.ts @@ -10,6 +10,7 @@ import { ModalRoutingExperimentalExamplePage2Component } from '../examples/modal import { AccordionShowcaseComponent } from './accordion-showcase/accordion-showcase.component'; import { ActionSheetShowcaseComponent } from './action-sheet-showcase/action-sheet-showcase.component'; import { AlertShowcaseComponent } from './alert-showcase/alert-showcase.component'; +import { AlertExperimentalShowcaseComponent } from './alert-experimental-showcase/alert-experimental-showcase.component'; import { AvatarShowcaseComponent } from './avatar-showcase/avatar-showcase.component'; import { BadgeShowcaseComponent } from './badge-showcase/badge-showcase.component'; import { ButtonShowcaseComponent } from './button-showcase/button-showcase.component'; @@ -227,6 +228,10 @@ export const routes: Routes = [ path: 'alert', component: AlertShowcaseComponent, }, + { + path: 'alert-experimental', + component: AlertExperimentalShowcaseComponent, + }, { path: 'badge', component: BadgeShowcaseComponent, From 38a8781c50c5bc7adb10cd5836e9934844d06063 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 17:18:06 +0900 Subject: [PATCH 14/23] Rename the controller to include 'Experimental' --- .../src/services/alert.controller.spec.ts | 8 ++++---- .../alert-experimental/src/services/alert.controller.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts index d0702a771c..eef396f350 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -7,7 +7,7 @@ import { DesignTokenHelper } from '@kirbydesign/designsystem/helpers'; import { WindowRef } from '@kirbydesign/designsystem/types'; import { TestHelper } from '@kirbydesign/designsystem/testing'; -import { AlertController } from './alert.controller'; +import { AlertExperimentalController } from './alert.controller'; @Component({ template: ` @@ -17,12 +17,12 @@ import { AlertController } from './alert.controller'; class EmbeddedDummyComponent {} describe('AlertExperimentalController', () => { - let spectator: SpectatorService; - let alertController: AlertController; + let spectator: SpectatorService; + let alertController: AlertExperimentalController; const backdropOpacity = '0.4'; const createService = createServiceFactory({ - service: AlertController, + service: AlertExperimentalController, imports: [TestHelper.ionicModuleForTest], providers: [ { diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index 4b4985238f..85ca47a8f1 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -11,7 +11,7 @@ type AlertDismissObservables = { }; @Injectable() -export class AlertController { +export class AlertExperimentalController { constructor(private ionicModalController: ModalController) {} public showAlert(config: AlertConfig): AlertDismissObservables { From 572b4b5d26fd5c3eaabe5b470f0f3044ab8f13fb Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 17:28:08 +0900 Subject: [PATCH 15/23] Fix bug that prevents the bookcook from running --- apps/cookbook/tsconfig.json | 4 +++- apps/flows/tsconfig.json | 4 +++- .../alert-experimental/{ng-packge.json => ng-package.json} | 0 libs/designsystem/alert-experimental/src/public_api.ts | 3 ++- 4 files changed, 8 insertions(+), 3 deletions(-) rename libs/designsystem/alert-experimental/{ng-packge.json => ng-package.json} (100%) diff --git a/apps/cookbook/tsconfig.json b/apps/cookbook/tsconfig.json index 76a7dd3737..58789a711c 100644 --- a/apps/cookbook/tsconfig.json +++ b/apps/cookbook/tsconfig.json @@ -57,7 +57,9 @@ "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], - "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] + "@kirbydesign/designsystem/alert-experimental": [ + "libs/designsystem/alert-experimental/index.ts" + ] }, "target": "es2020" }, diff --git a/apps/flows/tsconfig.json b/apps/flows/tsconfig.json index 2daf1a5d17..93dd760165 100644 --- a/apps/flows/tsconfig.json +++ b/apps/flows/tsconfig.json @@ -75,7 +75,9 @@ "@kirbydesign/designsystem/reorder-list": ["libs/designsystem/reorder-list/index.ts"], "@kirbydesign/designsystem/toast": ["libs/designsystem/toast/index.ts"], "@kirbydesign/designsystem/grid": ["libs/designsystem/grid/index.ts"], - "@kirbydesign/designsystem/alert-experimental": ["alert-experimental/index.ts"] + "@kirbydesign/designsystem/alert-experimental": [ + "libs/designsystem/alert-experimental/index.ts" + ] }, "target": "es2020" }, diff --git a/libs/designsystem/alert-experimental/ng-packge.json b/libs/designsystem/alert-experimental/ng-package.json similarity index 100% rename from libs/designsystem/alert-experimental/ng-packge.json rename to libs/designsystem/alert-experimental/ng-package.json diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts index 6b6f3b9b1e..8d1def3ca0 100644 --- a/libs/designsystem/alert-experimental/src/public_api.ts +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -1 +1,2 @@ -export { AlertController } from './services/alert.controller'; +export { AlertExperimentalController } from './services/alert.controller'; +export { AlertExperimentalComponent } from './alert.component'; From 670ea02f3e1123eea699d3e16ce4807b06c8a007 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Mon, 13 Feb 2023 18:57:02 +0900 Subject: [PATCH 16/23] WIP: update cookbook example --- .../alert-experimental-example.component.html | 8 + .../alert-experimental-example.component.ts | 193 ++++++++++++++++++ .../src/app/examples/examples.common.ts | 2 + ...alert-experimental-showcase.component.html | 2 +- .../src/app/showcase/showcase.common.ts | 2 + .../src/config/alert-config.ts | 2 +- .../alert-experimental/src/public_api.ts | 1 + .../src/services/alert.controller.ts | 8 +- 8 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html create mode 100644 apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html new file mode 100644 index 0000000000..41d8ea934e --- /dev/null +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts new file mode 100644 index 0000000000..79b0081ea0 --- /dev/null +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts @@ -0,0 +1,193 @@ +import { Component } from '@angular/core'; +import { of, Subject, timer } from 'rxjs'; +import { map, takeUntil, takeWhile } from 'rxjs/operators'; + +import { + AlertExperimentalConfig, + AlertExperimentalController, +} from '@kirbydesign/designsystem/alert-experimental'; +import { ToastConfig, ToastController } from '@kirbydesign/designsystem'; + +const alertConfigWithIcon = { + title: 'Alert With Icon', + message: 'This message can have more than 1 line.', + okBtn: 'I agree', + cancelBtn: 'Take me back', + icon: { name: 'warning', themeColor: 'warning' }, +}; + +export const observableCodeSnippet = `showAlert() { + const alert = this.alertController.showAlert(config); + alert?.onWillDismiss.subscribe((response) => { + const { role, data } = response; + ... + }); + alert?.onDidDismiss.subscribe((response) => { + const { role, data } = response; + ... + }); +}`; + +@Component({ + selector: 'cookbook-alert-experimental-example', + templateUrl: './alert-experimental-example.component.html', + styles: [':host { display: block; }'], +}) +export class AlertExperimentalExampleComponent { + static readonly alertConfigWithIcon = `const config: AlertConfig = ${AlertExperimentalExampleComponent.stringify( + alertConfigWithIcon + )} + +this.alertController.showAlert(config);`; + + private static stringify(value: any): string { + return JSON.stringify(value, null, '\t') + .replace(/"(\w+)\":/g, '$1:') + .replace(/"/g, "'"); + } + + private alertClose$: Subject = new Subject(); + + static readonly alertConfigWithDynamicValues = `const title$ = of('Need more time?'); + const message$ = remainingSeconds$.pipe( + map((remainingSeconds) => \`Time remaining: \${remainingSeconds}\`) + ); + const config: AlertConfig = { + title: title$, + icon: { + name: 'clock', + themeColor: 'warning', + }, + message: message$, + okBtn: 'Logout', + cancelBtn: 'Take me back', + }; + + this.alertController.showAlert(config);`; + constructor( + private alertController: AlertExperimentalController, + private toastController: ToastController + ) {} + + showAlert() { + const config: AlertExperimentalConfig = { + title: 'Default Alert', + message: 'The default alert is just a title, a message, an OK and (optional) cancel button', + okBtn: 'I agree', + cancelBtn: 'Take me back', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showAlertWithIcon() { + const alert = this.alertController.showAlert(alertConfigWithIcon); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showAlertWithoutCancel() { + const config: AlertExperimentalConfig = { + title: 'Alert Without Cancel', + message: 'This is an alert that can only be acknowledged (no cancel option)', + okBtn: 'I understand', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showDestructiveAlert() { + const config: AlertExperimentalConfig = { + title: 'Desctructive Alert', + message: + 'This is to indicate that something destructive will happen when clicking the OK button', + cancelBtn: 'Get me out of here', + okBtn: { text: 'Confirm', isDestructive: true }, + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertDestructiveClosed(result.data); + }); + } + + showAlertWithNewline() { + const config: AlertExperimentalConfig = { + title: 'Alert with newline', + message: 'This is message one.\n\nThis is message two.', + okBtn: 'I agree', + cancelBtn: 'Take me back', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + showAlertWithDynamicValues() { + const countdownTimeInSeconds = 60; + const countdownTimeInMs = countdownTimeInSeconds * 1000; + const intervalInMs = 1000; + const toRemainingTimeInMs = (count: number) => countdownTimeInMs - count * intervalInMs; + const toSeconds = (timeInMs: number) => Math.ceil(timeInMs / 1000); + + const remainingTime$ = timer(0, intervalInMs).pipe( + map(toRemainingTimeInMs), + takeUntil(this.alertClose$), + takeWhile((countdownTimeInMs) => countdownTimeInMs >= 0) + ); + + const title$ = of('Need more time?'); + const message$ = remainingTime$.pipe( + map((remainingTimeInMs) => `Time remaining: ${toSeconds(remainingTimeInMs)}`) + ); + const config: AlertExperimentalConfig = { + title: title$, + icon: { + name: 'clock', + themeColor: 'warning', + }, + message: message$, + okBtn: 'Logout', + cancelBtn: 'Take me back', + }; + + const alert = this.alertController.showAlert(config); + + alert.onDidDismiss.subscribe((result) => { + this.onAlertClosed(result.data); + }); + } + + private onAlertClosed(result?: boolean) { + const config: ToastConfig = { + message: `Alert selection: ${result}`, + messageType: result ? 'success' : 'warning', + durationInMs: 1500, + }; + this.toastController.showToast(config); + this.alertClose$.next(); + } + + private onAlertDestructiveClosed(result?: boolean) { + const config: ToastConfig = { + message: result ? 'Message deleted' : 'Nothing happened', + messageType: result ? 'warning' : 'success', + durationInMs: 1500, + }; + this.toastController.showToast(config); + } +} diff --git a/apps/cookbook/src/app/examples/examples.common.ts b/apps/cookbook/src/app/examples/examples.common.ts index ad96d7f92d..41fdf5e7fe 100644 --- a/apps/cookbook/src/app/examples/examples.common.ts +++ b/apps/cookbook/src/app/examples/examples.common.ts @@ -1,6 +1,7 @@ import { AccordionExampleComponent } from './accordion-example/accordion-example.component'; import { ActionSheetExampleComponent } from './action-sheet-example/action-sheet-example.component'; import { AlertExampleComponent } from './alert-example/alert-example.component'; +import { AlertExperimentalExampleComponent } from './alert-experimental-example/alert-experimental-example.component'; import { AvatarExampleComponent } from './avatar-example/avatar-example.component'; import { ButtonExampleComponent } from './button-example/button-example.component'; import { CalendarCardExampleComponent } from './calendar-example/calendar-card-example.component'; @@ -69,6 +70,7 @@ export const COMPONENT_DECLARATIONS: any[] = [ ActionSheetExampleComponent, CheckboxExampleComponent, AlertExampleComponent, + AlertExperimentalExampleComponent, ToastExampleComponent, ToggleExampleComponent, EmptyStateExampleComponent, diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html index 7d9f1f0ebb..d1034dcfc2 100644 --- a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html @@ -1,5 +1,5 @@
- +

 

To show an alert, inject the Kirby diff --git a/apps/cookbook/src/app/showcase/showcase.common.ts b/apps/cookbook/src/app/showcase/showcase.common.ts index 2ac1b10d1d..5d4431e830 100644 --- a/apps/cookbook/src/app/showcase/showcase.common.ts +++ b/apps/cookbook/src/app/showcase/showcase.common.ts @@ -8,6 +8,7 @@ import { ChartExampleConfigBaseBarComponent } from '../examples/charts-example/e import { AccordionShowcaseComponent } from './accordion-showcase/accordion-showcase.component'; import { ActionSheetShowcaseComponent } from './action-sheet-showcase/action-sheet-showcase.component'; import { AlertShowcaseComponent } from './alert-showcase/alert-showcase.component'; +import { AlertExperimentalShowcaseComponent } from './alert-experimental-showcase/alert-experimental-showcase.component'; import { AvatarShowcaseComponent } from './avatar-showcase/avatar-showcase.component'; import { BadgeShowcaseComponent } from './badge-showcase/badge-showcase.component'; import { ButtonShowcaseComponent } from './button-showcase/button-showcase.component'; @@ -83,6 +84,7 @@ export const COMPONENT_EXPORTS: any[] = [ ActionSheetShowcaseComponent, CheckboxShowcaseComponent, AlertShowcaseComponent, + AlertExperimentalShowcaseComponent, ToastShowcaseComponent, ToggleShowcaseComponent, ToggleButtonShowcaseComponent, diff --git a/libs/designsystem/alert-experimental/src/config/alert-config.ts b/libs/designsystem/alert-experimental/src/config/alert-config.ts index 3883cf07eb..cb3b3b84e3 100644 --- a/libs/designsystem/alert-experimental/src/config/alert-config.ts +++ b/libs/designsystem/alert-experimental/src/config/alert-config.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs'; -export interface AlertConfig { +export interface AlertExperimentalConfig { title: string | Observable; message?: string | Observable; cancelBtn?: string; diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts index 8d1def3ca0..2cd2f8f589 100644 --- a/libs/designsystem/alert-experimental/src/public_api.ts +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -1,2 +1,3 @@ export { AlertExperimentalController } from './services/alert.controller'; export { AlertExperimentalComponent } from './alert.component'; +export { AlertExperimentalConfig } from './config/alert-config'; diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index 85ca47a8f1..a9ee002efc 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -3,7 +3,7 @@ import { ModalController } from '@ionic/angular'; import { from, Observable, Subject, switchMap, tap } from 'rxjs'; import { OverlayEventDetail } from '@ionic/core/components'; import { AlertExperimentalComponent } from '../alert.component'; -import { AlertConfig } from '../config/alert-config'; +import { AlertExperimentalConfig } from '../config/alert-config'; type AlertDismissObservables = { onWillDismiss: Observable; @@ -14,7 +14,7 @@ type AlertDismissObservables = { export class AlertExperimentalController { constructor(private ionicModalController: ModalController) {} - public showAlert(config: AlertConfig): AlertDismissObservables { + public showAlert(config: AlertExperimentalConfig): AlertDismissObservables { const $onWillDismiss = new Subject(); const onWillDismiss$ = $onWillDismiss.asObservable(); @@ -50,7 +50,7 @@ export class AlertExperimentalController { }; } - private getComponentProps(config: AlertConfig) { + private getComponentProps(config: AlertExperimentalConfig) { return { ...config, okBtn: this.getOkBtn(config), @@ -61,7 +61,7 @@ export class AlertExperimentalController { }; } - private getOkBtn(config: AlertConfig) { + private getOkBtn(config: AlertExperimentalConfig) { let text: string; if (config.okBtn) { From 90bf3e8dd28efb0915008d9ad96c3c13edc7ee2c Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Fri, 17 Feb 2023 20:07:29 +0900 Subject: [PATCH 17/23] Create a module for the alert --- .../alert-experimental/src/alert.module.ts | 11 +++++++++++ .../designsystem/alert-experimental/src/public_api.ts | 1 + 2 files changed, 12 insertions(+) create mode 100644 libs/designsystem/alert-experimental/src/alert.module.ts diff --git a/libs/designsystem/alert-experimental/src/alert.module.ts b/libs/designsystem/alert-experimental/src/alert.module.ts new file mode 100644 index 0000000000..a590b7084d --- /dev/null +++ b/libs/designsystem/alert-experimental/src/alert.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { CommonModule } from '@angular/common'; +import { AlertExperimentalComponent } from './alert.component'; +import { AlertExperimentalController } from './services/alert.controller'; + +@NgModule({ + imports: [CommonModule, IonicModule, AlertExperimentalComponent], + providers: [AlertExperimentalController], +}) +export class AlertExperimentalModule {} diff --git a/libs/designsystem/alert-experimental/src/public_api.ts b/libs/designsystem/alert-experimental/src/public_api.ts index 2cd2f8f589..91ab533ac7 100644 --- a/libs/designsystem/alert-experimental/src/public_api.ts +++ b/libs/designsystem/alert-experimental/src/public_api.ts @@ -1,3 +1,4 @@ export { AlertExperimentalController } from './services/alert.controller'; export { AlertExperimentalComponent } from './alert.component'; export { AlertExperimentalConfig } from './config/alert-config'; +export * from './alert.module'; From 5823f19f0bf265e7edf85a860b5a6afb473c450a Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Fri, 17 Feb 2023 20:12:22 +0900 Subject: [PATCH 18/23] Remove IonicModule from the alert module --- apps/cookbook/src/app/examples/examples.module.ts | 2 ++ libs/designsystem/alert-experimental/src/alert.module.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/cookbook/src/app/examples/examples.module.ts b/apps/cookbook/src/app/examples/examples.module.ts index ef90fe77af..b37a88868a 100644 --- a/apps/cookbook/src/app/examples/examples.module.ts +++ b/apps/cookbook/src/app/examples/examples.module.ts @@ -9,6 +9,7 @@ import { } from '@kirbydesign/designsystem'; import { SlideModule } from '@kirbydesign/designsystem/slide'; +import { AlertExperimentalModule } from '@kirbydesign/designsystem/alert-experimental'; import { CodeViewerModule } from '../shared/code-viewer/code-viewer.module'; import { AccordionExampleModule } from './accordion-example/accordion-example.module'; import { AvatarExampleModule } from './avatar-example/avatar-example.module'; @@ -71,6 +72,7 @@ const IMPORTS = [ ExperimentalExamplesModule, DataTableExampleModule, SlideModule, + AlertExperimentalModule, ]; @NgModule({ diff --git a/libs/designsystem/alert-experimental/src/alert.module.ts b/libs/designsystem/alert-experimental/src/alert.module.ts index a590b7084d..ed436548c8 100644 --- a/libs/designsystem/alert-experimental/src/alert.module.ts +++ b/libs/designsystem/alert-experimental/src/alert.module.ts @@ -1,11 +1,10 @@ import { NgModule } from '@angular/core'; -import { IonicModule } from '@ionic/angular'; import { CommonModule } from '@angular/common'; import { AlertExperimentalComponent } from './alert.component'; import { AlertExperimentalController } from './services/alert.controller'; @NgModule({ - imports: [CommonModule, IonicModule, AlertExperimentalComponent], + imports: [CommonModule, AlertExperimentalComponent], providers: [AlertExperimentalController], }) export class AlertExperimentalModule {} From 00e1a9e133e5f5bc49353f3b8c7ecbc2944d05c8 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Fri, 17 Feb 2023 21:30:16 +0900 Subject: [PATCH 19/23] Update the cookbook example --- .../alert-experimental-example.component.ts | 4 ++-- ...alert-experimental-showcase.component.html | 22 +++++++++---------- .../alert-experimental-showcase.component.ts | 15 ++++++++----- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts index 79b0081ea0..8019409852 100644 --- a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts @@ -34,7 +34,7 @@ export const observableCodeSnippet = `showAlert() { styles: [':host { display: block; }'], }) export class AlertExperimentalExampleComponent { - static readonly alertConfigWithIcon = `const config: AlertConfig = ${AlertExperimentalExampleComponent.stringify( + static readonly alertConfigWithIcon = `const config: AlertExperimentalConfig = ${AlertExperimentalExampleComponent.stringify( alertConfigWithIcon )} @@ -52,7 +52,7 @@ this.alertController.showAlert(config);`; const message$ = remainingSeconds$.pipe( map((remainingSeconds) => \`Time remaining: \${remainingSeconds}\`) ); - const config: AlertConfig = { + const config: AlertExperimentalConfig = { title: title$, icon: { name: 'clock', diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html index d1034dcfc2..7a8ce2d832 100644 --- a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html @@ -1,13 +1,13 @@

- +

 

To show an alert, inject the Kirby - modalController + AlertExperimentalController in your constructor, create an - AlertConfig + AlertExperimentalConfig and pass it to - modalController.showAlert + alertController.showAlert .

@@ -16,7 +16,7 @@ and message properties in - AlertConfig + AlertExperimentalConfig also accept an Observable for dynamic values:
@@ -29,13 +29,13 @@

[Optional] - If you need to obtain data back from the alert, you can pass a callback function: + If you need to obtain data back from the alert, you can subscribe to the + onWillDismiss + and + onDidDismiss + events like this: - this.modalController.showAlert(config, onAlertClose); - -private onAlertClose(result: boolean) {{ '{' }} - ... -{{ '}' }} +

Alert config properties:

Date: Tue, 21 Feb 2023 15:56:49 +0900 Subject: [PATCH 20/23] Update the cookbook according to review comments --- .../alert-experimental-example.component.ts | 26 +++++++++---------- ...alert-experimental-showcase.component.html | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts index 8019409852..650dce4c18 100644 --- a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts @@ -8,11 +8,11 @@ import { } from '@kirbydesign/designsystem/alert-experimental'; import { ToastConfig, ToastController } from '@kirbydesign/designsystem'; -const alertConfigWithIcon = { +const alertConfigWithIcon: AlertExperimentalConfig = { title: 'Alert With Icon', - message: 'This message can have more than 1 line.', - okBtn: 'I agree', - cancelBtn: 'Take me back', + message: 'This is an alert with an icon.', + okButton: 'I agree', + cancelButton: 'Take me back', icon: { name: 'warning', themeColor: 'warning' }, }; @@ -73,8 +73,8 @@ this.alertController.showAlert(config);`; const config: AlertExperimentalConfig = { title: 'Default Alert', message: 'The default alert is just a title, a message, an OK and (optional) cancel button', - okBtn: 'I agree', - cancelBtn: 'Take me back', + okButton: 'I agree', + cancelButton: 'Take me back', }; const alert = this.alertController.showAlert(config); @@ -96,7 +96,7 @@ this.alertController.showAlert(config);`; const config: AlertExperimentalConfig = { title: 'Alert Without Cancel', message: 'This is an alert that can only be acknowledged (no cancel option)', - okBtn: 'I understand', + okButton: 'I understand', }; const alert = this.alertController.showAlert(config); @@ -111,8 +111,8 @@ this.alertController.showAlert(config);`; title: 'Desctructive Alert', message: 'This is to indicate that something destructive will happen when clicking the OK button', - cancelBtn: 'Get me out of here', - okBtn: { text: 'Confirm', isDestructive: true }, + cancelButton: 'Get me out of here', + okButton: { text: 'Confirm', isDestructive: true }, }; const alert = this.alertController.showAlert(config); @@ -126,8 +126,8 @@ this.alertController.showAlert(config);`; const config: AlertExperimentalConfig = { title: 'Alert with newline', message: 'This is message one.\n\nThis is message two.', - okBtn: 'I agree', - cancelBtn: 'Take me back', + okButton: 'I agree', + cancelButton: 'Take me back', }; const alert = this.alertController.showAlert(config); @@ -161,8 +161,8 @@ this.alertController.showAlert(config);`; themeColor: 'warning', }, message: message$, - okBtn: 'Logout', - cancelBtn: 'Take me back', + okButton: 'Logout', + cancelButton: 'Take me back', }; const alert = this.alertController.showAlert(config); diff --git a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html index 7a8ce2d832..c88c2e0c6f 100644 --- a/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html +++ b/apps/cookbook/src/app/showcase/alert-experimental-showcase/alert-experimental-showcase.component.html @@ -1,5 +1,5 @@
- +

 

To show an alert, inject the Kirby From 4455b05a02510cda9336c7114e1e694dd87b4f33 Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Tue, 21 Feb 2023 15:57:07 +0900 Subject: [PATCH 21/23] Remove shorthand names --- .../src/alert.component.html | 14 ++++++------- .../alert-experimental/src/alert.component.ts | 10 +++++----- .../src/config/alert-config.ts | 4 ++-- .../src/services/alert.controller.ts | 20 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/alert.component.html b/libs/designsystem/alert-experimental/src/alert.component.html index 536f29b035..5ede8578ff 100644 --- a/libs/designsystem/alert-experimental/src/alert.component.html +++ b/libs/designsystem/alert-experimental/src/alert.component.html @@ -8,22 +8,22 @@

diff --git a/libs/designsystem/alert-experimental/src/alert.component.ts b/libs/designsystem/alert-experimental/src/alert.component.ts index b8439a7a95..2f43b03d87 100644 --- a/libs/designsystem/alert-experimental/src/alert.component.ts +++ b/libs/designsystem/alert-experimental/src/alert.component.ts @@ -50,9 +50,9 @@ export class AlertExperimentalComponent implements AfterViewInit { @Input() iconName: string; @Input() iconThemeColor: string; - @Input() okBtn: string; - @Input() okBtnIsDestructive: boolean; - @Input() cancelBtn: string; + @Input() okButton: string; + @Input() okButtonIsDestructive: boolean; + @Input() cancelButton: string; constructor(private elementRef: ElementRef, private windowRef: WindowRef) {} @@ -68,12 +68,12 @@ export class AlertExperimentalComponent implements AfterViewInit { this.windowRef.nativeWindow.scrollTo({ top: this.scrollY }); } - onCancel() { + cancelAlert() { const ionModalElement = this.elementRef.nativeElement.closest('ion-modal'); ionModalElement && ionModalElement.dismiss(false); } - onOk() { + approveAlert() { const ionModalElement = this.elementRef.nativeElement.closest('ion-modal'); ionModalElement && ionModalElement.dismiss(true); } diff --git a/libs/designsystem/alert-experimental/src/config/alert-config.ts b/libs/designsystem/alert-experimental/src/config/alert-config.ts index cb3b3b84e3..4490e7f342 100644 --- a/libs/designsystem/alert-experimental/src/config/alert-config.ts +++ b/libs/designsystem/alert-experimental/src/config/alert-config.ts @@ -3,14 +3,14 @@ import { Observable } from 'rxjs'; export interface AlertExperimentalConfig { title: string | Observable; message?: string | Observable; - cancelBtn?: string; + cancelButton?: string; icon?: { name: string; themeColor?: string; }; - okBtn?: + okButton?: | string | { text: string; diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.ts index a9ee002efc..3d1d4b1ca9 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.ts @@ -53,28 +53,28 @@ export class AlertExperimentalController { private getComponentProps(config: AlertExperimentalConfig) { return { ...config, - okBtn: this.getOkBtn(config), - cancelBtn: config.cancelBtn, - okBtnIsDestructive: this.getOkBtnIsDestructive(config), + okButton: this.getOkButton(config), + cancelButton: config.cancelButton, + okButtonIsDestructive: this.getOkButtonIsDestructive(config), iconName: config.icon && config.icon.name, iconThemeColor: config.icon && config.icon.themeColor, }; } - private getOkBtn(config: AlertExperimentalConfig) { + private getOkButton(config: AlertExperimentalConfig) { let text: string; - if (config.okBtn) { - if (typeof config.okBtn === 'string') { - text = config.okBtn; + if (config.okButton) { + if (typeof config.okButton === 'string') { + text = config.okButton; } else { - text = config.okBtn.text; + text = config.okButton.text; } } return text; } - getOkBtnIsDestructive(config) { - return typeof config.okBtn === 'object' ? config.okBtn.isDestructive : undefined; + getOkButtonIsDestructive(config) { + return typeof config.okButton === 'object' ? config.okButton.isDestructive : undefined; } } From 28766457598403cb980db022f3074b462f11ca2e Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Tue, 21 Feb 2023 16:07:18 +0900 Subject: [PATCH 22/23] Fix broken tests and arrange beforeEach according to AAA --- .../alert-experimental-example.component.ts | 4 ++-- .../alert-experimental/src/alert.component.spec.ts | 11 ++++++----- .../src/services/alert.controller.spec.ts | 4 +++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts index 650dce4c18..f76dd42e66 100644 --- a/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts +++ b/apps/cookbook/src/app/examples/alert-experimental-example/alert-experimental-example.component.ts @@ -18,11 +18,11 @@ const alertConfigWithIcon: AlertExperimentalConfig = { export const observableCodeSnippet = `showAlert() { const alert = this.alertController.showAlert(config); - alert?.onWillDismiss.subscribe((response) => { + alert.onWillDismiss.subscribe((response) => { const { role, data } = response; ... }); - alert?.onDidDismiss.subscribe((response) => { + alert.onDidDismiss.subscribe((response) => { const { role, data } = response; ... }); diff --git a/libs/designsystem/alert-experimental/src/alert.component.spec.ts b/libs/designsystem/alert-experimental/src/alert.component.spec.ts index f2dbad6e3a..efa61dd828 100644 --- a/libs/designsystem/alert-experimental/src/alert.component.spec.ts +++ b/libs/designsystem/alert-experimental/src/alert.component.spec.ts @@ -36,12 +36,12 @@ describe('AlertExperimentalComponent', () => { it('should render', () => { const expected = 'Test OK Button Text'; - expect(spectator.component.okBtn).toEqual(expected); + expect(spectator.component.okButton).toEqual(expected); expect(okButton).toHaveText(expected); }); it('should support isDestructive', () => { - spectator.setInput({ okBtnIsDestructive: true }); + spectator.setInput({ okButtonIsDestructive: true }); expect(okButton).toBeDefined(); expect(okButton).toHaveClass('destructive'); @@ -53,7 +53,7 @@ describe('AlertExperimentalComponent', () => { }); it('should have large ok button when no cancel button', () => { - spectator.setInput({ cancelBtn: null }); + spectator.setInput({ cancelButton: null }); expect(okButton).toHaveClass('lg'); }); @@ -79,16 +79,17 @@ describe('AlertExperimentalComponent', () => { it('should render', () => { const expected = 'Test Cancel Button Text'; - expect(spectator.component.cancelBtn).toEqual(expected); + expect(spectator.component.cancelButton).toEqual(expected); expect(cancelButton).toHaveText(expected); }); it('should not render when cancelBtn not set', () => { - spectator.setInput({ cancelBtn: null }); + spectator.setInput({ cancelButton: null }); expect(spectator.query('.cancel-btn')).toBeNull(); }); }); + describe('icon', () => { it('should render', () => { spectator.setInput({ iconName: 'warning' }); diff --git a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts index eef396f350..6399b118fc 100644 --- a/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts +++ b/libs/designsystem/alert-experimental/src/services/alert.controller.spec.ts @@ -44,11 +44,13 @@ describe('AlertExperimentalController', () => { beforeEach(async () => { ionModalController = spectator.inject(IonicModalController); + await alertController.showAlert({ title: 'Alert' }); await TestHelper.waitForTimeout(50); ionModal = await ionModalController.getTop(); - expect(ionModal).toBeTruthy(); backdrop = ionModal.shadowRoot.querySelector('ion-backdrop'); + + expect(ionModal).toBeTruthy(); expect(backdrop).toBeTruthy(); }); From 513df3a7e1eb22d660ccad67e0775e038b813efe Mon Sep 17 00:00:00 2001 From: Mark Drastrup Date: Tue, 21 Feb 2023 16:11:30 +0900 Subject: [PATCH 23/23] Fix broken tests --- .../alert-experimental/src/alert.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/designsystem/alert-experimental/src/alert.component.spec.ts b/libs/designsystem/alert-experimental/src/alert.component.spec.ts index efa61dd828..78aca7b70f 100644 --- a/libs/designsystem/alert-experimental/src/alert.component.spec.ts +++ b/libs/designsystem/alert-experimental/src/alert.component.spec.ts @@ -16,8 +16,8 @@ describe('AlertExperimentalComponent', () => { beforeEach(() => { spectator = createHost(` `);