UNPKG

@progress/kendo-angular-navigation

Version:

Kendo UI Navigation for Angular

665 lines (650 loc) 30.7 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, EventEmitter, HostBinding, Input, NgZone, Output, Renderer2, ViewChild, QueryList, forwardRef } from '@angular/core'; import { NgIf, NgClass, NgTemplateOutlet, NgFor, NgStyle } from '@angular/common'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { Subscription } from 'rxjs'; import { ActionSheetHeaderTemplateDirective, ActionSheetItemTemplateDirective, ActionSheetContentTemplateDirective, ActionSheetFooterTemplateDirective, ActionSheetTemplateDirective } from './models'; import { isDocumentAvailable, isPresent, Keys } from '@progress/kendo-angular-common'; import { getId, getActionSheetItemIndex, getFirstAndLastFocusable, ACTIONSHEET_ITEM_INDEX_ATTRIBUTE } from '../common/util'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { AnimationBuilder } from '@angular/animations'; import { slideDown, slideUp } from './animation/animations'; import { take } from 'rxjs/operators'; import { ActionSheetListComponent } from './list.component'; import { ButtonDirective } from '@progress/kendo-angular-buttons'; import { ActionSheetViewComponent } from './actionsheet-view.component'; 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 }; /** * Represents the [Kendo UI ActionSheet component for Angular](slug:overview_actionsheet). * Use this component to display a set of choices related to a user-initiated task in a modal sheet that slides up from the bottom of the screen. * * @example * ```html * <kendo-actionsheet [items]="actionItems" [expanded]="true"> * </kendo-actionsheet> * ``` */ export class ActionSheetComponent { element; ngZone; renderer; localizationService; builder; cdr; /** * @hidden */ currentView = 1; /** * @hidden */ get hostClass() { return this.expanded; } /** * @hidden */ direction; /** * Specifies the action buttons displayed in the ActionSheet footer. */ actions; /** * Configures the layout of the action buttons in the footer. By default, actions are arranged horizontally and stretched to fill the container. */ actionsLayout = { orientation: 'horizontal', alignment: 'stretched' }; /** * Determines whether the ActionSheet closes when the overlay is clicked. * * @default false */ overlayClickClose = false; /** * Sets the title text displayed in the ActionSheet header. */ title; /** * Sets the subtitle text displayed below the title in the header. */ subtitle; /** * Provides the collection of items rendered in the ActionSheet content area. */ items; /** * Applies CSS classes to the inner ActionSheet element. Accepts any value supported by [`ngClass`](link:site.data.urls.angular['ngclassapi']). */ cssClass; /** * Applies inline styles to the inner ActionSheet element. Accepts any value supported by [`ngStyle`](link:site.data.urls.angular['ngstyleapi']). */ cssStyle; /** * Configures the opening and closing animations for the ActionSheet ([see example](slug:animations_actionsheet)). * * @default true */ animation = true; /** * Controls whether the ActionSheet is expanded or collapsed. * * @default false */ expanded = false; /** * Sets the `aria-labelledby` attribute of the ActionSheet wrapper element. * Use this option when the built-in header element is replaced through the [`ActionSheetHeaderTemplate`](slug:api_navigation_actionsheetheadertemplatedirective) * or [`ActionSheetContentTemplate`](slug:api_navigation_actionsheetcontenttemplatedirective). */ titleId = getId('k-actionsheet-title'); /** * @hidden * * Determines if the ActionSheet should focus the first focusable element when opened. */ initialFocus = true; /** * Fires when the `expanded` property of the component is updated. * You can use this event to provide two-way binding for the `expanded` property. */ expandedChange = new EventEmitter(); /** * Fires when any of the ActionSheet action buttons is clicked. */ action = new EventEmitter(); /** * Fires when the ActionSheet is expanded and its animation is complete. */ expand = new EventEmitter(); /** * Fires when the ActionSheet is collapsed and its animation is complete. */ collapse = new EventEmitter(); /** * Fires when an ActionSheet item is clicked. */ itemClick = new EventEmitter(); /** * Fires when the modal overlay is clicked. */ overlayClick = new EventEmitter(); /** * @hidden */ childContainer; /** * @hidden */ actionSheetViews; /** * @hidden */ actionSheetTemplate; /** * @hidden */ headerTemplate; /** * @hidden */ contentTemplate; /** * @hidden */ itemTemplate; /** * @hidden */ footerTemplate; dynamicRTLSubscription; rtl = false; domSubs = new Subscription(); player; animationEnd = new EventEmitter(); constructor(element, ngZone, renderer, localizationService, builder, cdr) { this.element = element; this.ngZone = ngZone; this.renderer = renderer; this.localizationService = localizationService; this.builder = builder; this.cdr = cdr; validatePackage(packageMetadata); this.dynamicRTLSubscription = this.localizationService.changes.subscribe(({ rtl }) => { this.rtl = rtl; this.direction = this.rtl ? 'rtl' : 'ltr'; }); } ngAfterViewInit() { this.initDomEvents(); this.setCssVariables(); } ngOnChanges(changes) { if (changes['expanded'] && this.expanded) { this.setExpanded(true); } } ngOnDestroy() { this.domSubs.unsubscribe(); if (this.dynamicRTLSubscription) { this.dynamicRTLSubscription.unsubscribe(); } if (this.player) { this.player.destroy(); } } /** * @hidden * Navigates to the next view. */ nextView() { if (this.currentView < this.actionSheetViews.length) { this.currentView += 1; } } /** * @hidden * Navigates to the previous view. */ prevView() { if (this.currentView > 1) { this.currentView -= 1; } } /** * Toggles the visibility of the ActionSheet. * * @param expanded? - Boolean. Specifies if the ActionSheet will be expanded or collapsed. */ toggle(expanded) { const previous = this.expanded; const current = isPresent(expanded) ? expanded : !previous; if (current === previous) { return; } if (current === true) { this.setExpanded(true); } else if (current === false && !this.animation) { this.setExpanded(false); } if (this.animation) { this.animationEnd.pipe(take(1)) .subscribe(() => { this.onAnimationEnd(current); }); this.playAnimation(current); } else { this[current ? 'expand' : 'collapse'].emit(); } } /** * @hidden */ get orientationClass() { return this.actionsLayout.orientation ? `k-actions-${this.actionsLayout.orientation}` : ''; } /** * @hidden */ get alignmentClass() { return this.actionsLayout.alignment ? `k-actions-${this.actionsLayout.alignment}` : ''; } /** * @hidden */ get topGroupItems() { return this.items?.filter(item => !item.group || item.group === 'top'); } /** * @hidden */ get bottomGroupItems() { return this.items?.filter(item => item.group === 'bottom'); } /** * @hidden */ onItemClick(ev) { this.itemClick.emit(ev); } /** * @hidden */ onOverlayClick() { this.overlayClick.emit(); if (this.overlayClickClose) { this.toggle(false); } } /** * @hidden */ get shouldRenderSeparator() { return this.topGroupItems?.length > 0 && this.bottomGroupItems?.length > 0; } initDomEvents() { if (!this.element) { return; } this.ngZone.runOutsideAngular(() => { this.domSubs.add(this.renderer.listen(this.element.nativeElement, 'keydown', (ev) => { this.onKeyDown(ev); })); }); } setCssVariables() { if (!this.element || !isDocumentAvailable()) { return; } // The following syntax is used as it does not violate CSP compliance this.element.nativeElement.style.setProperty('--kendo-actionsheet-height', 'auto'); this.element.nativeElement.style.setProperty('--kendo-actionsheet-max-height', 'none'); } onKeyDown(event) { const target = event.target; if (event.code === Keys.Tab) { this.ngZone.run(() => { this.keepFocusWithinComponent(target, event); }); } if (event.code === Keys.Escape) { this.ngZone.run(() => { this.overlayClick.emit(); }); } if (event.code === Keys.Enter || event.code === Keys.NumpadEnter) { this.ngZone.run(() => { this.triggerItemClick(target, event); }); } } handleInitialFocus() { const [firstFocusable] = getFirstAndLastFocusable(this.element.nativeElement); if (firstFocusable && this.initialFocus) { firstFocusable.focus(); } } keepFocusWithinComponent(target, event) { const wrapper = this.element.nativeElement; const [firstFocusable, lastFocusable] = 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(); } } triggerItemClick(target, event) { const itemIndex = getActionSheetItemIndex(target, ACTIONSHEET_ITEM_INDEX_ATTRIBUTE, this.element.nativeElement); const item = isPresent(itemIndex) ? this.items[itemIndex] : null; if (!item || item.disabled) { return; } this.itemClick.emit({ item, originalEvent: event }); } setExpanded(value) { this.expanded = value; this.expandedChange.emit(value); if (this.expanded) { this.cdr.detectChanges(); this.handleInitialFocus(); } } onAnimationEnd(currentExpanded) { if (currentExpanded) { this.expand.emit(); } else { this.setExpanded(false); this.collapse.emit(); } } playAnimation(expanded) { const duration = typeof this.animation !== 'boolean' && this.animation.duration ? this.animation.duration : DEFAULT_ANIMATION_CONFIG.duration; const contentHeight = getComputedStyle(this.childContainer.nativeElement).height; const animation = expanded ? slideUp(duration, contentHeight) : slideDown(duration, contentHeight); const factory = this.builder.build(animation); this.player = factory.create(this.childContainer.nativeElement); this.player.onDone(() => { if (this.player) { this.animationEnd.emit(); this.player.destroy(); this.player = null; } }); this.player.play(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ActionSheetComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i1.LocalizationService }, { token: i2.AnimationBuilder }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ActionSheetComponent, isStandalone: true, selector: "kendo-actionsheet", inputs: { actions: "actions", actionsLayout: "actionsLayout", overlayClickClose: "overlayClickClose", title: "title", subtitle: "subtitle", items: "items", cssClass: "cssClass", cssStyle: "cssStyle", animation: "animation", expanded: "expanded", titleId: "titleId", initialFocus: "initialFocus" }, outputs: { expandedChange: "expandedChange", action: "action", expand: "expand", collapse: "collapse", itemClick: "itemClick", overlayClick: "overlayClick" }, host: { properties: { "class.k-actionsheet-container": "this.hostClass", "attr.dir": "this.direction" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.actionsheet.component' } ], queries: [{ propertyName: "actionSheetTemplate", first: true, predicate: ActionSheetTemplateDirective, descendants: true }, { propertyName: "headerTemplate", first: true, predicate: ActionSheetHeaderTemplateDirective, descendants: true }, { propertyName: "contentTemplate", first: true, predicate: ActionSheetContentTemplateDirective, descendants: true }, { propertyName: "itemTemplate", first: true, predicate: ActionSheetItemTemplateDirective, descendants: true }, { propertyName: "footerTemplate", first: true, predicate: ActionSheetFooterTemplateDirective, descendants: true }, { propertyName: "actionSheetViews", predicate: i0.forwardRef(function () { return ActionSheetViewComponent; }) }], viewQueries: [{ propertyName: "childContainer", first: true, predicate: ["childContainer"], descendants: true }], exportAs: ["kendoActionSheet"], usesOnChanges: true, ngImport: i0, template: ` <ng-container *ngIf="expanded"> <div class="k-overlay" (click)="onOverlayClick()"></div> <div class="k-animation-container k-animation-container-shown"> <div #childContainer class="k-child-animation-container" [style]="'bottom: 0px; width: 100%;'"> <div class="k-actionsheet k-actionsheet-bottom" [ngClass]="cssClass" [ngStyle]="cssStyle" role="dialog" aria-modal="true" [attr.aria-labelledby]="titleId" [style.--kendo-actionsheet-view-current]="actionSheetViews?.length > 0 ? currentView : null" > <ng-content *ngIf="actionSheetViews?.length > 0" select="kendo-actionsheet-view"></ng-content> <div *ngIf="actionSheetViews?.length === 0" class="k-actionsheet-view"> <ng-template *ngIf="actionSheetTemplate; else defaultTemplate" [ngTemplateOutlet]="actionSheetTemplate?.templateRef"> </ng-template> <ng-template #defaultTemplate> <div *ngIf="title || subtitle || headerTemplate" class="k-actionsheet-titlebar"> <ng-template *ngIf="headerTemplate; else defaultHeaderTemplate" [ngTemplateOutlet]="headerTemplate?.templateRef"> </ng-template> <ng-template #defaultHeaderTemplate> <div class="k-actionsheet-titlebar-group k-hbox"> <div class="k-actionsheet-title" [id]="titleId"> <div *ngIf="title" class="k-text-center">{{title}}</div> <div *ngIf="subtitle" class="k-actionsheet-subtitle k-text-center">{{subtitle}}</div> </div> </div> </ng-template> </div> <div *ngIf="items || contentTemplate" class="k-actionsheet-content"> <ng-template *ngIf="contentTemplate; else defaultContentTemplate" [ngTemplateOutlet]="contentTemplate?.templateRef"> </ng-template> <ng-template #defaultContentTemplate> <div *ngIf="topGroupItems" kendoActionSheetList class="k-list-ul" role="group" [groupItems]="topGroupItems" [allItems]="items" [itemTemplate]="itemTemplate?.templateRef" (itemClick)="onItemClick($event)"> </div> <hr *ngIf="shouldRenderSeparator" class="k-hr"/> <div *ngIf="bottomGroupItems" kendoActionSheetList class="k-list-ul" role="group" [groupItems]="bottomGroupItems" [allItems]="items" [itemTemplate]="itemTemplate?.templateRef" (itemClick)="onItemClick($event)"> </div> </ng-template> </div> <div *ngIf="footerTemplate || actions" [ngClass]="[orientationClass, alignmentClass, 'k-actions', 'k-actionsheet-footer']"> <ng-template *ngIf="footerTemplate" [ngTemplateOutlet]="footerTemplate?.templateRef"> </ng-template> <ng-container *ngIf="!footerTemplate && actions"> <button *ngFor="let actionButton of actions" kendoButton type="button" [icon]="actionButton.icon" [title]="actionButton.title" [svgIcon]="actionButton.svgIcon" [themeColor]="actionButton.themeColor" [fillMode]="actionButton.fillMode" [size]="actionButton.size" [attr.aria-label]="actionButton.text" (click)="action.emit(actionButton)" > {{ actionButton.text }} </button> </ng-container> </div> </ng-template> </div> </div> </div> </div> </ng-container> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ActionSheetListComponent, selector: "[kendoActionSheetList]", inputs: ["groupItems", "allItems", "itemTemplate"], outputs: ["itemClick"] }, { kind: "component", type: ButtonDirective, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ActionSheetComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendoActionSheet', selector: 'kendo-actionsheet', template: ` <ng-container *ngIf="expanded"> <div class="k-overlay" (click)="onOverlayClick()"></div> <div class="k-animation-container k-animation-container-shown"> <div #childContainer class="k-child-animation-container" [style]="'bottom: 0px; width: 100%;'"> <div class="k-actionsheet k-actionsheet-bottom" [ngClass]="cssClass" [ngStyle]="cssStyle" role="dialog" aria-modal="true" [attr.aria-labelledby]="titleId" [style.--kendo-actionsheet-view-current]="actionSheetViews?.length > 0 ? currentView : null" > <ng-content *ngIf="actionSheetViews?.length > 0" select="kendo-actionsheet-view"></ng-content> <div *ngIf="actionSheetViews?.length === 0" class="k-actionsheet-view"> <ng-template *ngIf="actionSheetTemplate; else defaultTemplate" [ngTemplateOutlet]="actionSheetTemplate?.templateRef"> </ng-template> <ng-template #defaultTemplate> <div *ngIf="title || subtitle || headerTemplate" class="k-actionsheet-titlebar"> <ng-template *ngIf="headerTemplate; else defaultHeaderTemplate" [ngTemplateOutlet]="headerTemplate?.templateRef"> </ng-template> <ng-template #defaultHeaderTemplate> <div class="k-actionsheet-titlebar-group k-hbox"> <div class="k-actionsheet-title" [id]="titleId"> <div *ngIf="title" class="k-text-center">{{title}}</div> <div *ngIf="subtitle" class="k-actionsheet-subtitle k-text-center">{{subtitle}}</div> </div> </div> </ng-template> </div> <div *ngIf="items || contentTemplate" class="k-actionsheet-content"> <ng-template *ngIf="contentTemplate; else defaultContentTemplate" [ngTemplateOutlet]="contentTemplate?.templateRef"> </ng-template> <ng-template #defaultContentTemplate> <div *ngIf="topGroupItems" kendoActionSheetList class="k-list-ul" role="group" [groupItems]="topGroupItems" [allItems]="items" [itemTemplate]="itemTemplate?.templateRef" (itemClick)="onItemClick($event)"> </div> <hr *ngIf="shouldRenderSeparator" class="k-hr"/> <div *ngIf="bottomGroupItems" kendoActionSheetList class="k-list-ul" role="group" [groupItems]="bottomGroupItems" [allItems]="items" [itemTemplate]="itemTemplate?.templateRef" (itemClick)="onItemClick($event)"> </div> </ng-template> </div> <div *ngIf="footerTemplate || actions" [ngClass]="[orientationClass, alignmentClass, 'k-actions', 'k-actionsheet-footer']"> <ng-template *ngIf="footerTemplate" [ngTemplateOutlet]="footerTemplate?.templateRef"> </ng-template> <ng-container *ngIf="!footerTemplate && actions"> <button *ngFor="let actionButton of actions" kendoButton type="button" [icon]="actionButton.icon" [title]="actionButton.title" [svgIcon]="actionButton.svgIcon" [themeColor]="actionButton.themeColor" [fillMode]="actionButton.fillMode" [size]="actionButton.size" [attr.aria-label]="actionButton.text" (click)="action.emit(actionButton)" > {{ actionButton.text }} </button> </ng-container> </div> </ng-template> </div> </div> </div> </div> </ng-container> `, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.actionsheet.component' } ], standalone: true, imports: [NgIf, NgFor, NgStyle, NgClass, NgTemplateOutlet, ActionSheetListComponent, ButtonDirective] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i1.LocalizationService }, { type: i2.AnimationBuilder }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { hostClass: [{ type: HostBinding, args: ['class.k-actionsheet-container'] }], direction: [{ type: HostBinding, args: ['attr.dir'] }], actions: [{ type: Input }], actionsLayout: [{ type: Input }], overlayClickClose: [{ type: Input }], title: [{ type: Input }], subtitle: [{ type: Input }], items: [{ type: Input }], cssClass: [{ type: Input }], cssStyle: [{ type: Input }], animation: [{ type: Input }], expanded: [{ type: Input }], titleId: [{ type: Input }], initialFocus: [{ type: Input }], expandedChange: [{ type: Output }], action: [{ type: Output }], expand: [{ type: Output }], collapse: [{ type: Output }], itemClick: [{ type: Output }], overlayClick: [{ type: Output }], childContainer: [{ type: ViewChild, args: ['childContainer'] }], actionSheetViews: [{ type: ContentChildren, args: [forwardRef(() => ActionSheetViewComponent)] }], actionSheetTemplate: [{ type: ContentChild, args: [ActionSheetTemplateDirective] }], headerTemplate: [{ type: ContentChild, args: [ActionSheetHeaderTemplateDirective] }], contentTemplate: [{ type: ContentChild, args: [ActionSheetContentTemplateDirective] }], itemTemplate: [{ type: ContentChild, args: [ActionSheetItemTemplateDirective] }], footerTemplate: [{ type: ContentChild, args: [ActionSheetFooterTemplateDirective] }] } });