@progress/kendo-angular-tooltip
Version:
Kendo UI Tooltip for Angular - A highly customizable and easily themeable tooltip from the creators developers trust for professional Angular components.
1,342 lines (1,322 loc) • 97.3 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 * as i0 from '@angular/core';
import { forwardRef, Directive, Input, Optional, isDevMode, EventEmitter, Component, HostBinding, Output, ViewChild, ContentChild, ElementRef, Injectable, InjectionToken, Inject, NgModule } from '@angular/core';
import * as i1 from '@progress/kendo-angular-l10n';
import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { PreventableEvent, focusableSelector, Keys, isDocumentAvailable, closest, hasObservers, ResizeBatchService } from '@progress/kendo-angular-common';
import * as i1$1 from '@progress/kendo-angular-popup';
import { PopupService } from '@progress/kendo-angular-popup';
import { take, auditTime, filter } from 'rxjs/operators';
import { Subscription, BehaviorSubject, Subject, combineLatest, fromEvent } from 'rxjs';
import { validatePackage } from '@progress/kendo-licensing';
import { NgIf, NgStyle, NgClass, NgTemplateOutlet } from '@angular/common';
import { xIcon } from '@progress/kendo-svg-icons';
import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons';
/**
* @hidden
*/
class LocalizedMessagesDirective extends ComponentMessages {
service;
/**
* The title of the close button.
*/
closeTitle;
constructor(service) {
super();
this.service = service;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LocalizedMessagesDirective, deps: [{ token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: LocalizedMessagesDirective, isStandalone: true, selector: "[kendoTooltipLocalizedMessages]", inputs: { closeTitle: "closeTitle" }, providers: [
{
provide: ComponentMessages,
useExisting: forwardRef(() => LocalizedMessagesDirective)
}
], usesInheritance: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LocalizedMessagesDirective, decorators: [{
type: Directive,
args: [{
providers: [
{
provide: ComponentMessages,
useExisting: forwardRef(() => LocalizedMessagesDirective)
}
],
selector: `[kendoTooltipLocalizedMessages]`,
standalone: true
}]
}], ctorParameters: function () { return [{ type: i1.LocalizationService }]; }, propDecorators: { closeTitle: [{
type: Input
}] } });
/**
* @hidden
*/
const ERRORS = {
popover: `Invalid value provided for the 'popover' property. The accepted data types are 'PopoverComponent' or 'PopoverFn'.`,
templateData: `templateData must be a function, but received`,
showOn: `Invalid value provided for the 'showOn' property. The available options are 'click', 'hover', 'focus' or 'none'.`
};
/**
* Arguments for the `show` event. The `show` event fires when a popover is about
* to be opened. If you cancel the event, the opening is prevented.
*/
class PopoverShowEvent extends PreventableEvent {
/**
* The host element related to the popover.
*/
anchor;
/**
* @hidden
* Constructs the event arguments for the `show` event.
* @param anchor - The host element related to the popover.
*/
constructor(anchor) {
super();
this.anchor = anchor;
}
}
/**
* Arguments for the `hide` event. The `hide` event fires when a popover is about
* to be closed. If you cancel the event, the popover stays open.
*/
class PopoverHideEvent extends PreventableEvent {
/**
* The host element related to the popover.
*/
anchor;
/**
* The popover element.
*/
popover;
/**
* @hidden
* Constructs the event arguments for the `hide` event.
* @param anchor - The host element related to the popover.
* @param popover - The popover element.
*/
constructor(anchor, popover) {
super();
this.anchor = anchor;
this.popover = popover;
}
}
/**
* Arguments for the `shown` event. The `shown` event fires after the popover has opened and its opening animation has finished.
*/
class PopoverShownEvent {
/**
* The host element related to the popover.
*/
anchor;
/**
* The popover element.
*/
popover;
/**
* @hidden
* Constructs the event arguments for the `shown` event.
* @param anchor - The host element related to the popover.
* @param popover - The popover element.
*/
constructor(anchor, popover) {
this.anchor = anchor;
this.popover = popover;
}
}
/**
* Arguments for the `hidden` event. The `hidden` event fires after the popover has closed and its closing animation has finished.
*/
class PopoverHiddenEvent {
/**
* The host element related to the popover.
*/
anchor;
/**
* @hidden
* Constructs the event arguments for the `hidden` event.
* @param anchor - The host element related to the popover.
*/
constructor(anchor) {
this.anchor = anchor;
}
}
/**
* @hidden
*/
let idx = 0;
/**
* @hidden
*/
let popoverTitleIdx = 0;
/**
* @hidden
*/
let popoverBodyIdx = 0;
/**
* @hidden
*/
const getId = (prefix, idSource) => {
switch (idSource) {
case 'popoverTitle':
return `${prefix}-${++popoverTitleIdx}`;
case 'popoverBody':
return `${prefix}-${++popoverBodyIdx}`;
default:
return `${prefix}-${++idx}`;
}
};
/**
* @hidden
*/
function align(position, offset) {
let anchorAlign = {};
let popupAlign = {};
let popupMargin = {};
switch (position) {
case 'top':
anchorAlign = { horizontal: 'center', vertical: 'top' };
popupAlign = { horizontal: 'center', vertical: 'bottom' };
popupMargin = { horizontal: 0, vertical: offset };
break;
case 'bottom':
anchorAlign = { horizontal: 'center', vertical: 'bottom' };
popupAlign = { horizontal: 'center', vertical: 'top' };
popupMargin = { horizontal: 0, vertical: offset };
break;
case 'right':
anchorAlign = { horizontal: 'right', vertical: 'center' };
popupAlign = { horizontal: 'left', vertical: 'center' };
popupMargin = { horizontal: offset, vertical: 0 };
break;
case 'left':
anchorAlign = { horizontal: 'left', vertical: 'center' };
popupAlign = { horizontal: 'right', vertical: 'center' };
popupMargin = { horizontal: offset, vertical: 0 };
break;
default: break;
}
return {
anchorAlign,
popupAlign,
popupMargin
};
}
/**
* @hidden
*/
function collision(inputcollision, position) {
if (inputcollision) {
return inputcollision;
}
if (position === 'top' || position === 'bottom') {
return { horizontal: 'fit', vertical: 'flip' };
}
return { horizontal: 'flip', vertical: 'fit' };
}
function isDocumentNode(container) {
return container.nodeType === 9;
}
/**
* @hidden
*/
function closestBySelector(element, selector) {
if (element.closest) {
return element.closest(selector);
}
const matches = Element.prototype.matches ?
(el, sel) => el.matches(sel)
: (el, sel) => el.msMatchesSelector(sel);
let node = element;
while (node && !isDocumentNode(node)) {
if (matches(node, selector)) {
return node;
}
node = node.parentNode;
}
}
/**
* @hidden
*/
function contains(container, child) {
if (!container) {
return false;
}
if (isDocumentNode(container)) {
return false;
}
if (container.contains) {
return container.contains(child);
}
if (container.compareDocumentPosition) {
return !!(container.compareDocumentPosition(child) & Node.DOCUMENT_POSITION_CONTAINED_BY);
}
}
/**
* @hidden
*/
const hasParent = (node, parent) => {
while (node && node !== parent) {
node = node.parentNode;
}
return node;
};
/**
* @hidden
*/
function getCenterOffset(item, dir, size) {
const rect = item.getBoundingClientRect();
return rect[dir] + (rect[size] / 2);
}
/**
* @hidden
*/
function containsItem(collection, item) {
return collection.indexOf(item) !== -1;
}
/**
* @hidden
*/
function getAllFocusableChildren(parent) {
return parent.querySelectorAll(focusableSelector);
}
/**
* @hidden
*/
function getFirstAndLastFocusable(parent) {
const all = getAllFocusableChildren(parent);
const firstFocusable = all.length > 0 ? all[0] : parent;
const lastFocusable = all.length > 0 ? all[all.length - 1] : parent;
return [firstFocusable, lastFocusable];
}
/**
* @hidden
*/
const packageMetadata = {
name: '@progress/kendo-angular-tooltip',
productName: 'Kendo UI for Angular',
productCode: 'KENDOUIANGULAR',
productCodes: ['KENDOUIANGULAR'],
publishDate: 1743579699,
version: '18.4.0',
licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/'
};
/**
* Represents a template that defines the content of the Popover title.
*
* To define the template, nest an `<ng-template>` tag
* with the `kendoPopoverTitleTemplate` directive inside the `<kendo-popover>` tag.
*/
class PopoverTitleTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverTitleTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverTitleTemplateDirective, isStandalone: true, selector: "[kendoPopoverTitleTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverTitleTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoPopoverTitleTemplate]',
standalone: true
}]
}], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{
type: Optional
}] }]; } });
/**
* Represents a template that defines the content of the Popover body.
*
* To define the template, nest an `<ng-template>` tag
* with the `kendoPopoverBodyTemplate` directive inside the `<kendo-popover>` tag.
*/
class PopoverBodyTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverBodyTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverBodyTemplateDirective, isStandalone: true, selector: "[kendoPopoverBodyTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverBodyTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoPopoverBodyTemplate]',
standalone: true
}]
}], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{
type: Optional
}] }]; } });
/**
* Represents a template that defines the content of the Popover actions.
*
* To define the template, nest an `<ng-template>` tag
* with the `kendoPopoverActionsTemplate` directive inside the `<kendo-popover>` tag.
*/
class PopoverActionsTemplateDirective {
templateRef;
constructor(templateRef) {
this.templateRef = templateRef;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverActionsTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverActionsTemplateDirective, isStandalone: true, selector: "[kendoPopoverActionsTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverActionsTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoPopoverActionsTemplate]',
standalone: true
}]
}], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{
type: Optional
}] }]; } });
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Represents the [Kendo UI Popover component for Angular]({% slug overview_popover %}).
* Used to display additional information that is related to a target element.
*
* @example
* ```ts-no-run
* <kendo-popover>
* <ng-template kendoPopoverTitleTemplate>Foo Title</ng-template>
* <ng-template kendoPopoverBodyTemplate>Foo Body</ng-template>
* <ng-template kendoPopoverActionsTemplate>Foo Actions</ng-template>
* </kendo-popover>
* ```
*/
class PopoverComponent {
localization;
renderer;
element;
zone;
/**
* @hidden
*/
anchor;
/**
* Specifies the position of the Popover in relation to its anchor element. [See example]({% slug positioning_popover %})
*
* The possible options are:
* `top`
* `bottom`
* `right` (Default)
* `left`
*/
position = 'right';
/**
* Specifies the distance from the Popover to its anchor element in pixels.
*
* @default `6`
*/
set offset(value) {
this._offset = value;
}
get offset() {
const calloutBuffer = 14;
return this.callout
? calloutBuffer + this._offset
: this._offset;
}
/**
* Determines the width of the popover. Numeric values are treated as pixels.
* @default 'auto'
*/
set width(value) {
this._width = typeof value === 'number' ? `${value}px` : value;
}
get width() {
return this._width;
}
/**
* Determines the height of the popover. Numeric values are treated as pixels.
* @default 'auto'
*/
set height(value) {
this._height = typeof value === 'number' ? `${value}px` : value;
}
get height() {
return this._height;
}
/**
* @hidden
*/
direction;
/**
* Specifies the main header text of the Popover.
*
* If a `titleTemplate` is provided it would take precedence over the title.
*/
title;
/**
* @hidden
* Specifies the secondary header text of the Popover.
*
* If a `titleTemplate` is provided it would take precedence over the subtitle.
*/
subtitle;
/**
* Represents the text that will be rendered in the Popover body section.
*
* If a `bodyTemplate` is provided it would take precedence over this text.
*/
body;
/**
* Determines whether a callout will be rendered along the Popover. [See example]({% slug callout_popover %})
*
* @default true
*/
callout = true;
/**
* Enables and configures the Popover animation. [See example]({% slug animations_popover %})
*
* The possible options are:
*
* * `boolean`—Enables the default animation
* * `PopoverAnimation`—A configuration object which allows setting the `direction`, `duration` and `type` of the animation.
*
* @default false
*/
animation = false;
/**
* Defines a callback function which returns custom data passed to the Popover templates.
* It exposes the current anchor element as an argument. [See example](slug:templates_popover#toc-passing-data-to-templates)
*/
set templateData(fn) {
if (isDevMode && typeof fn !== 'function') {
throw new Error(`${ERRORS.templateData} ${JSON.stringify(fn)}.`);
}
this._templateData = fn;
}
get templateData() {
return this._templateData;
}
/**
* @hidden
* Determines the visibility of the Popover.
*/
visible = false;
/**
* @hidden
*/
get isHidden() {
return !this.visible;
}
/**
* @hidden
*/
get hasAttributeHidden() {
return !this.visible;
}
/**
* Fires before the Popover is about to be shown ([see example]({% slug events_popover %})).
* The event is preventable. If canceled, the Popover will not be displayed. [See example]({% slug events_popover %})
*/
show = new EventEmitter();
/**
* Fires after the Popover has been shown and the animation has ended. [See example]({% slug events_popover %})
*/
shown = new EventEmitter();
/**
* Fires when the Popover is about to be hidden ([see example]({% slug events_popover %})).
* The event is preventable. If canceled, the Popover will remain visible.
*/
hide = new EventEmitter();
/**
* Fires after the Popover has been hidden and the animation has ended. [See example]({% slug events_popover %})
*/
hidden = new EventEmitter();
/**
* @hidden
*/
closeOnKeyDown = new EventEmitter();
/**
* @hidden
*/
popoverWrapper;
/**
* @hidden
*/
titleTemplateWrapper;
/**
* @hidden
*/
bodyTemplateWrapper;
/**
* @hidden
*/
titleTemplate;
/**
* @hidden
*/
bodyTemplate;
/**
* @hidden
*/
actionsTemplate;
/**
* @hidden
*/
contextData;
/**
* @hidden
*/
_width = 'auto';
/**
* @hidden
*/
_height = 'auto';
/**
* @hidden
*/
popoverId = '';
_offset = 6;
subs = new Subscription();
constructor(localization, renderer, element, zone) {
this.localization = localization;
this.renderer = renderer;
this.element = element;
this.zone = zone;
validatePackage(packageMetadata);
}
ngOnInit() {
this.popoverId = getId('k-popover');
this.subs.add(this.localization.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; }));
this.subs.add(this.renderer.listen(this.element.nativeElement, 'keydown', event => this.onKeyDown(event)));
}
ngAfterViewInit() {
this.zone.onStable.pipe(take(1)).subscribe(() => {
if (this.visible) {
const wrapper = this.popoverWrapper.nativeElement;
const focusablePopoverChildren = getAllFocusableChildren(wrapper);
if (focusablePopoverChildren.length > 0) {
focusablePopoverChildren[0].focus();
}
this.setAriaAttributes(wrapper, focusablePopoverChildren);
}
});
}
ngOnDestroy() {
this.subs.unsubscribe();
}
/**
* @hidden
*/
getCalloutPosition() {
switch (this.position) {
case 'top': return { 'k-callout-s': true };
case 'bottom': return { 'k-callout-n': true };
case 'left': return { 'k-callout-e': true };
case 'right': return { 'k-callout-w': true };
default: return { 'k-callout-s': true };
}
}
/**
* @hidden
*/
onKeyDown(event) {
const keyCode = event.keyCode;
const target = event.target;
if (keyCode === Keys.Tab) {
this.keepFocusWithinComponent(target, event);
}
if (keyCode === Keys.Escape) {
this.closeOnKeyDown.emit();
}
}
_templateData = () => null;
keepFocusWithinComponent(target, event) {
const wrapper = this.popoverWrapper.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();
}
}
setAriaAttributes(wrapper, focusablePopoverChildren) {
if (this.titleTemplate) {
const titleRef = this.titleTemplateWrapper.nativeElement;
const focusableHeaderChildren = getAllFocusableChildren(titleRef).length > 0;
if (focusableHeaderChildren) {
const headerId = getId('k-popover-header', 'popoverTitle');
this.renderer.setAttribute(titleRef, 'id', headerId);
this.renderer.setAttribute(wrapper, 'aria-labelledby', headerId);
}
}
if (this.bodyTemplate) {
const bodyRef = this.bodyTemplateWrapper.nativeElement;
const focusableBodyChildren = getAllFocusableChildren(bodyRef).length > 0;
if (focusableBodyChildren) {
const bodyId = getId('k-popover-body', 'popoverBody');
this.renderer.setAttribute(bodyRef, 'id', bodyId);
this.renderer.setAttribute(wrapper, 'aria-describedby', bodyId);
}
}
this.renderer.setAttribute(wrapper, 'id', this.popoverId);
this.renderer.setAttribute(wrapper, 'role', focusablePopoverChildren.length > 0 ? 'dialog' : 'tooltip');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverComponent, deps: [{ token: i1.LocalizationService }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: PopoverComponent, isStandalone: true, selector: "kendo-popover", inputs: { position: "position", offset: "offset", width: "width", height: "height", title: "title", subtitle: "subtitle", body: "body", callout: "callout", animation: "animation", templateData: "templateData" }, outputs: { show: "show", shown: "shown", hide: "hide", hidden: "hidden", closeOnKeyDown: "closeOnKeyDown" }, host: { properties: { "attr.dir": "this.direction", "class.k-hidden": "this.isHidden", "attr.aria-hidden": "this.hasAttributeHidden", "style.width": "this._width", "style.height": "this._height" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.popover'
}
], queries: [{ propertyName: "titleTemplate", first: true, predicate: PopoverTitleTemplateDirective, descendants: true }, { propertyName: "bodyTemplate", first: true, predicate: PopoverBodyTemplateDirective, descendants: true }, { propertyName: "actionsTemplate", first: true, predicate: PopoverActionsTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "popoverWrapper", first: true, predicate: ["popoverWrapper"], descendants: true }, { propertyName: "titleTemplateWrapper", first: true, predicate: ["titleTemplateWrapper"], descendants: true }, { propertyName: "bodyTemplateWrapper", first: true, predicate: ["bodyTemplateWrapper"], descendants: true }], ngImport: i0, template: `
<div #popoverWrapper *ngIf="visible" class="k-popover k-popup" [ngStyle]="{'width': width, 'height': height}">
<div class="k-popover-callout" [ngClass]="getCalloutPosition()" *ngIf="callout"></div>
<div class="k-popover-inner" *ngIf="callout; else noCallout">
<ng-container *ngTemplateOutlet="noCallout"></ng-container>
</div>
<ng-template #noCallout>
<div #titleTemplateWrapper *ngIf="titleTemplate || title" class="k-popover-header">
<ng-template *ngIf="titleTemplate"
[ngTemplateOutlet]="titleTemplate?.templateRef"
[ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }">
</ng-template>
<ng-container *ngIf="title && !titleTemplate">
{{ title }}
</ng-container>
</div>
<div #bodyTemplateWrapper *ngIf="bodyTemplate || body" class="k-popover-body">
<ng-template *ngIf="bodyTemplate"
[ngTemplateOutlet]="bodyTemplate?.templateRef"
[ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }">
</ng-template>
<ng-container *ngIf="body && !bodyTemplate">
{{ body }}
</ng-container>
</div>
<div *ngIf="actionsTemplate" class="k-popover-actions k-actions k-actions-stretched k-actions-horizontal">
<ng-template *ngIf="actionsTemplate"
[ngTemplateOutlet]="actionsTemplate?.templateRef"
[ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }">
</ng-template>
</div>
</ng-template>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverComponent, decorators: [{
type: Component,
args: [{
selector: 'kendo-popover',
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.popover'
}
],
template: `
<div #popoverWrapper *ngIf="visible" class="k-popover k-popup" [ngStyle]="{'width': width, 'height': height}">
<div class="k-popover-callout" [ngClass]="getCalloutPosition()" *ngIf="callout"></div>
<div class="k-popover-inner" *ngIf="callout; else noCallout">
<ng-container *ngTemplateOutlet="noCallout"></ng-container>
</div>
<ng-template #noCallout>
<div #titleTemplateWrapper *ngIf="titleTemplate || title" class="k-popover-header">
<ng-template *ngIf="titleTemplate"
[ngTemplateOutlet]="titleTemplate?.templateRef"
[ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }">
</ng-template>
<ng-container *ngIf="title && !titleTemplate">
{{ title }}
</ng-container>
</div>
<div #bodyTemplateWrapper *ngIf="bodyTemplate || body" class="k-popover-body">
<ng-template *ngIf="bodyTemplate"
[ngTemplateOutlet]="bodyTemplate?.templateRef"
[ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }">
</ng-template>
<ng-container *ngIf="body && !bodyTemplate">
{{ body }}
</ng-container>
</div>
<div *ngIf="actionsTemplate" class="k-popover-actions k-actions k-actions-stretched k-actions-horizontal">
<ng-template *ngIf="actionsTemplate"
[ngTemplateOutlet]="actionsTemplate?.templateRef"
[ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }">
</ng-template>
</div>
</ng-template>
</div>
`,
standalone: true,
imports: [NgIf, NgStyle, NgClass, NgTemplateOutlet]
}]
}], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { position: [{
type: Input
}], offset: [{
type: Input
}], width: [{
type: Input
}], height: [{
type: Input
}], direction: [{
type: HostBinding,
args: ['attr.dir']
}], title: [{
type: Input
}], subtitle: [{
type: Input
}], body: [{
type: Input
}], callout: [{
type: Input
}], animation: [{
type: Input
}], templateData: [{
type: Input
}], isHidden: [{
type: HostBinding,
args: ['class.k-hidden']
}], hasAttributeHidden: [{
type: HostBinding,
args: ['attr.aria-hidden']
}], show: [{
type: Output
}], shown: [{
type: Output
}], hide: [{
type: Output
}], hidden: [{
type: Output
}], closeOnKeyDown: [{
type: Output
}], popoverWrapper: [{
type: ViewChild,
args: ['popoverWrapper']
}], titleTemplateWrapper: [{
type: ViewChild,
args: ['titleTemplateWrapper']
}], bodyTemplateWrapper: [{
type: ViewChild,
args: ['bodyTemplateWrapper']
}], titleTemplate: [{
type: ContentChild,
args: [PopoverTitleTemplateDirective, { static: false }]
}], bodyTemplate: [{
type: ContentChild,
args: [PopoverBodyTemplateDirective, { static: false }]
}], actionsTemplate: [{
type: ContentChild,
args: [PopoverActionsTemplateDirective, { static: false }]
}], _width: [{
type: HostBinding,
args: ['style.width']
}], _height: [{
type: HostBinding,
args: ['style.height']
}] } });
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
const validShowOptions = ['hover', 'click', 'none', 'focus'];
/**
* @hidden
*/
class PopoverDirectivesBase {
ngZone;
popupService;
renderer;
/**
* Specifies the popover instance that will be rendered.
* Accepts a [`PopoverComponent`]({% slug api_tooltip_popovercomponent %}) instance or
* a [`PopoverFn`]({% slug api_tooltip_popoverfn %}) callback which returns a [`PopoverComponent`]({% slug api_tooltip_popovercomponent %}) instance
* depending on the current anchor element.
*
* [See example](slug:templates_popover#toc-passing-data-to-templates)
*/
set popover(value) {
if (value instanceof PopoverComponent || typeof value === `function`) {
this._popover = value;
}
else {
if (isDevMode) {
throw new Error(ERRORS.popover);
}
}
}
get popover() {
return this._popover;
}
/**
* The event on which the Popover will be shown
*
* The supported values are:
* - `click` (default) —The Popover will be shown when its `anchor` element is clicked.
* - `hover`—The Popover will be shown when its `anchor` element is hovered.
* - `focus`—The Popover will be shown when its `anchor` element is focused.
* - `none`—The Popover will not be shown on user interaction. It could be rendered via the Popover API methods.
*/
set showOn(value) {
if (isDevMode && !containsItem(validShowOptions, value)) {
throw new Error(ERRORS.showOn);
}
this._showOn = value;
}
get showOn() {
return this._showOn;
}
/**
* @hidden
*/
anchor = null;
popupRef;
disposeHoverOverListener;
disposeHoverOutListener;
disposeClickListener;
disposePopupHoverOutListener;
disposePopupHoverInListener;
disposePopupFocusOutListener;
subs = new Subscription();
_popoverService;
_hideSub;
_focusInsideSub;
_popover;
_showOn = 'click';
_popupOpenSub;
_popupCloseSub;
_popupSubs;
constructor(ngZone, popupService, renderer) {
this.ngZone = ngZone;
this.popupService = popupService;
this.renderer = renderer;
}
ngAfterViewInit() {
if (!isDocumentAvailable()) {
return;
}
this.manageEvents();
}
ngOnDestroy() {
this.closePopup();
this.subs.unsubscribe();
this._popupSubs && this._popupSubs.unsubscribe();
if (this.disposeHoverOverListener) {
this.disposeHoverOverListener();
}
if (this.disposeHoverOutListener) {
this.disposeHoverOutListener();
}
if (this.disposeClickListener) {
this.disposeClickListener();
}
if (this._focusInsideSub) {
this._focusInsideSub.unsubscribe();
}
if (this._hideSub) {
this._hideSub.unsubscribe();
}
if (this._popupOpenSub) {
this._popupOpenSub.unsubscribe();
}
if (this._popupCloseSub) {
this._popupCloseSub.unsubscribe();
}
}
/**
* Hides the Popover ([See example]({% slug programmaticcontrol_popover %})).
*/
hide() {
this.closePopup();
}
/**
* @hidden
*/
closePopup() {
if (this.popupRef) {
if (this.anchor) {
this.renderer.removeAttribute(this.anchor, 'aria-describedby');
}
this.popupRef.close();
this.popupRef = null;
if (this.disposePopupHoverOutListener) {
this.disposePopupHoverOutListener();
}
if (this.disposePopupHoverInListener) {
this.disposePopupHoverInListener();
}
if (this.disposePopupFocusOutListener) {
this.disposePopupFocusOutListener();
}
this._popupSubs.unsubscribe();
}
}
/**
* @hidden
*/
openPopup(anchor) {
this.anchor = anchor instanceof ElementRef ? anchor.nativeElement : anchor;
const popoverComp = this.popover instanceof PopoverComponent ? this.popover : this.popover(this.anchor);
const alignSettings = align(popoverComp.position, popoverComp.offset);
const anchorAlign = alignSettings.anchorAlign;
const popupAlign = alignSettings.popupAlign;
const popupMargin = alignSettings.popupMargin;
const _animation = popoverComp.animation;
this.popupRef = this.popupService.open({
anchor: { nativeElement: this.anchor },
animate: _animation,
content: PopoverComponent,
popupAlign,
anchorAlign,
margin: popupMargin,
collision: { horizontal: 'fit', vertical: 'fit' }
});
const popupInstance = this.popupRef.content.instance;
this._popupSubs = new Subscription();
if (anchor) {
this._popupSubs.add(this.renderer.listen(this.anchor, 'keydown', event => this.onKeyDown(event)));
this.renderer.setAttribute(this.anchor, 'aria-describedby', popupInstance.popoverId);
}
this._popupSubs.add(popupInstance.closeOnKeyDown.subscribe(() => {
this.anchor.focus();
this.hide();
}));
this.applySettings(this.popupRef.content, popoverComp);
this.monitorPopup();
this.initializeCompletionEvents(popoverComp, this.anchor);
}
/**
* @hidden
*/
isPrevented(anchorElement, show) {
const popoverComp = this.popover instanceof PopoverComponent ? this.popover : this.popover(anchorElement);
let eventArgs;
// eslint-disable-next-line prefer-const
eventArgs = this.initializeEvents(popoverComp, eventArgs, show, anchorElement);
return eventArgs.isDefaultPrevented();
}
/**
* @hidden
*/
monitorPopup() {
if (this.showOn === 'hover') {
this.ngZone.runOutsideAngular(() => {
const popup = this.popupRef.popupElement;
this.disposePopupHoverInListener = this.renderer.listen(popup, 'mouseenter', _ => {
this.ngZone.run(_ => this._popoverService.emitPopoverState(true));
});
this.disposePopupHoverOutListener = this.renderer.listen(popup, 'mouseleave', _ => {
this.ngZone.run(_ => this._popoverService.emitPopoverState(false));
});
});
}
if (this.showOn === 'focus') {
this.ngZone.runOutsideAngular(() => {
const popup = this.popupRef.popupElement;
this.disposePopupFocusOutListener = this.renderer.listen(popup, 'focusout', (e) => {
const isInsidePopover = closest(e.relatedTarget, (node) => node.classList && node.classList.contains('k-popover'));
if (!isInsidePopover) {
this.ngZone.run(_ => this._popoverService.emitFocusInsidePopover(false));
}
});
});
}
}
applySettings(contentComponent, popover) {
const content = contentComponent.instance;
content.visible = true;
content.anchor = this.anchor;
content.position = popover.position;
content.offset = popover.offset;
content.width = popover.width;
content.height = popover.height;
content.title = popover.title;
content.body = popover.body;
content.callout = popover.callout;
content.animation = popover.animation;
content.contextData = popover.templateData(this.anchor);
content.titleTemplate = popover.titleTemplate;
content.bodyTemplate = popover.bodyTemplate;
content.actionsTemplate = popover.actionsTemplate;
this.popupRef.content.changeDetectorRef.detectChanges();
}
manageEvents() {
this.ngZone.runOutsideAngular(() => {
switch (this.showOn) {
case 'hover':
this.subscribeToShowEvents([{
name: 'mouseenter', handler: this.mouseenterHandler
}, {
name: 'mouseleave', handler: this.mouseleaveHandler
}]);
break;
case 'focus':
this.subscribeToShowEvents([{
name: 'focus', handler: this.focusHandler
}, {
name: 'blur', handler: this.blurHandler
}]);
break;
case 'click':
this.subscribeClick();
break;
default:
break;
}
});
}
/**
* @hidden
*/
initializeEvents(popoverComp, eventArgs, show, anchorElement) {
if (show) {
eventArgs = new PopoverShowEvent(anchorElement);
if (this.shouldEmitEvent(!!this.popupRef, 'show', popoverComp)) {
this.ngZone.run(() => popoverComp.show.emit(eventArgs));
}
}
else {
eventArgs = new PopoverHideEvent(anchorElement, this.popupRef);
if (this.shouldEmitEvent(!!this.popupRef, 'hide', popoverComp)) {
this.ngZone.run(() => popoverComp.hide.emit(eventArgs));
}
}
return eventArgs;
}
onKeyDown(event) {
const keyCode = event.keyCode;
if (keyCode === Keys.Escape) {
this.hide();
}
}
initializeCompletionEvents(popoverComp, _anchor) {
if (this.shouldEmitCompletionEvents('shown', popoverComp)) {
this.popupRef.popupOpen.subscribe(() => {
const eventArgs = new PopoverShownEvent(_anchor, this.popupRef);
popoverComp.shown.emit(eventArgs);
});
}
if (this.shouldEmitCompletionEvents('hidden', popoverComp)) {
this.popupRef.popupClose.subscribe(() => {
this.ngZone.run(_ => {
const eventArgs = new PopoverHiddenEvent(_anchor);
popoverComp.hidden.emit(eventArgs);
});
});
}
}
shouldEmitEvent(hasPopup, event, popoverComp) {
if ((event === 'show' && !hasPopup && hasObservers(popoverComp[event]))
|| (event === 'hide' && hasPopup && hasObservers(popoverComp[event]))) {
return true;
}
return false;
}
shouldEmitCompletionEvents(event, popoverComp) {
if ((hasObservers(popoverComp[event]) && !this._popupOpenSub)
|| (hasObservers(popoverComp[event]) && !this._popupCloseSub)) {
return true;
}
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverDirectivesBase, deps: [{ token: i0.NgZone }, { token: i1$1.PopupService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverDirectivesBase, inputs: { popover: "popover", showOn: "showOn" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverDirectivesBase, decorators: [{
type: Directive,
args: [{}]
}], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i1$1.PopupService }, { type: i0.Renderer2 }]; }, propDecorators: { popover: [{
type: Input
}], showOn: [{
type: Input
}] } });
/**
* @hidden
*/
class PopoverService {
ngZone;
_pointerOverPopup = new BehaviorSubject(null);
_pointerOverAnchor = new BehaviorSubject(null);
_focusInsidePopover = new BehaviorSubject(null);
_hidePopover = new Subject();
_isOrigin;
originAnchor;
currentAnchor;
subs = new Subscription();
constructor(ngZone) {
this.ngZone = ngZone;
this.monitor();
}
ngOnDestroy() {
this.subs.unsubscribe();
}
get isPopoverHovered() {
return this._pointerOverPopup.asObservable();
}
emitPopoverState(isHovered) {
this.ngZone.run(_ => this._pointerOverPopup.next(isHovered));
}
get isAnchorHovered() {
return this._pointerOverAnchor.asObservable();
}
emitAnchorState(isHovered, anchor) {
this._isOrigin = this.originAnchor === anchor;
this.currentAnchor = anchor;
if (isHovered) {
this.originAnchor = anchor;
}
this.ngZone.run(_ => this._pointerOverAnchor.next(isHovered));
}
get isFocusInsidePopover() {
return this._focusInsidePopover.asObservable();
}
emitFocusInsidePopover(isFocused) {
this.ngZone.run(_ => this._focusInsidePopover.next(isFocused));
this._focusInsidePopover.next(null);
}
get hidePopover() {
return this._hidePopover.asObservable();
}
monitor() {
this.subs.add(combineLatest(this.isPopoverHovered, this.isAnchorHovered).pipe(
// `auditTime` is used because the `mouseleave` event is emitted before `mouseenter`
// i.e. there is a millisecond in which the pointer leaves the first target (e.g. anchor) and hasn't reached the second one (e.g. popup)
// resulting in both observables emitting `false`
auditTime(20)).subscribe(val => {
const [isPopoverHovered, isAnchorHovered] = val;
this._hidePopover.next([isPopoverHovered, isAnchorHovered, this._isOrigin, this.currentAnchor]);
}));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: i0.NgZone }]; } });
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Represents the [`kendoPopoverAnchor`](slug:configuration_popover#toc-popover-anchor) directive.
* It is used to target an element, which should display a popover on interaction.
*
* @example
* ```ts-no-run
* <button kendoPopoverAnchor [popover]="myPopover">Show Popover</button>
* ```
*/
class PopoverAnchorDirective extends PopoverDirectivesBase {
hostEl;
ngZone;
popupService;
renderer;
popoverService;
constructor(hostEl, ngZone, popupService, renderer, popoverService) {
super(ngZone, popupService, renderer);
this.hostEl = hostEl;
this.ngZone = ngZone;
this.popupService = popupService;
this.renderer = renderer;
this.popoverService = popoverService;
this._popoverService = this.popoverService;
}
ngOnChanges(changes) {
if (changes['showOn'] && !changes['showOn'].isFirstChange()) {
this.subs.unsubscribe();
if (this.disposeClickListener) {
this.disposeClickListener();
}
this.subs = new Subscription();
this.manageEvents();
}
}
/**
* Shows the Popover. [See example]({% slug programmaticcontrol_popover %})
*/
show() {
if (this.popupRef) {
return;
}
this.ngZone.run(() => {
this.openPopup(this.hostEl);
});
this.popupRef.popupAnchorViewportLeave
.pipe(take(1))
.subscribe(() => this.hide());
}
/**
* Toggles the visibility of the Popover. [See example]({% slug programmaticcontrol_popover %})
*/
toggle() {
if (this.popupRef) {
this.hide();
}
else {
this.show();
}
}
subscribeToShowEvents(arr) {
const hostEl = this.hostEl.nativeElement;
this.subs.add(this.renderer.listen(hostEl, arr[0].name, () => {
this.popoverService.emitAnchorState(true, hostEl);
arr[0].handler();
}));
this.subs.add(this.renderer.listen(hostEl, arr[1].name, (e) => {
this.popoverService.emitAnchorState(false, null);
arr[1].handler({ domEvent: e });
}));
}
subscribeClick() {
if (this.disposeClickListener) {
this.disposeClickListener();
}
this.disposeClickListener = this.renderer.listen(document, 'click', (e) => {
this.onClick(e);
});
}
mouseenterHandler = () => {
this.controlVisibility(this.hostEl.nativeElement, true);
};
mouseleaveHandler = () => {
if (this.isPrevented(this.hostEl.nativeElement, false)) {
return;
}
if (!this._hideSub) {
this._hideSub = this.popoverService.hidePopover.subscribe((val) => {
const [isPopoverHovered, isAnchorHovered] = val;
if (!isPopoverHovered && !isAnchorHovered) {
this.hide();
}
});
}
};
focusHandler = () => {
this.controlVisibility(this.hostEl.nativeElement, true);
};
blurHandler = (args) => {
const event = args.domEvent;
if (this.isPrevented(this.hostEl.nativeElement, false)) {
return;
}
// from anchor to popup focus check
const isFocusInside = !!closest(event.relatedTarget, (node) => node.classList && node.classList.contains('k-popover'));
if (!isFocusInside) {
this.hide();
}
if (!this._focusInsideSub) {
// inside popup focus check
this._focusInsideSub = this.popoverService.isFocusInsidePopover.pipe(filter(v => v !== null)).subscribe((val) => {
if (!val) {
this.hide();
}
});
}
};
/**
* @hidden
*/
onClick(event) {
const isInsidePopup = !!closest(event.target, (node) => node.classList && node.classList.contains('k-popup'));
const isAnchor = !!closest(event.target, (node) => node === this.hostEl.nativeElement);
if (isInsidePopup || (this.popupRef && isAnchor)) {
return;
}
if (isAnchor) {
// on open