UNPKG

@progress/kendo-angular-dialog

Version:
617 lines (608 loc) 25 kB
/**----------------------------------------------------------------------------------------- * 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" @overlayAppear></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" @overlayAppear></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'] }] } });