@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.
472 lines (463 loc) • 19.2 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, ContentChild, ElementRef, EventEmitter, HostBinding, Input, isDevMode, NgZone, Output, Renderer2, ViewChild } from '@angular/core';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { Subscription } from 'rxjs';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from '../package-metadata';
import { ERRORS } from '../constants';
import { PopoverTitleTemplateDirective } from './template-directives/title-template.directive';
import { PopoverBodyTemplateDirective } from './template-directives/body-template.directive';
import { PopoverActionsTemplateDirective } from './template-directives/actions-template.directive';
import { Keys } from '@progress/kendo-angular-common';
import { take } from 'rxjs/operators';
import { getAllFocusableChildren, getFirstAndLastFocusable, getId } from '../utils';
import { NgIf, NgStyle, NgClass, NgTemplateOutlet } from '@angular/common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
/**
* 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>
* ```
*/
export 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']
}] } });