@progress/kendo-angular-dialog
Version:
Dialog Package for Angular
617 lines (608 loc) • 25 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, EventEmitter, HostBinding, Input, ViewChild, Output, ElementRef, Renderer2, ChangeDetectorRef, NgZone, ContentChildren, QueryList, ViewChildren } from '@angular/core';
import { NgStyle, NgIf, NgTemplateOutlet } from '@angular/common';
import { animate, AnimationBuilder, state, style, transition, trigger } from '@angular/animations';
import { DialogActionsComponent } from './dialog-actions.component';
import { DialogTitleBarComponent } from './dialog-titlebar.component';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { hasClasses, Keys, isPresent, isFocusable, DIALOG_ELEMENTS_HANDLING_ARROWS, DIALOG_ELEMENTS_HANDLING_ESC_KEY, createValueWithUnit, parseCSSClassNames, findPrimaryButton } from '../common/util';
import { focusableSelector, shouldShowValidationUI, setHTMLAttributes } from '@progress/kendo-angular-common';
import { DialogCloseResult } from './models/dialog-close-result';
import { DIALOG_LOCALIZATION_SERVICE } from './../localization/dialog-localization.service';
import { take } from 'rxjs/operators';
import { Subscription } from 'rxjs';
import { animateContent } from './dialog-animations/animate-content';
import { isDocumentAvailable, WatermarkOverlayComponent } from '@progress/kendo-angular-common';
import { LocalizedMessagesDirective } from '../localization/localized-messages.directive';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "@angular/animations";
const DEFAULT_ANIMATION_CONFIG = { duration: 300, type: 'translate' };
/**
* Represents the [Kendo UI Dialog component for Angular]({% slug overview_dialog_dialogs %}).
*
* Use this component to display modal dialog windows in your application.
*
* @example
* ```ts
* import { Component } from '@angular/core';
*
* @Component({
* selector: 'my-app',
* template: `
* <kendo-dialog title="Example Dialog">
* <p>Dialog content goes here.</p>
* </kendo-dialog>
* `
* })
* export class AppComponent {}
* ```
*
* @remarks
* Supported children components are: {@link DialogTitleBarComponent}, {@link DialogActionsComponent}, {@link CustomMessagesComponent}.
*/
export class DialogComponent {
wrapper;
renderer;
cdr;
ngZone;
builder;
/**
* Specifies the action buttons to render in the Dialog.
*
* @type {DialogAction[]}
*/
actions;
/**
* Sets the layout of the action buttons in the Dialog. Applies only if you specify action buttons through the `actions` option.
*
* @type {ActionsLayout}
* @default 'stretched'
*/
actionsLayout = 'stretched';
/**
* Sets the query selector for the element to receive initial focus. ([See examples.]({% slug initial_focus_dialog %}))
*
* @type {string}
*/
autoFocusedElement;
/**
* Sets the text in the Dialog title bar.
*
* @type {string}
*/
title;
/**
* Sets the width of the Dialog. Use a number for pixels or a string for units (for example, `50%`).
*
* @type {number | string}
*/
width;
/**
* Sets the minimum width of the Dialog. Use a number for pixels or a string for units (for example, `50%`).
*
* @type {number | string}
*/
minWidth;
/**
* Sets the maximum width of the Dialog. Use a number for pixels or a string for units (for example, `50%`).
*
* @type {number | string}
*/
maxWidth;
/**
* Sets the height of the Dialog. Use a number for pixels or a string for units (for example, `50%`).
*
* @type {number | string}
*/
height;
/**
* Sets the minimum height of the Dialog. Use a number for pixels or a string for units (for example, `50%`).
*
* @type {number | string}
*/
minHeight;
/**
* Sets the maximum height of the Dialog. Use a number for pixels or a string for units (for example, `50%`).
*
* @type {number | string}
*/
maxHeight;
/**
* Configures the Dialog opening animation ([see example]({% slug animations_dialog %})).
* The default animation type is `translate` and the duration is `300ms`.
*
* @type {boolean | DialogAnimation}
* @default true
*/
animation = true;
/**
* Sets a predefined theme color for the Dialog. The color applies to the title bar background and border, and updates the text color.
*
* @type {DialogThemeColor}
*/
set themeColor(themeColor) {
this.handleThemeColorClass(this.themeColor, themeColor);
this._themeColor = themeColor;
}
get themeColor() {
return this._themeColor;
}
/**
* @hidden
*/
set htmlAttributes(attributes) {
setHTMLAttributes(attributes, this.renderer, this.wrapper.nativeElement);
const el = this.wrapper.nativeElement;
const dir = el.getAttribute('dir');
const tIndex = el.getAttribute('tabindex');
if (this.direction !== dir && dir) {
this.direction = dir;
}
if (this.tabIndex !== tIndex && tIndex) {
this.tabIndex = tIndex;
}
this._htmlAttributes = attributes;
}
get htmlAttributes() {
return this._htmlAttributes;
}
/**
* @hidden
*/
set cssClass(classes) {
this.setServiceClasses(this._cssClass, classes);
this._cssClass = classes;
}
get cssClass() {
return this._cssClass;
}
/**
* @hidden
*/
contentTemplate;
/**
* @hidden
*/
titleId = null;
/**
* @hidden
*/
contentId = null;
/**
* @hidden
*/
closeTitle;
/**
* @hidden
*/
showLicenseWatermark = false;
/**
* Emits when the user clicks an action button in the Dialog. Fires only if you specify action buttons through the `actions` option.
*
* @type {EventEmitter<DialogAction>}
*/
action = new EventEmitter();
/**
* Emits when the user clicks the **Close** button or presses the `ESC` key.
*
* @type {EventEmitter<any>}
*/
close = new EventEmitter();
get dir() {
return this.direction;
}
tabIndex = 0;
titlebarContent;
titlebarView;
actionsView;
dialog;
_htmlAttributes;
_cssClass;
_themeColor = null;
direction;
subscriptions = [];
domSubs = new Subscription();
constructor(wrapper, renderer, localization, cdr, ngZone, builder) {
this.wrapper = wrapper;
this.renderer = renderer;
this.cdr = cdr;
this.ngZone = ngZone;
this.builder = builder;
const isValid = validatePackage(packageMetadata);
this.showLicenseWatermark = shouldShowValidationUI(isValid);
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.subscriptions.push(localization.changes.subscribe(({ rtl }) => (this.direction = rtl ? 'rtl' : 'ltr')));
this.titleId = this.generateTitleId();
this.contentId = this.generateContentId();
}
ngAfterContentInit() {
this.bubble('close', this.titlebarContent.first);
this.renderer.setAttribute(this.wrapper.nativeElement.querySelector('.k-dialog'), 'aria-describedby', this.contentId);
if (this.titlebarContent.first) {
this.titlebarContent.first.id = this.titleId;
}
else {
this.subscriptions.push(this.titlebarContent.changes.subscribe(() => {
if (isPresent(this.titlebarContent.first)) {
this.titlebarContent.first.id = this.titleId;
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.bubble('close', this.titlebarContent.first);
this.renderer.setAttribute(this.wrapper.nativeElement.querySelector('.k-dialog'), 'aria-labelledby', this.titleId);
});
}
}));
}
}
ngAfterViewInit() {
if (!isDocumentAvailable()) {
return;
}
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.handleInitialFocus();
});
this.bubble('close', this.titlebarView.first);
this.bubble('action', this.actionsView);
if (this.titlebarView.first || this.titlebarContent.first) {
//Needed for Dialogs created via service
this.renderer.setAttribute(this.wrapper.nativeElement.querySelector('.k-dialog'), 'aria-labelledby', this.titleId);
}
else {
this.subscriptions.push(this.titlebarView.changes.subscribe(() => {
if (isPresent(this.titlebarView.first)) {
this.titlebarView.first.id = this.titleId;
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.bubble('close', this.titlebarView.first);
this.renderer.setAttribute(this.wrapper.nativeElement.querySelector('.k-dialog'), 'aria-labelledby', this.titleId);
});
}
}));
}
this.initDomEvents();
this.handleThemeColorClass(null, this.themeColor);
}
ngOnInit() {
if (this.animation) {
animateContent(this.animation, DEFAULT_ANIMATION_CONFIG, this.dialog.nativeElement, this.builder);
}
this.renderer.removeAttribute(this.wrapper.nativeElement, 'title');
this.cdr.detectChanges();
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
this.subscriptions = [];
if (this.domSubs) {
this.domSubs.unsubscribe();
}
}
/**
* Focuses the wrapper of the Dialog component.
*/
focus() {
const wrapper = this.wrapper.nativeElement;
if (isPresent(wrapper)) {
wrapper.focus();
}
}
initDomEvents() {
if (!this.wrapper) {
return;
}
this.ngZone.runOutsideAngular(() => {
this.domSubs.add(this.renderer.listen(this.wrapper.nativeElement, 'keydown', (ev) => {
this.onKeyDown(ev);
}));
});
}
onKeyDown(event) {
const target = event.target;
const parent = target.parentElement;
if (hasClasses(target, DIALOG_ELEMENTS_HANDLING_ESC_KEY) || hasClasses(parent, DIALOG_ELEMENTS_HANDLING_ESC_KEY)) {
if (event.keyCode === Keys.esc) {
this.ngZone.run(() => {
this.close.emit(new DialogCloseResult());
});
}
}
if (hasClasses(target, 'k-button') && hasClasses(parent, DIALOG_ELEMENTS_HANDLING_ARROWS) &&
(event.keyCode === Keys.left || event.keyCode === Keys.right)) {
this.ngZone.run(() => {
this.handleActionButtonFocus(parent, event.keyCode);
});
}
if (event.keyCode === Keys.tab) {
this.ngZone.run(() => {
this.keepFocusWithinComponent(target, event);
});
}
}
setServiceClasses(prevValue, value) {
const el = this.wrapper.nativeElement;
if (prevValue) {
parseCSSClassNames(prevValue).forEach(className => {
this.renderer.removeClass(el, className);
});
}
if (value) {
parseCSSClassNames(value).forEach(className => {
this.renderer.addClass(el, className);
});
}
}
/**
* @hidden
*/
handleInitialFocus() {
const wrapper = this.wrapper.nativeElement;
const primaryButton = this.findPrimary(wrapper);
if (this.autoFocusedElement) {
const initiallyFocusedElement = wrapper.querySelector(this.autoFocusedElement);
if (initiallyFocusedElement) {
initiallyFocusedElement.focus();
}
}
else if (this.shouldFocusPrimary(primaryButton)) {
primaryButton.focus();
}
else {
wrapper.focus();
}
}
/**
* @hidden
*/
findPrimary(wrapper) {
const actionBtns = wrapper.querySelectorAll('.k-actions .k-button');
return findPrimaryButton(actionBtns);
}
/**
* @hidden
*/
handleActionButtonFocus(parent, key) {
const focusableActionButtons = this.getAllFocusableChildren(parent);
for (let i = 0; i < focusableActionButtons.length; i++) {
const current = focusableActionButtons[i];
if (current === document.activeElement) {
if (key === Keys.left && i > 0) {
focusableActionButtons[i - 1].focus();
break;
}
if (key === Keys.right && i < focusableActionButtons.length - 1) {
focusableActionButtons[i + 1].focus();
break;
}
}
}
}
/**
* @hidden
*/
keepFocusWithinComponent(target, event) {
const wrapper = this.wrapper.nativeElement;
const [firstFocusable, lastFocusable] = this.getFirstAndLastFocusable(wrapper);
const tabAfterLastFocusable = !event.shiftKey && target === lastFocusable;
const shiftTabAfterFirstFocusable = event.shiftKey && target === firstFocusable;
if (tabAfterLastFocusable) {
event.preventDefault();
firstFocusable.focus();
}
if (shiftTabAfterFirstFocusable) {
event.preventDefault();
lastFocusable.focus();
}
}
/**
* @hidden
*/
shouldFocusPrimary(el) {
return isPresent(el) && isFocusable(el);
}
/**
* @hidden
*/
getAllFocusableChildren(parent) {
return parent.querySelectorAll(focusableSelector);
}
/**
* @hidden
*/
getFirstAndLastFocusable(parent) {
const all = this.getAllFocusableChildren(parent);
const firstFocusable = all.length > 0 ? all[0] : parent;
const lastFocusable = all.length > 0 ? all[all.length - 1] : parent;
return [firstFocusable, lastFocusable];
}
/**
* @hidden
*/
generateTitleId() {
return 'kendo-dialog-title-' + Math.ceil(Math.random() * 1000000).toString();
}
/**
* @hidden
*/
generateContentId() {
return 'kendo-dialog-content-' + Math.ceil(Math.random() * 1000000).toString();
}
get wrapperClass() {
return true;
}
get styles() {
const styles = {};
if (this.width) {
styles.width = createValueWithUnit(this.width);
}
if (this.height) {
styles.height = createValueWithUnit(this.height);
}
if (this.minWidth) {
styles.minWidth = createValueWithUnit(this.minWidth);
}
if (this.maxWidth) {
styles.maxWidth = createValueWithUnit(this.maxWidth);
}
if (this.minHeight) {
styles.minHeight = createValueWithUnit(this.minHeight);
}
if (this.maxHeight) {
styles.maxHeight = createValueWithUnit(this.maxHeight);
}
return styles;
}
bubble(eventName, component) {
if (component) {
const emit = e => this[eventName].emit(e);
const s = component[eventName].subscribe(emit);
this.subscriptions.push(s);
}
}
handleThemeColorClass(previousValue, currentValue) {
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
const dialog = this.dialog.nativeElement;
if (previousValue) {
const classToRemove = `k-dialog-${previousValue}`;
this.renderer.removeClass(dialog, classToRemove);
}
if (currentValue) {
const classToAdd = `k-dialog-${currentValue}`;
this.renderer.addClass(dialog, classToAdd);
}
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DialogComponent, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i1.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i2.AnimationBuilder }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: DialogComponent, isStandalone: true, selector: "kendo-dialog", inputs: { actions: "actions", actionsLayout: "actionsLayout", autoFocusedElement: "autoFocusedElement", title: "title", width: "width", minWidth: "minWidth", maxWidth: "maxWidth", height: "height", minHeight: "minHeight", maxHeight: "maxHeight", animation: "animation", themeColor: "themeColor" }, outputs: { action: "action", close: "close" }, host: { properties: { "attr.dir": "this.dir", "attr.tabIndex": "this.tabIndex", "class.k-dialog-wrapper": "this.wrapperClass" } }, providers: [
LocalizationService,
{
provide: DIALOG_LOCALIZATION_SERVICE,
useExisting: LocalizationService
},
{
provide: L10N_PREFIX,
useValue: 'kendo.dialog'
}
], queries: [{ propertyName: "titlebarContent", predicate: DialogTitleBarComponent }], viewQueries: [{ propertyName: "actionsView", first: true, predicate: DialogActionsComponent, descendants: true }, { propertyName: "dialog", first: true, predicate: ["dialog"], descendants: true, static: true }, { propertyName: "titlebarView", predicate: DialogTitleBarComponent, descendants: true }], exportAs: ["kendoDialog"], ngImport: i0, template: `
<ng-container
kendoDialogLocalizedMessages
i18n-closeTitle="kendo.dialog.closeTitle|The title of the close button"
closeTitle="Close"
>
<div class="k-overlay" ></div>
<div #dialog class="k-window k-dialog" role="dialog" aria-modal="true" [ngStyle]="styles">
<kendo-dialog-titlebar *ngIf="title" [closeTitle]="closeTitle" [id]="titleId">{{ title }}</kendo-dialog-titlebar>
<ng-content select="kendo-dialog-titlebar" *ngIf="!title"></ng-content>
<div [id]="contentId" class="k-window-content k-dialog-content">
<ng-content *ngIf="!contentTemplate"></ng-content>
<ng-template [ngTemplateOutlet]="contentTemplate" *ngIf="contentTemplate"></ng-template>
</div>
<ng-content select="kendo-dialog-actions" *ngIf="!actions"></ng-content>
<kendo-dialog-actions *ngIf="actions" [actions]="actions" [layout]="actionsLayout"> </kendo-dialog-actions>
<div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div>
</div>
</ng-container>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "\n [kendoDialogLocalizedMessages],\n [kendoWindowLocalizedMessages],\n [kendoDialogTitleBarLocalizedMessages]\n " }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: DialogTitleBarComponent, selector: "kendo-dialog-titlebar", inputs: ["id", "closeTitle"], outputs: ["close"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: DialogActionsComponent, selector: "kendo-dialog-actions", inputs: ["actions", "layout"], outputs: ["action"] }, { kind: "component", type: WatermarkOverlayComponent, selector: "div[kendoWatermarkOverlay]" }], animations: [
trigger('overlayAppear', [
state('in', style({ opacity: 1 })),
transition('void => *', [style({ opacity: 0.1 }), animate('.3s cubic-bezier(.2, .6, .4, 1)')])
])
] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DialogComponent, decorators: [{
type: Component,
args: [{
animations: [
trigger('overlayAppear', [
state('in', style({ opacity: 1 })),
transition('void => *', [style({ opacity: 0.1 }), animate('.3s cubic-bezier(.2, .6, .4, 1)')])
])
],
exportAs: 'kendoDialog',
providers: [
LocalizationService,
{
provide: DIALOG_LOCALIZATION_SERVICE,
useExisting: LocalizationService
},
{
provide: L10N_PREFIX,
useValue: 'kendo.dialog'
}
],
selector: 'kendo-dialog',
template: `
<ng-container
kendoDialogLocalizedMessages
i18n-closeTitle="kendo.dialog.closeTitle|The title of the close button"
closeTitle="Close"
>
<div class="k-overlay" ></div>
<div #dialog class="k-window k-dialog" role="dialog" aria-modal="true" [ngStyle]="styles">
<kendo-dialog-titlebar *ngIf="title" [closeTitle]="closeTitle" [id]="titleId">{{ title }}</kendo-dialog-titlebar>
<ng-content select="kendo-dialog-titlebar" *ngIf="!title"></ng-content>
<div [id]="contentId" class="k-window-content k-dialog-content">
<ng-content *ngIf="!contentTemplate"></ng-content>
<ng-template [ngTemplateOutlet]="contentTemplate" *ngIf="contentTemplate"></ng-template>
</div>
<ng-content select="kendo-dialog-actions" *ngIf="!actions"></ng-content>
<kendo-dialog-actions *ngIf="actions" [actions]="actions" [layout]="actionsLayout"> </kendo-dialog-actions>
<div kendoWatermarkOverlay *ngIf="showLicenseWatermark"></div>
</div>
</ng-container>
`,
standalone: true,
imports: [LocalizedMessagesDirective, NgStyle, NgIf, DialogTitleBarComponent, NgTemplateOutlet, DialogActionsComponent, WatermarkOverlayComponent]
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i2.AnimationBuilder }]; }, propDecorators: { actions: [{
type: Input
}], actionsLayout: [{
type: Input
}], autoFocusedElement: [{
type: Input
}], title: [{
type: Input
}], width: [{
type: Input
}], minWidth: [{
type: Input
}], maxWidth: [{
type: Input
}], height: [{
type: Input
}], minHeight: [{
type: Input
}], maxHeight: [{
type: Input
}], animation: [{
type: Input
}], themeColor: [{
type: Input
}], action: [{
type: Output
}], close: [{
type: Output
}], dir: [{
type: HostBinding,
args: ['attr.dir']
}], tabIndex: [{
type: HostBinding,
args: ['attr.tabIndex']
}], titlebarContent: [{
type: ContentChildren,
args: [DialogTitleBarComponent, { descendants: false }]
}], titlebarView: [{
type: ViewChildren,
args: [DialogTitleBarComponent]
}], actionsView: [{
type: ViewChild,
args: [DialogActionsComponent, { static: false }]
}], dialog: [{
type: ViewChild,
args: ['dialog', { static: true }]
}], wrapperClass: [{
type: HostBinding,
args: ['class.k-dialog-wrapper']
}] } });