Skip to content

Commit

Permalink
feat(ngx-dialog): Restrict dialog close on closeOnEscape or closeOnBl…
Browse files Browse the repository at this point in the history
…ur via beforeClose (#891)

* feat: enhance dialog close behavior more controllable with beforeClose

Co-authored-by: Jayson Harshbarger <[email protected]>
  • Loading branch information
surya-pabbineedi and Hypercubed authored Jul 7, 2022
1 parent eb5bfd0 commit 9c83c2c
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 46 deletions.
2 changes: 2 additions & 0 deletions projects/swimlane/ngx-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## HEAD (unreleased)

- Enhancement (`ngx-dialog`): Dialog close behavior can be controlled by `beforeClose` method when `closeOnEscape` or `closeOnBlur` are `true`

## 42.0.8 (2022-6-24)

- Fix (`ngx-json-editor-flat`): Applying titlecase pipe to type value in html.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,6 @@ describe('DialogComponent', () => {
});
});

describe('containsTarget', () => {
it('should be true when closeOnBlur and contains dialog', () => {
const target = { classList: { contains: () => true } };
component.closeOnBlur = true;
expect(component.containsTarget(target)).toBeTruthy();
});

it('should be false when !closeOnBlur', () => {
const target = { classList: { contains: () => true } };
component.closeOnBlur = false;
expect(component.containsTarget(target)).toBeFalsy();
});
});

describe('onEscapeKeyDown', () => {
it('should call hide when closeOnEscape', () => {
const spy = spyOn(component, 'hide');
Expand All @@ -104,23 +90,15 @@ describe('DialogComponent', () => {
component.onEscapeKeyDown();
expect(spy).not.toHaveBeenCalled();
});
});

describe('onDocumentClick', () => {
it('should hide if contains target', () => {
const spy = spyOn(component, 'containsTarget').and.returnValue(true);
component.visible = true;
component.onDocumentClick({});
expect(spy).toHaveBeenCalled();
expect(component.visible).toBeFalsy();
});

it('should not hide if doesnt contain target', () => {
const spy = spyOn(component, 'containsTarget').and.returnValue(false);
component.visible = true;
component.onDocumentClick({});
expect(spy).toHaveBeenCalled();
expect(component.visible).toBeTruthy();
it('should not invoke .hide() if closeOnEscape is true but canClose is false', () => {
const hideSpy = spyOn(component, 'hide');
const beforeCloseSpy = jasmine.createSpy('beforeClose', () => false);
component.beforeClose = beforeCloseSpy;
component.closeOnEscape = true;
component.onEscapeKeyDown();
expect(beforeCloseSpy).toHaveBeenCalled();
expect(hideSpy).not.toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ export class DialogComponent implements OnInit, OnDestroy {
this._zIndex = coerceNumberProperty(zIndex);
}

@Input() beforeClose: () => boolean;

@Output() open = new EventEmitter<boolean | void>();
@Output() close = new EventEmitter<boolean | void>();

Expand All @@ -123,6 +125,10 @@ export class DialogComponent implements OnInit, OnDestroy {
return this.visible ? 'active' : 'inactive';
}

get canClose(): boolean {
return this.beforeClose ? this.beforeClose() : true;
}

readonly DialogFormat = DialogFormat;

private _closeOnBlur?: boolean;
Expand All @@ -134,7 +140,10 @@ export class DialogComponent implements OnInit, OnDestroy {
constructor(private readonly element: ElementRef, private readonly renderer2: Renderer2) {}

ngOnInit(): void {
if (this.visible) this.show();
if (this.visible) {
this.show();
this.element.nativeElement?.focus();
}
// backwards compatibility
if (this.title) {
this.dialogTitle = this.title;
Expand All @@ -156,18 +165,9 @@ export class DialogComponent implements OnInit, OnDestroy {
this.close.emit();
}

containsTarget(target: any): boolean {
return this.closeOnBlur && target.classList.contains('dialog');
}

@HostListener('keydown.esc')
onEscapeKeyDown(): void {
if (this.closeOnEscape) this.hide();
}

@HostListener('document:click', ['$event.target'])
onDocumentClick(target: any): void {
if (this.containsTarget(target)) {
onEscapeKeyDown() {
if (this.closeOnEscape && this.canClose) {
this.hide();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EventEmitter } from '@angular/core';
import { InjectionService } from '../../services/injection/injection.service';
import { OverlayService } from '../overlay/overlay.service';
import { DialogService } from './dialog.service';
import { of } from 'rxjs';

describe('DialogService', () => {
let service: DialogService;
Expand All @@ -14,7 +15,7 @@ describe('DialogService', () => {
const overlayServiceStub = {
removeTriggerComponent: () => ({}),
show: () => ({}),
click: { subscribe: () => ({}) },
click: of({}),
instance: { zIndex: {} }
};

Expand Down Expand Up @@ -90,6 +91,23 @@ describe('DialogService', () => {
expect(spy).toHaveBeenCalled();
expect(overlaySpy).not.toHaveBeenCalled();
});

it('should not close on blur if beforeClose returns false', () => {
const component = {
instance: {
close: new EventEmitter<void>(),
showOverlay: true,
closeOnBlur: true,
beforeClose: () => false
}
};
const spy = spyOn(injectionService, 'appendComponent').and.returnValue(component as any);
const overlaySpy = spyOn(overlayService.click, 'subscribe');

service.create({ closeOnBlur: true });
expect(spy).toHaveBeenCalled();
expect(overlaySpy).not.toHaveBeenCalled();
});
});

describe('destroy', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { InjectionRegistryService } from '../../services/injection-registry/injection-registry.service';

import { InjectionService } from '../../services/injection/injection.service';
Expand Down Expand Up @@ -76,7 +77,7 @@ export class DialogService<T = DialogComponent> extends InjectionRegistryService
zIndex
});
if (triggerComponent.instance.closeOnBlur) {
overlaySub = this.overlayService.click.subscribe(kill);
overlaySub = this.overlayService.click.pipe(filter(() => triggerComponent.instance.canClose)).subscribe(kill);
}
});
}
Expand Down
60 changes: 60 additions & 0 deletions src/app/dialogs/dialog-page/dialog-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h3 class="style-header">Dialog</h3>
<ngx-section class="shadow" [sectionTitle]="'Component'">
<ngx-dialog dialogTitle="Attack Alert" *ngIf="dialogVis" (onClose)="dialogVis = false" [zIndex]="10">
<p>Attack Found!</p>
<ngx-input label="Details" [autofocus]="true"></ngx-input>
</ngx-dialog>

<button type="button" class="btn" (click)="dialogVis = !dialogVis">
Expand Down Expand Up @@ -285,3 +286,62 @@ <h1>Full screen</h1>
</ngx-tab>
</ngx-tabs>
</ngx-section>

<ngx-section class="shadow" [sectionTitle]="'closeOnBlur & closeOnEscape'">
<ng-template #dirtyDialogTmpl let-context="context">
<p>
Can close the dialog on esc or blur: <i>{{ canClose | json }}</i>
</p>

<button type="button" class="btn" (click)="canClose = !canClose">Toggle canClose</button>
</ng-template>

<button
type="button"
class="btn"
(click)="openDialog({ title: 'Dirty Check', closeOnBlur: true, closeOnEscape: true, beforeClose, template: dirtyDialogTmpl })"
>
Open Dialog
</button>

<br />
<br />

<ngx-tabs>
<ngx-tab label="Markup">
<app-prism>
<![CDATA[<button type="button" class="btn" (click)="openDialog({ title: 'Dirty Check', closeOnBlur: true, closeOnEscape: true, beforeClose, template: dirtyDialogTmpl })"
> Open Dialog </button>]]>
</app-prism>
</ngx-tab>
<ngx-tab label="TypeScript">
<app-prism language="js">
<![CDATA[import { ChangeDetectionStrategy, Component, TemplateRef, ViewChild } from '@angular/core';
import { DialogService } from '@swimlane/ngx-ui';

@Component({
selector: 'app-dialog-page',
templateUrl: './dialog-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogPageComponent {
dialogVis: any;
canClose = true;

@ViewChild('dialogTmpl', { static: true })
dialogTpl: TemplateRef<any>;

constructor(public dialogMngr: DialogService) {}

openDialog(options) {
this.dialogMngr.create(options);
}

beforeClose = (): boolean => {
return this.canClose;
};
}]]>
</app-prism>
</ngx-tab>
</ngx-tabs>
</ngx-section>
5 changes: 5 additions & 0 deletions src/app/dialogs/dialog-page/dialog-page.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DialogService } from '@swimlane/ngx-ui';
})
export class DialogPageComponent {
dialogVis: any;
canClose = true;

@ViewChild('dialogTmpl', { static: true })
dialogTpl: TemplateRef<any>;
Expand All @@ -17,4 +18,8 @@ export class DialogPageComponent {
openDialog(options) {
this.dialogMngr.create(options);
}

beforeClose = (): boolean => {
return this.canClose;
};
}
13 changes: 11 additions & 2 deletions src/app/dialogs/dialog-page/dialog-page.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { DialogModule, SectionModule, TabsModule } from '@swimlane/ngx-ui';
import { DialogModule, SectionModule, TabsModule, InputModule } from '@swimlane/ngx-ui';
import { PrismModule } from '../../common/prism/prism.module';

import { DialogPageRoutingModule } from './dialog-page-routing.module';
import { DialogPageComponent } from './dialog-page.component';

@NgModule({
declarations: [DialogPageComponent],
imports: [CommonModule, FormsModule, PrismModule, SectionModule, DialogModule, TabsModule, DialogPageRoutingModule]
imports: [
CommonModule,
FormsModule,
PrismModule,
InputModule,
SectionModule,
DialogModule,
TabsModule,
DialogPageRoutingModule
]
})
export class DialogPageModule {}

0 comments on commit 9c83c2c

Please sign in to comment.