Skip to content

Commit

Permalink
Merge pull request #2514 from kirbydesign/enhancement/376-modal-alert…
Browse files Browse the repository at this point in the history
…-before-close

Add option to show an alert before dismissing modal
  • Loading branch information
mark-drastrup authored Oct 24, 2022
2 parents 61c50e5 + 4d7a92f commit 96ac460
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@
[disabled]="disabled"
text="Collapse title"
></kirby-checkbox>

<kirby-checkbox
[checked]="alertBeforeClose"
(checkedChange)="toggleAlertBeforeClose($event)"
[disabled]="disabled"
text="Alert before closing"
></kirby-checkbox>
<small
>(Collapse title is unavailable for routed modals. See
<a href="https://github.com/kirbydesign/designsystem/issues/2192" target="_blank">issue #2192</a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export class ModalExampleConfigurationComponent {
@Input() collapseTitle: boolean;
@Output() collapseTitleChange = new EventEmitter<boolean>();

@Input() alertBeforeClose: boolean;
@Output() alertBeforeCloseChange = new EventEmitter<boolean>();

@Input() showFooter: boolean;
@Output() showFooterChange = new EventEmitter<boolean>();

Expand Down Expand Up @@ -100,6 +103,12 @@ export class ModalExampleConfigurationComponent {
this.collapseTitleChange.emit(this.collapseTitle);
}

toggleAlertBeforeClose(value: boolean) {
if (this.preventChangeEvent) return;
this.alertBeforeClose = value;
this.alertBeforeCloseChange.emit(this.alertBeforeClose);
}

toggleShowDummyContent(show: boolean) {
this.showDummyContent = show;
this.showDummyContentChange.emit(this.showDummyContent);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';

import { ModalConfig, ModalController } from '@kirbydesign/designsystem';
import { AlertConfig, ModalConfig, ModalController } from '@kirbydesign/designsystem';
import { WindowRef } from '@kirbydesign/designsystem/types/window-ref';

import { ModalCompactExampleComponent } from './compact-example/modal-compact-example.component';
Expand All @@ -17,6 +17,7 @@ const config = {
[(showFooter)]="showFooter"
[(displayFooterAsInline)]="displayFooterAsInline"
[(collapseTitle)]="collapseTitle"
[(alertBeforeClose)]="alertBeforeClose"
[(showDummyContent)]="showDummyContent"
[(delayLoadDummyContent)]="delayLoadDummyContent"
[(loadAdditionalContent)]="loadAdditionalContent"
Expand Down Expand Up @@ -115,6 +116,21 @@ export class EmbeddedComponent() {
const returnData: CustomDataType = {...};
this.modal?.close(returnData);
}`,
alertBeforeCloseCodeSnippet: `// Inside the parent (caller) component:
@Component()
export class ParentComponent() {
const alertConfig: AlertConfig = {
title: 'Do you want to close the modal?',
okBtn: {
text: 'Yes',
isDestructive: true,
},
cancelBtn: 'No',
};
this.modalController.showModal(config, null, alertConfig)
}
`,
scrollingCodeSnippet: `import { KirbyAnimation, Modal } from '@kirbydesign/designsystem';
...
constructor(@Optional() @SkipSelf() private modal: Modal) {}
Expand Down Expand Up @@ -197,6 +213,7 @@ export class ModalExampleDefaultComponent {
drawerCodeSnippet = config.drawerCodeSnippet;
callbackCodeSnippet = config.callbackCodeSnippet;
callbackWithDataCodeSnippet = config.callbackWithDataCodeSnippet;
alertBeforeCloseCodeSnippet = config.alertBeforeCloseCodeSnippet;
didPresentCodeSnippet = config.didPresentCodeSnippet;
willCloseCodeSnippet = config.willCloseCodeSnippet;
scrollingCodeSnippet = config.scrollingCodeSnippet;
Expand All @@ -211,6 +228,7 @@ export class ModalExampleDefaultComponent {
showFooter = false;
displayFooterAsInline = false;
collapseTitle = false;
alertBeforeClose = false;
showDummyContent = true;
delayLoadDummyContent = true;
loadAdditionalContent = false;
Expand Down Expand Up @@ -256,7 +274,20 @@ export class ModalExampleDefaultComponent {
openFullHeight: this.openFullHeight,
},
};
await this.modalController.showModal(config, this.onOverlayClose.bind(this));

let alertConfig: AlertConfig = null;
if (this.alertBeforeClose) {
alertConfig = {
title: 'Do you want to close the modal?',
okBtn: {
text: 'Yes',
isDestructive: true,
},
cancelBtn: 'No',
};
}

await this.modalController.showModal(config, this.onOverlayClose.bind(this), alertConfig);
}

async showModal() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
Support for <a href="#" (click)="scrollTo(returnData)">returning data</a> from the modal to the
parent/caller component
</li>
<li>
Support for <a href="#" (click)="scrollTo(alertBeforeClose)">showing an alert before closing</a>
</li>
<li>Support for <a href="#" (click)="scrollTo(scrolling)">scrolling</a></li>
<li>Support for <a href="#" (click)="scrollTo(disableScrolling)">disabling of scrolling</a></li>
<li>(Optional) <a href="#" (click)="scrollTo(footer)">Fixed footer</a></li>
Expand Down Expand Up @@ -171,6 +174,13 @@ <h2 #returnData>Return data from the modal <em>(optional)</em>:</h2>
<cookbook-code-viewer [ts]="defaultExample.callbackWithDataCodeSnippet"></cookbook-code-viewer>
</p>

<h2 #alertBeforeClose>Show an alert before dismissing the modal <em>(optional)</em>:</h2>
<p>
If you need the user to confirm that the modal should be closed, you can pass an AlertConfig as an
optional third argument
<cookbook-code-viewer [ts]="defaultExample.alertBeforeCloseCodeSnippet"></cookbook-code-viewer>
</p>

<h2>Inside the embedded component:</h2>
<p>To create an embedded component, ensure:</p>
<ol>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,15 @@ export class ModalController implements OnDestroy {
});
}

public async showModal(config: ModalConfig, onClose?: (data?: any) => void): Promise<void> {
await this.showAndRegisterOverlay(() => this.modalHelper.showModalWindow(config), onClose);
public async showModal(
config: ModalConfig,
onClose?: (data?: any) => void,
alertConfig?: AlertConfig
): Promise<void> {
await this.showAndRegisterOverlay(
() => this.modalHelper.showModalWindow(config, alertConfig),
onClose
);
}

public async navigateToModal(path: string | string[], queryParams?: Params): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Component, ElementRef, OnInit, Optional, ViewChild } from '@angular/cor
import { RouterTestingModule } from '@angular/router/testing';
import { ModalController as IonicModalController } from '@ionic/angular';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { MockComponents } from 'ng-mocks';

import { DesignTokenHelper } from '@kirbydesign/core';

Expand All @@ -16,9 +15,11 @@ import { ModalCompactWrapperComponent } from '../modal-wrapper/compact/modal-com
import { ModalConfig, ModalSize } from '../modal-wrapper/config/modal-config';
import { ModalWrapperComponent } from '../modal-wrapper/modal-wrapper.component';

import { AlertConfig } from '../alert/config/alert-config';
import { ModalNavigationService } from './modal-navigation.service';
import { ModalHelper } from './modal.helper';
import { Modal, Overlay } from './modal.interfaces';
import { AlertHelper } from './alert.helper';

@Component({
template: `
Expand Down Expand Up @@ -106,7 +107,7 @@ describe('ModalHelper', () => {
ContentOverflowsWithFooterEmbeddedComponent,
ContentWithNoOverflowEmbeddedComponent,
],
mocks: [ModalNavigationService],
mocks: [ModalNavigationService, AlertHelper],
});

beforeAll(() => {
Expand Down Expand Up @@ -135,14 +136,14 @@ describe('ModalHelper', () => {
await overlay.dismiss();
});

const openOverlay = async (config: ModalConfig) => {
overlay = await modalHelper.showModalWindow(config);
const openOverlay = async (config: ModalConfig, alertConfig?: AlertConfig) => {
overlay = await modalHelper.showModalWindow(config, alertConfig);
ionModal = await ionModalController.getTop();
expect(ionModal).toBeTruthy();
};

const openModal = async (component?: any, size?: ModalSize) => {
await openOverlay({ flavor: 'modal', component, size });
const openModal = async (component?: any, size?: ModalSize, alertConfig?: AlertConfig) => {
await openOverlay({ flavor: 'modal', component, size }, alertConfig);
};

const openDrawer = async (
Expand All @@ -166,6 +167,29 @@ describe('ModalHelper', () => {
expect(document.activeElement).toEqual(input);
});

describe('canDismiss', () => {
it('should pass "true" to "canDismiss", if no alertConfig is provided', async () => {
await openModal();

expect(ionModal.canDismiss).toEqual(true);
});

it('should pass a function to "canDismiss", if an alertConfig is provided', async () => {
const alertConfig: AlertConfig = {
title: 'Do you want to close the modal?',
okBtn: 'Yes',
cancelBtn: 'No',
};
// Mock 'showAlert' to prevent the test from timing out
// due to nested async that is not resolved
spectator.service.showAlert = async () => true;

await openModal(null, null, alertConfig);

expect(typeof ionModal?.canDismiss).toEqual('function');
});
});

describe(`when drawer can interact with background`, () => {
beforeEach(async () => {
await openDrawer(null, null, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { ModalController } from '@ionic/angular';

import { KirbyAnimation } from '../../../animation/kirby-animation';
import { WindowRef } from '../../../types/window-ref';
import { AlertConfig } from '../alert/config/alert-config';
import { ModalCompactWrapperComponent } from '../modal-wrapper/compact/modal-compact-wrapper.component';
import { ModalConfig, ModalFlavor, ModalSize } from '../modal-wrapper/config/modal-config';
import { ModalWrapperComponent } from '../modal-wrapper/modal-wrapper.component';
import { AlertHelper } from './alert.helper';

import { ModalAnimationBuilderService } from './modal-animation-builder.service';
import { Overlay } from './modal.interfaces';
Expand All @@ -19,10 +21,11 @@ export class ModalHelper {
constructor(
private ionicModalController: ModalController,
private modalAnimationBuilder: ModalAnimationBuilderService,
private windowRef: WindowRef
private windowRef: WindowRef,
private alertHelper: AlertHelper
) {}

public async showModalWindow(config: ModalConfig): Promise<Overlay> {
public async showModalWindow(config: ModalConfig, alertConfig?: AlertConfig): Promise<Overlay> {
config.flavor = config.flavor || 'modal';
const modalPresentingElement = await this.getPresentingElement(config.flavor);

Expand All @@ -49,6 +52,14 @@ export class ModalHelper {
this.windowRef.nativeWindow.document.body.classList.add(allow_scroll_class);
}

let canDismiss: boolean | (() => Promise<boolean>) = true;
if (alertConfig) {
canDismiss = async () => {
const canBeDismissed = await this.showAlert(alertConfig);
return canBeDismissed;
};
}

const ionModal = await this.ionicModalController.create({
component: config.flavor === 'compact' ? ModalCompactWrapperComponent : ModalWrapperComponent,
cssClass: [
Expand All @@ -66,6 +77,7 @@ export class ModalHelper {
swipeToClose: config.flavor != 'compact',
presentingElement: modalPresentingElement,
keyboardClose: false,
canDismiss,
enterAnimation,
leaveAnimation,
});
Expand All @@ -89,6 +101,12 @@ export class ModalHelper {
ModalHelper.presentingElement = element;
}

public async showAlert(config: AlertConfig): Promise<boolean> {
const alert = await this.alertHelper.showAlert(config);
const result = await alert.onWillDismiss;
return result.data;
}

private async getPresentingElement(flavor?: ModalFlavor) {
let modalPresentingElement: HTMLElement = undefined;
if (!flavor || flavor === 'modal') {
Expand Down

0 comments on commit 96ac460

Please sign in to comment.