@progress/kendo-angular-navigation
Version:
Kendo UI Navigation for Angular
665 lines (650 loc) • 30.7 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 { 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]
}] } });