Skip to content

Feat: prevent closing with option for confirmation dialog#131

Merged
yonathan-utila merged 11 commits intomasterfrom
close-protection
Feb 5, 2026
Merged

Feat: prevent closing with option for confirmation dialog#131
yonathan-utila merged 11 commits intomasterfrom
close-protection

Conversation

@yonathan-utila
Copy link
Copy Markdown
Collaborator

@yonathan-utila yonathan-utila commented Feb 4, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our guidelines: CONTRIBUTING.md#commit
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Documentation content changes
[ ] Other... Please describe:

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

[ ] Yes
[ ] No

Other information

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

closeGuardAction: 'escape',
});

// guard will fire on esdc keypress
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

ref?.beforeClose(async () => {
const confirmRef = this.showConfirmationDialog();
const result = await firstValueFrom(confirmRef.afterClosed$);
return result === true;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return result

</head>
<body>
<app-root />
<app-root></app-root>
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant ? check this


bootstrapApplication(AppComponent, {
providers: [
provideZoneChangeDetection(),
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check this

beforeCloseGuards: GuardFN<unknown>[] = [];
onClose: (result?: unknown) => void;
onReset: (offset?: DragOffset) => void;
private currentCloseAction?: CloseGuardAction;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find better naming, activeGuardAction

.prettierignore Outdated
.next
out

# Logs
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove that

"importHelpers": true,
"target": "ES2022",
"lib": ["es2022", "dom"],
"skipLibCheck": true,
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NetanelBasal lets talk about this flag. I had some troubles while migrating because of some exposed angular module.

@yonathan-utila yonathan-utila changed the title Close protection Feat: prevent closing with option for confirmation dialog Feb 4, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a configurable close-guard mechanism to the dialog library so beforeClose guards can be limited to specific close triggers (ESC/backdrop/close button), and updates the playground to demonstrate confirmation-on-close behavior. This PR also includes a broad Angular/Nx/tooling upgrade.

Changes:

  • Introduce closeGuardAction/CloseGuardAction and thread close “action” through ESC/backdrop/close-button close paths to selectively run beforeClose guards.
  • Update playground with a confirmation dialog + demos for “ESC-only” and “all-actions” protection.
  • Upgrade Angular/Nx/tooling versions and adjust related workspace config (migrations, targets, formatting ignores).

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tsconfig.base.json Enables skipLibCheck during TS compilation.
package.json Major Angular/Nx/tooling dependency updates and script changes.
migrations.json Replaced/expanded migration entries (Nx migrations).
libs/dialog/src/public-api.ts Exposes CloseGuardAction from the library public API.
libs/dialog/src/lib/types.ts Adds CloseGuardAction type + closeGuardAction config option.
libs/dialog/src/lib/dialog.component.ts Tracks close origin (ESC/backdrop) and passes action into close().
libs/dialog/src/lib/dialog-ref.ts Adds action-aware close handling and guard gating (shouldRunGuards).
apps/playground/src/main.ts Adds provideZoneChangeDetection() to providers.
apps/playground/src/index.html Fixes <app-root> element markup.
apps/playground/src/app/confirmation-dialog.component.ts Adds a simple confirmation dialog component.
apps/playground/src/app/app.component.ts Adds demos for action-specific close guards + confirmation flow.
apps/playground/src/app/app.component.html Adds UI section to test close protection guards.
apps/playground/project.json Updates serve target option from browserTarget to buildTarget.
.prettierignore Expands ignored files (deps, builds, lockfiles, generated artifacts).
Comments suppressed due to low confidence (1)

libs/dialog/src/lib/dialog-ref.ts:49

  • InternalDialogRef.close() defaults action to 'closeButton', but close() is also used for programmatic closes (e.g., DialogService.closeAll() and any consumer calling ref.close()), so those will be treated as a close-button action too. If closeGuardAction: 'closeButton' is intended to mean an actual UI close-button click, consider adding a distinct action like 'programmatic' (and default to that), and pass 'closeButton' only from the dialog close button handler/directive.
  close(result?: unknown, action: CloseGuardAction = 'closeButton'): void {
    this.currentCloseAction = action;
    this.canClose(result)
      .pipe(filter<boolean>(Boolean))
      .subscribe({ next: () => this.onClose(result) });
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +86
@@ -55,6 +57,13 @@ export class InternalDialogRef extends DialogRef {
}

canClose(result: unknown): Observable<boolean> {
const shouldRunGuards = this.shouldRunGuards();

if (!shouldRunGuards) {
// skip checks, close the dialog right away
return of(true);
}

const guards$ = this.beforeCloseGuards
.map((guard) => guard(result))
.filter((value) => value !== undefined && value !== true)
@@ -65,6 +74,17 @@ export class InternalDialogRef extends DialogRef {
return merge(...guards$).pipe(defaultIfEmpty(true), first());
}

private shouldRunGuards(): boolean {
const { closeGuardAction } = this.config;
// we want to make sure that user already using a guard but NOT using the closeActionGuard type, that their checks still run
if (!closeGuardAction || closeGuardAction === 'all') {
return true;
}

//else, if the user has provided a closeGuardAction, only run it if it matches the currentCloseAction
return closeGuardAction === this.currentCloseAction;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new closeGuardAction behavior is not covered by existing unit tests. There are already extensive beforeClose tests in libs/dialog/src/lib/specs/dialog.service.spec.ts, but none verify that guards run only for the configured action (e.g., run on 'escape' but skip on 'backdrop'). Please add tests covering at least: action matches, action mismatches, and 'all' behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +24
export class ConfirmationDialogComponent {
message: string;

private dialogRef = inject(DialogRef<boolean>);

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfirmationDialogComponent declares a message field but never uses it, and the dialog data passed in showConfirmationDialog() is also unused. Either remove the unused field/data, or render the message (e.g., read from dialogRef.data) so the component reflects the provided config.

Copilot uses AI. Check for mistakes.
package.json Outdated
Comment on lines +21 to +28
"@angular/animations": "21.1.2",
"@angular/common": "^21.1.2",
"@angular/compiler": "21.1.2",
"@angular/core": "21.1.2",
"@angular/forms": "21.1.2",
"@angular/platform-browser": "21.1.2",
"@angular/platform-browser-dynamic": "21.1.2",
"@angular/router": "21.1.2",
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Angular framework packages should typically be kept on the exact same version range to avoid mismatched peer dependency resolutions. Here @angular/common is ^21.1.2 while most other @angular/* dependencies are pinned to 21.1.2. Align the version specifiers (either pin all to the same exact version or use the same range for all).

Copilot uses AI. Check for mistakes.
fromEvent<KeyboardEvent>(this.document.body, 'keyup').pipe(
filter(({ key }) => key === 'Escape'),
map(() => closeConfig.escape),
// wr now track what truggers the actual closing, esc or backdrop click so we can act accordingly with the closingGuards
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment contains typos (e.g., "truggers") and is hard to read. Please rewrite it clearly (or remove it) so future maintainers can understand why the action is being tracked.

Suggested change
// wr now track what truggers the actual closing, esc or backdrop click so we can act accordingly with the closingGuards
// Track whether the dialog is closed via the Escape key or a backdrop click so close guards can react accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +84
// we want to make sure that user already using a guard but NOT using the closeActionGuard type, that their checks still run
if (!closeGuardAction || closeGuardAction === 'all') {
return true;
}

//else, if the user has provided a closeGuardAction, only run it if it matches the currentCloseAction
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments in shouldRunGuards() have multiple typos and refer to a "closeActionGuard type" that doesn't exist in this code. Please fix the wording to accurately describe the closeGuardAction behavior to avoid confusion.

Suggested change
// we want to make sure that user already using a guard but NOT using the closeActionGuard type, that their checks still run
if (!closeGuardAction || closeGuardAction === 'all') {
return true;
}
//else, if the user has provided a closeGuardAction, only run it if it matches the currentCloseAction
// If no closeGuardAction is configured, or it is set to 'all', always run all beforeClose guards.
// This keeps the behavior consistent for users who were already using guards before closeGuardAction was introduced.
if (!closeGuardAction || closeGuardAction === 'all') {
return true;
}
// Otherwise, only run guards when the configured closeGuardAction matches the action that triggered close().

Copilot uses AI. Check for mistakes.
closeGuardAction: 'escape',
});

// guard will fire on esdc keypress
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the comment: "esdc" should be "esc".

Suggested change
// guard will fire on esdc keypress
// guard will fire on esc keypress

Copilot uses AI. Check for mistakes.
imports: [CommonModule],
template: `
<div style="padding: 10px">
<p>Are you sure you want to close this dialog ?</p>
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor punctuation/spacing: there is an extra space before the question mark in the dialog text.

Suggested change
<p>Are you sure you want to close this dialog ?</p>
<p>Are you sure you want to close this dialog?</p>

Copilot uses AI. Check for mistakes.
package.json Outdated
Comment on lines +20 to +59
"dependencies": {
"@angular/animations": "^17.0.4",
"@angular/common": "^17.0.4",
"@angular/compiler": "^17.0.4",
"@angular/core": "^17.0.4",
"@angular/forms": "^17.0.4",
"@angular/platform-browser": "^17.0.4",
"@angular/platform-browser-dynamic": "^17.0.4",
"@angular/router": "^17.0.4",
"@angular/animations": "21.1.2",
"@angular/common": "^21.1.2",
"@angular/compiler": "21.1.2",
"@angular/core": "21.1.2",
"@angular/forms": "21.1.2",
"@angular/platform-browser": "21.1.2",
"@angular/platform-browser-dynamic": "21.1.2",
"@angular/router": "21.1.2",
"@ngneat/overview": "6.0.0",
"rxjs": "~7.5.0",
"tslib": "2.3.1",
"zone.js": "~0.14.2"
"zone.js": "0.16.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.3",
"@angular/cli": "^17.0.3",
"@angular/compiler-cli": "^17.0.4",
"@angular-devkit/core": "16.2.10",
"@angular-devkit/schematics": "16.2.10",
"@angular-eslint/eslint-plugin": "^16.3.1",
"@angular-eslint/eslint-plugin-template": "^16.3.1",
"@angular-eslint/template-parser": "^16.3.1",
"@angular-devkit/build-angular": "^21.1.2",
"@angular-devkit/core": "^21.1.2",
"@angular-devkit/schematics": "^21.1.2",
"@angular-eslint/eslint-plugin": "21.2.0",
"@angular-eslint/eslint-plugin-template": "21.2.0",
"@angular-eslint/template-parser": "21.2.0",
"@angular/cli": "^21.1.2",
"@angular/compiler-cli": "21.1.2",
"@commitlint/cli": "18.4.1",
"@commitlint/config-angular": "18.4.0",
"@commitlint/config-conventional": "18.4.0",
"@jscutlery/semver": "4.0.0",
"@jscutlery/semver": "5.7.1",
"@ngneat/spectator": "15.0.1",
"@nx/angular": "17.1.3",
"@nx/eslint-plugin": "17.1.3",
"@nx/js": "17.1.3",
"@nx/linter": "17.1.3",
"@nx/workspace": "17.1.3",
"@schematics/angular": "^15.2.7",
"@swc-node/register": "~1.6.7",
"@swc/core": "~1.3.85",
"@nx/angular": "22.4.5",
"@nx/eslint-plugin": "22.4.5",
"@nx/js": "22.4.5",
"@nx/workspace": "22.4.5",
"@schematics/angular": "^21.1.2",
"@swc-node/register": "^1.8.0",
"@swc/core": "^1.15.0",
"@types/jasmine": "4.3.0",
"@types/node": "18.17.18",
"@types/node": "18.19.99",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"angular-cli-ghpages": "1.0.7",
"angular-cli-ghpages": "3.0.2",
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title/description describe a dialog close-guard feature, but this PR also includes a major Angular/Nx/tooling upgrade (e.g., Angular 17 -> 21, Nx 17 -> 22) and migration-related changes. This is a significant scope increase; please either split the dependency/tooling upgrade into a separate PR or update the PR description to explicitly call out the upgrade and its impact.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 10
{
"migrations": [
{
"version": "16.0.0",
"factory": "./update-16/remove-default-project-option",
"description": "Remove 'defaultProject' option from workspace configuration. The project to use will be determined from the current working directory.",
"package": "@angular/cli",
"name": "remove-default-project-option"
"cli": "nx",
"version": "19.2.0-beta.2",
"description": "Updates the default workspace data directory to .nx/workspace-data",
"implementation": "./src/migrations/update-19-2-0/move-workspace-data-directory",
"package": "nx",
"name": "19-2-0-move-graph-cache-directory"
},
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was replaced with a large set of Nx migration entries, which appears unrelated to the stated feature in the PR title/description. Please confirm whether the dependency/tooling upgrade is intentional for this PR; otherwise, consider moving these migration changes to a dedicated upgrade PR to reduce review/merge risk.

Copilot uses AI. Check for mistakes.
type Sizes = 'sm' | 'md' | 'lg' | 'fullScreen' | string;
export type DragConstraint = 'none' | 'bounce' | 'constrain';
export type CloseStrategy = boolean | 'onlyLastStrategy';
export type CloseGuardAction = 'escape' | 'backdrop' | 'closeButton' | 'all';
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CloseGuardAction is used both as a config value (which includes the special value 'all') and as a concrete close-origin passed to close(). That conflates two concepts and allows passing 'all' as an actual close action, which doesn't represent a real trigger. Consider introducing two separate types (e.g., CloseAction = 'escape' | 'backdrop' | 'closeButton' and CloseGuardAction = CloseAction | 'all') and use CloseAction for the close(..., action) parameter.

Suggested change
export type CloseGuardAction = 'escape' | 'backdrop' | 'closeButton' | 'all';
export type CloseAction = 'escape' | 'backdrop' | 'closeButton';
export type CloseGuardAction = CloseAction | 'all';

Copilot uses AI. Check for mistakes.
@yonathan-utila yonathan-utila merged commit ecddd8a into master Feb 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants