Skip to content

Commit

Permalink
feat(module: popconfirm): support async close (#7533)
Browse files Browse the repository at this point in the history
Co-authored-by: luolei <luolei@kuaishou.com>
  • Loading branch information
rorry121 and luolei committed Aug 4, 2022
1 parent 60beabc commit 797b261
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 5 deletions.
16 changes: 16 additions & 0 deletions components/popconfirm/demo/async.md
@@ -0,0 +1,16 @@
---
order: 6
title:
zh-CN: 异步关闭
en-US: Asynchronously close
---

## zh-CN

点击确定后异步关闭 Popconfirm,例如提交表单。

## en-US

Asynchronously close a popconfirm when a the OK button is pressed. For example, you can use this pattern when you submit a form.


41 changes: 41 additions & 0 deletions components/popconfirm/demo/async.ts
@@ -0,0 +1,41 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs';

import { NzMessageService } from 'ng-zorro-antd/message';

@Component({
selector: 'nz-demo-popconfirm-async',
template: `
<button
nz-popconfirm
nzPopconfirmTitle="Title"
[nzBeforeConfirm]="beforeConfirm"
(nzOnConfirm)="confirm()"
(nzOnCancel)="cancel()"
nz-button
nzType="primary"
>
Open Popconfirm with async logic
</button>
`
})
export class NzDemoPopconfirmAsyncComponent {
cancel(): void {
this.nzMessageService.info('click cancel');
}

confirm(): void {
this.nzMessageService.info('click confirm');
}

beforeConfirm(): Observable<boolean> {
return new Observable(observer => {
setTimeout(() => {
observer.next(true);
observer.complete();
}, 3000);
});
}

constructor(private nzMessageService: NzMessageService) {}
}
16 changes: 16 additions & 0 deletions components/popconfirm/demo/promise.md
@@ -0,0 +1,16 @@
---
order: 7
title:
zh-CN: 基于 Promise 的异步关闭
en-US: Asynchronously close on Promise
---

## zh-CN

点击确定后异步关闭 Popconfirm,例如提交表单。

## en-US

Asynchronously close a popconfirm when the OK button is pressed. For example, you can use this pattern when you submit a form.


39 changes: 39 additions & 0 deletions components/popconfirm/demo/promise.ts
@@ -0,0 +1,39 @@
import { Component } from '@angular/core';

import { NzMessageService } from 'ng-zorro-antd/message';

@Component({
selector: 'nz-demo-popconfirm-promise',
template: `
<button
nz-popconfirm
nzPopconfirmTitle="Title"
[nzBeforeConfirm]="beforeConfirm"
(nzOnConfirm)="confirm()"
(nzOnCancel)="cancel()"
nz-button
nzType="primary"
>
Open Popconfirm with Promise
</button>
`
})
export class NzDemoPopconfirmPromiseComponent {
cancel(): void {
this.nzMessageService.info('click cancel');
}

confirm(): void {
this.nzMessageService.info('click confirm');
}

beforeConfirm(): Promise<boolean> {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 3000);
});
}

constructor(private nzMessageService: NzMessageService) {}
}
1 change: 1 addition & 0 deletions components/popconfirm/doc/index.en-US.md
Expand Up @@ -46,6 +46,7 @@ import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
| `[nzCondition]` | Whether to directly emit `onConfirm` without showing Popconfirm | `boolean` | `false` | - |
| `[nzIcon]` | Customize icon of confirmation | `string \| TemplateRef<void>` | - | - |
| `[nzAutoFocus]` | Autofocus a button | `null \| 'ok' \| 'cancel'` | `null` ||
| `[nzBeforeConfirm]` | The hook before the confirmation operation, decides whether to continue responding to the `nzOnConfirm` callback, supports asynchronous verification. | `(() => Observable<boolean> \| Promise<boolean> \| boolean) \| null` | `null` | - |
| `(nzOnCancel)` | Callback of cancel | `EventEmitter<void>` | - | - |
| `(nzOnConfirm)` | Callback of confirmation | `EventEmitter<void>` | - | - |

Expand Down
1 change: 1 addition & 0 deletions components/popconfirm/doc/index.zh-CN.md
Expand Up @@ -50,6 +50,7 @@ import { NzPopconfirmModule } from 'ng-zorro-antd/popconfirm';
| `[nzCondition]` | 是否直接触发 `nzOnConfirm` 而不弹出框 | `boolean` | `false` | - |
| `[nzIcon]` | 自定义弹出框的 icon | `string \| TemplateRef<void>` | - | - |
| `[nzAutoFocus]` | 按钮的自动聚焦 | `null \| 'ok' \| 'cancel'` | `null` ||
| `[nzBeforeConfirm]` | 确认操作之前的钩子,决定是否继续响应 `nzOnConfirm` 回调,支持异步验证。 | `(() => Observable<boolean> \| Promise<boolean> \| boolean) \| null` | `null` | - |
| `(nzOnCancel)` | 点击取消的回调 | `EventEmitter<void>` | - | - |
| `(nzOnConfirm)` | 点击确认的回调 | `EventEmitter<void>` | - | - |

Expand Down
71 changes: 71 additions & 0 deletions components/popconfirm/popconfirm.spec.ts
Expand Up @@ -3,6 +3,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Observable } from 'rxjs';

import { NzButtonType } from 'ng-zorro-antd/button';
import { dispatchMouseEvent } from 'ng-zorro-antd/core/testing';
Expand Down Expand Up @@ -144,6 +145,74 @@ describe('NzPopconfirm', () => {
expect(component.cancel).toHaveBeenCalledTimes(0);
}));

it('should before confirm work', fakeAsync(() => {
const triggerElement = component.stringTemplate.nativeElement;

dispatchMouseEvent(triggerElement, 'click');
fixture.detectChanges();
expect(getTitleText()!.textContent).toContain('title-string');
expect(component.confirm).toHaveBeenCalledTimes(0);
expect(component.cancel).toHaveBeenCalledTimes(0);

component.beforeConfirm = () => false;
fixture.detectChanges();

dispatchMouseEvent(getTooltipTrigger(1), 'click');
waitingForTooltipToggling();
expect(component.confirm).toHaveBeenCalledTimes(0);
expect(component.cancel).toHaveBeenCalledTimes(0);
expect(getTitleText()!.textContent).toContain('title-string');
}));

it('should before confirm observable work', fakeAsync(() => {
const triggerElement = component.stringTemplate.nativeElement;

dispatchMouseEvent(triggerElement, 'click');
fixture.detectChanges();
expect(getTitleText()!.textContent).toContain('title-string');
expect(component.confirm).toHaveBeenCalledTimes(0);
expect(component.cancel).toHaveBeenCalledTimes(0);

component.beforeConfirm = () =>
new Observable(observer => {
setTimeout(() => {
observer.next(true);
observer.complete();
}, 200);
});

dispatchMouseEvent(getTooltipTrigger(1), 'click');
tick(200 + 10);
waitingForTooltipToggling();
expect(getTitleText()).toBeNull();
expect(component.confirm).toHaveBeenCalledTimes(1);
expect(component.cancel).toHaveBeenCalledTimes(0);
}));

it('should before confirm promise work', fakeAsync(() => {
const triggerElement = component.stringTemplate.nativeElement;

dispatchMouseEvent(triggerElement, 'click');
fixture.detectChanges();
expect(getTitleText()!.textContent).toContain('title-string');
expect(component.confirm).toHaveBeenCalledTimes(0);
expect(component.cancel).toHaveBeenCalledTimes(0);

component.beforeConfirm = () =>
new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 200);
});

dispatchMouseEvent(getTooltipTrigger(1), 'click');
tick(200 + 10);
waitingForTooltipToggling();
expect(getTitleText()).toBeNull();
expect(component.confirm).toHaveBeenCalledTimes(1);
expect(component.cancel).toHaveBeenCalledTimes(0);
}));

it('should nzPopconfirmShowArrow work', fakeAsync(() => {
const triggerElement = component.stringTemplate.nativeElement;
dispatchMouseEvent(triggerElement, 'click');
Expand Down Expand Up @@ -178,6 +247,7 @@ describe('NzPopconfirm', () => {
nzCancelText="cancel-text"
[nzAutofocus]="autoFocus"
[nzCondition]="condition"
[nzBeforeConfirm]="beforeConfirm"
[nzPopconfirmShowArrow]="nzPopconfirmShowArrow"
[nzPopconfirmBackdrop]="nzPopconfirmBackdrop"
(nzOnConfirm)="confirm()"
Expand Down Expand Up @@ -210,6 +280,7 @@ export class NzPopconfirmTestNewComponent {
icon: string | undefined = undefined;
nzPopconfirmBackdrop = false;
autoFocus: NzAutoFocusType = null;
beforeConfirm: (() => Observable<boolean> | Promise<boolean> | boolean) | null = null;

@ViewChild('stringTemplate', { static: false }) stringTemplate!: ElementRef;
@ViewChild('templateTemplate', { static: false }) templateTemplate!: ElementRef;
Expand Down
39 changes: 34 additions & 5 deletions components/popconfirm/popconfirm.ts
Expand Up @@ -27,15 +27,15 @@ import {
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { finalize, first, takeUntil } from 'rxjs/operators';

import { NzButtonType } from 'ng-zorro-antd/button';
import { zoomBigMotion } from 'ng-zorro-antd/core/animation';
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
import { BooleanInput, NgStyleInterface, NzSafeAny, NzTSType } from 'ng-zorro-antd/core/types';
import { InputBoolean } from 'ng-zorro-antd/core/util';
import { InputBoolean, wrapIntoObservable } from 'ng-zorro-antd/core/util';
import { NzTooltipBaseDirective, NzToolTipComponent, NzTooltipTrigger, PropertyMapping } from 'ng-zorro-antd/tooltip';

export type NzAutoFocusType = null | 'ok' | 'cancel';
Expand Down Expand Up @@ -70,6 +70,7 @@ export class NzPopconfirmDirective extends NzTooltipBaseDirective {
@Input() nzOkType?: string;
@Input() nzOkDanger?: boolean;
@Input() nzCancelText?: string;
@Input() nzBeforeConfirm?: () => Observable<boolean> | Promise<boolean> | boolean;
@Input() nzIcon?: string | TemplateRef<void>;
@Input() @InputBoolean() nzCondition: boolean = false;
@Input() @InputBoolean() nzPopconfirmShowArrow: boolean = true;
Expand All @@ -90,6 +91,7 @@ export class NzPopconfirmDirective extends NzTooltipBaseDirective {
nzOkType: ['nzOkType', () => this.nzOkType],
nzOkDanger: ['nzOkDanger', () => this.nzOkDanger],
nzCancelText: ['nzCancelText', () => this.nzCancelText],
nzBeforeConfirm: ['nzBeforeConfirm', () => this.nzBeforeConfirm],
nzCondition: ['nzCondition', () => this.nzCondition],
nzIcon: ['nzIcon', () => this.nzIcon],
nzPopconfirmShowArrow: ['nzPopconfirmShowArrow', () => this.nzPopconfirmShowArrow],
Expand Down Expand Up @@ -190,6 +192,7 @@ export class NzPopconfirmDirective extends NzTooltipBaseDirective {
[nzSize]="'small'"
[nzType]="nzOkType !== 'danger' ? nzOkType : 'primary'"
[nzDanger]="nzOkDanger || nzOkType === 'danger'"
[nzLoading]="confirmLoading"
(click)="onConfirm()"
[attr.cdkFocusInitial]="nzAutoFocus === 'ok' || null"
>
Expand Down Expand Up @@ -217,6 +220,7 @@ export class NzPopconfirmComponent extends NzToolTipComponent implements OnDestr
nzOkType: NzButtonType | 'danger' = 'primary';
nzOkDanger: boolean = false;
nzAutoFocus: NzAutoFocusType = null;
nzBeforeConfirm: (() => Observable<boolean> | Promise<boolean> | boolean) | null = null;

readonly nzOnCancel = new Subject<void>();
readonly nzOnConfirm = new Subject<void>();
Expand All @@ -227,6 +231,8 @@ export class NzPopconfirmComponent extends NzToolTipComponent implements OnDestr

override _prefix = 'ant-popover';

confirmLoading = false;

constructor(
cdr: ChangeDetectorRef,
private elementRef: ElementRef,
Expand Down Expand Up @@ -262,14 +268,37 @@ export class NzPopconfirmComponent extends NzToolTipComponent implements OnDestr
this.restoreFocus();
}

handleConfirm(): void {
this.nzOnConfirm.next();
super.hide();
}

onCancel(): void {
this.nzOnCancel.next();
super.hide();
}

onConfirm(): void {
this.nzOnConfirm.next();
super.hide();
if (this.nzBeforeConfirm) {
const observable = wrapIntoObservable(this.nzBeforeConfirm()).pipe(first());
this.confirmLoading = true;
observable
.pipe(
finalize(() => {
this.confirmLoading = false;
this.cdr.markForCheck();
}),
takeUntil(this.nzVisibleChange),
takeUntil(this.destroy$)
)
.subscribe(value => {
if (value) {
this.handleConfirm();
}
});
} else {
this.handleConfirm();
}
}

private capturePreviouslyFocusedElement(): void {
Expand Down

0 comments on commit 797b261

Please sign in to comment.