@clr/angular
Version:
Angular components for Clarity
1,083 lines (1,068 loc) • 45.8 kB
JavaScript
import { hasModifierKey } from '@angular/cdk/keycodes';
import * as i1 from '@angular/cdk/overlay';
import { OverlayConfig } from '@angular/cdk/overlay';
import { DomPortal } from '@angular/cdk/portal';
import { isPlatformBrowser } from '@angular/common';
import * as i0 from '@angular/core';
import { ElementRef, Injectable, PLATFORM_ID, Input, Optional, SkipSelf, Inject, Directive, InjectionToken, HostListener, EventEmitter, Output, NgModule } from '@angular/core';
import { preventArrowKeyScroll, ClrPosition, Keys } from '@clr/angular/utils';
import { Subject, merge, timer, switchMap, fromEvent } from 'rxjs';
import { filter } from 'rxjs/operators';
class ClrPopoverService {
constructor() {
this.panelClass = [];
this._open = false;
this._openChange = new Subject();
this._openEventChange = new Subject();
this._positionChange = new Subject();
this._resetPositions = new Subject();
this._updatePosition = new Subject();
this._popoverVisible = new Subject();
}
get originElement() {
return this.origin instanceof ElementRef ? this.origin : null;
}
get originPoint() {
return this.origin && 'x' in this.origin && 'y' in this.origin ? this.origin : null;
}
get openChange() {
return this._openChange.asObservable();
}
get popoverVisible() {
return this._popoverVisible.asObservable();
}
get openEvent() {
return this._openEvent;
}
set openEvent(event) {
this._openEvent = event;
this._openEventChange.next(event);
}
get open() {
return this._open;
}
set open(value) {
value = !!value;
if (this._open !== value) {
this._open = value;
this._openChange.next(value);
}
}
get resetPositionsChange() {
return this._resetPositions.asObservable();
}
positionChange(position) {
this._positionChange.next(position);
}
updatePositionChange() {
return this._updatePosition.asObservable();
}
getPositionChange() {
return this._positionChange.asObservable();
}
getEventChange() {
return this._openEventChange.asObservable();
}
/**
* Sometimes, we need to remember the event that triggered the toggling to avoid loops.
* This is for instance the case of components that open on a click, but close on a click outside.
*/
toggleWithEvent(event) {
preventArrowKeyScroll(event);
this.openEvent = event;
this.open = !this.open;
}
/**
* Opens the popover at a specific screen coordinate.
* Useful for context menus where the popover should appear at the cursor position.
*/
openAtPoint(point, targetElement) {
if (this._open) {
this._open = false;
this._openChange.next(false);
}
this.origin = point;
this.pointTargetElement = targetElement;
this.open = true;
}
popoverVisibleEmit(visible) {
this._popoverVisible.next(visible);
}
resetPositions() {
this._resetPositions.next();
}
updatePosition() {
this._updatePosition.next();
}
focusCloseButton() {
this.closeButtonRef.nativeElement?.focus();
}
focusOrigin() {
this.originElement?.nativeElement?.focus({ preventScroll: true });
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverService, decorators: [{
type: Injectable
}] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
var ClrPopoverType;
(function (ClrPopoverType) {
ClrPopoverType[ClrPopoverType["SIGNPOST"] = 0] = "SIGNPOST";
ClrPopoverType[ClrPopoverType["TOOLTIP"] = 1] = "TOOLTIP";
ClrPopoverType[ClrPopoverType["DROPDOWN"] = 2] = "DROPDOWN";
ClrPopoverType[ClrPopoverType["DEFAULT"] = 3] = "DEFAULT";
})(ClrPopoverType || (ClrPopoverType = {}));
var ClrPopoverPosition;
(function (ClrPopoverPosition) {
ClrPopoverPosition["TOP_RIGHT"] = "top-right";
ClrPopoverPosition["TOP_MIDDLE"] = "top-middle";
ClrPopoverPosition["TOP_LEFT"] = "top-left";
ClrPopoverPosition["RIGHT"] = "right";
ClrPopoverPosition["RIGHT_TOP"] = "right-top";
ClrPopoverPosition["RIGHT_MIDDLE"] = "right-middle";
ClrPopoverPosition["RIGHT_BOTTOM"] = "right-bottom";
ClrPopoverPosition["LEFT"] = "left";
ClrPopoverPosition["LEFT_TOP"] = "left-top";
ClrPopoverPosition["LEFT_MIDDLE"] = "left-middle";
ClrPopoverPosition["LEFT_BOTTOM"] = "left-bottom";
ClrPopoverPosition["BOTTOM_RIGHT"] = "bottom-right";
ClrPopoverPosition["BOTTOM_MIDDLE"] = "bottom-middle";
ClrPopoverPosition["BOTTOM_LEFT"] = "bottom-left";
})(ClrPopoverPosition || (ClrPopoverPosition = {}));
const TOOLTIP_POSITIONS = [
ClrPopoverPosition.RIGHT, // default. must be at index 0
ClrPopoverPosition.LEFT,
ClrPopoverPosition.BOTTOM_RIGHT,
ClrPopoverPosition.BOTTOM_LEFT,
ClrPopoverPosition.TOP_RIGHT,
ClrPopoverPosition.TOP_LEFT,
];
const DROPDOWN_POSITIONS = [
ClrPopoverPosition.BOTTOM_LEFT, // default. must be at index 0
ClrPopoverPosition.BOTTOM_RIGHT,
ClrPopoverPosition.TOP_LEFT,
ClrPopoverPosition.TOP_RIGHT,
ClrPopoverPosition.RIGHT_TOP,
ClrPopoverPosition.RIGHT_BOTTOM,
ClrPopoverPosition.LEFT_TOP,
ClrPopoverPosition.LEFT_BOTTOM,
];
const SIGNPOST_POSITIONS = [
ClrPopoverPosition.RIGHT_MIDDLE, // default. must be at index 0
ClrPopoverPosition.RIGHT_TOP,
ClrPopoverPosition.RIGHT_BOTTOM,
ClrPopoverPosition.TOP_RIGHT,
ClrPopoverPosition.TOP_LEFT,
ClrPopoverPosition.TOP_MIDDLE,
ClrPopoverPosition.BOTTOM_RIGHT,
ClrPopoverPosition.BOTTOM_MIDDLE,
ClrPopoverPosition.BOTTOM_LEFT,
ClrPopoverPosition.LEFT_BOTTOM,
ClrPopoverPosition.LEFT_MIDDLE,
ClrPopoverPosition.LEFT_TOP,
];
function getPositionsArray(type) {
switch (type) {
case ClrPopoverType.TOOLTIP:
return TOOLTIP_POSITIONS;
case ClrPopoverType.DROPDOWN:
return DROPDOWN_POSITIONS;
case ClrPopoverType.SIGNPOST:
case ClrPopoverType.DEFAULT:
default:
return SIGNPOST_POSITIONS;
}
}
function getConnectedPositions(type) {
const result = [];
getPositionsArray(type).forEach(position => {
result.push(mapPopoverKeyToPosition(position, type));
});
return result;
}
const POPOVER_OFFSETS = {
[ClrPopoverType.SIGNPOST]: 16,
[ClrPopoverType.TOOLTIP]: 21,
[ClrPopoverType.DROPDOWN]: 2,
[ClrPopoverType.DEFAULT]: 0,
};
function getOffset(key, type) {
const offset = POPOVER_OFFSETS[type] || 0;
switch (key) {
// TOP
case ClrPopoverPosition.TOP_LEFT:
case ClrPopoverPosition.TOP_MIDDLE:
case ClrPopoverPosition.TOP_RIGHT:
return {
offsetY: -offset,
offsetX: 0,
};
// LEFT
case ClrPopoverPosition.LEFT_TOP:
case ClrPopoverPosition.LEFT_MIDDLE:
case ClrPopoverPosition.LEFT:
case ClrPopoverPosition.LEFT_BOTTOM:
return {
offsetY: 0,
offsetX: -offset,
};
// RIGHT
case ClrPopoverPosition.RIGHT_TOP:
case ClrPopoverPosition.RIGHT_MIDDLE:
case ClrPopoverPosition.RIGHT:
case ClrPopoverPosition.RIGHT_BOTTOM:
return {
offsetY: 0,
offsetX: offset,
};
// BOTTOM and DEFAULT
case ClrPopoverPosition.BOTTOM_RIGHT:
case ClrPopoverPosition.BOTTOM_MIDDLE:
case ClrPopoverPosition.BOTTOM_LEFT:
default:
return {
offsetY: offset,
offsetX: 0,
};
}
}
const STANDARD_ORIGINS = {
// TOP
[ClrPopoverPosition.TOP_RIGHT]: { origin: ClrPosition.TOP_CENTER, content: ClrPosition.BOTTOM_LEFT },
[ClrPopoverPosition.TOP_MIDDLE]: { origin: ClrPosition.TOP_CENTER, content: ClrPosition.BOTTOM_CENTER },
[ClrPopoverPosition.TOP_LEFT]: { origin: ClrPosition.TOP_CENTER, content: ClrPosition.BOTTOM_RIGHT },
// LEFT
[ClrPopoverPosition.LEFT]: { origin: ClrPosition.LEFT_CENTER, content: ClrPosition.RIGHT_TOP },
[ClrPopoverPosition.LEFT_TOP]: { origin: ClrPosition.LEFT_CENTER, content: ClrPosition.RIGHT_BOTTOM },
[ClrPopoverPosition.LEFT_MIDDLE]: { origin: ClrPosition.LEFT_CENTER, content: ClrPosition.RIGHT_CENTER },
[ClrPopoverPosition.LEFT_BOTTOM]: { origin: ClrPosition.LEFT_CENTER, content: ClrPosition.RIGHT_TOP },
// RIGHT
[ClrPopoverPosition.RIGHT]: { origin: ClrPosition.RIGHT_CENTER, content: ClrPosition.LEFT_TOP },
[ClrPopoverPosition.RIGHT_TOP]: { origin: ClrPosition.RIGHT_CENTER, content: ClrPosition.LEFT_BOTTOM },
[ClrPopoverPosition.RIGHT_MIDDLE]: { origin: ClrPosition.RIGHT_CENTER, content: ClrPosition.LEFT_CENTER },
[ClrPopoverPosition.RIGHT_BOTTOM]: { origin: ClrPosition.RIGHT_CENTER, content: ClrPosition.LEFT_TOP },
// BOTTOM
[ClrPopoverPosition.BOTTOM_RIGHT]: { origin: ClrPosition.BOTTOM_CENTER, content: ClrPosition.TOP_LEFT },
[ClrPopoverPosition.BOTTOM_MIDDLE]: { origin: ClrPosition.BOTTOM_CENTER, content: ClrPosition.TOP_CENTER },
[ClrPopoverPosition.BOTTOM_LEFT]: { origin: ClrPosition.BOTTOM_CENTER, content: ClrPosition.TOP_RIGHT },
};
const DROPDOWN_ORIGINS = {
// TOP
[ClrPopoverPosition.TOP_RIGHT]: { origin: ClrPosition.TOP_RIGHT, content: ClrPosition.BOTTOM_RIGHT },
[ClrPopoverPosition.TOP_LEFT]: { origin: ClrPosition.TOP_LEFT, content: ClrPosition.BOTTOM_LEFT },
// LEFT
[ClrPopoverPosition.LEFT_TOP]: { origin: ClrPosition.LEFT_TOP, content: ClrPosition.TOP_RIGHT },
[ClrPopoverPosition.LEFT_BOTTOM]: { origin: ClrPosition.LEFT_BOTTOM, content: ClrPosition.BOTTOM_RIGHT },
// RIGHT
[ClrPopoverPosition.RIGHT_TOP]: { origin: ClrPosition.RIGHT_TOP, content: ClrPosition.LEFT_TOP },
[ClrPopoverPosition.RIGHT_BOTTOM]: { origin: ClrPosition.RIGHT_BOTTOM, content: ClrPosition.LEFT_BOTTOM },
// BOTTOM
[ClrPopoverPosition.BOTTOM_RIGHT]: { origin: ClrPosition.BOTTOM_LEFT, content: ClrPosition.TOP_RIGHT },
[ClrPopoverPosition.BOTTOM_LEFT]: { origin: ClrPosition.BOTTOM_RIGHT, content: ClrPosition.TOP_LEFT },
};
function mapPopoverKeyToPosition(key, type) {
let offset = getOffset(key, type);
const defaultPosition = { origin: ClrPosition.BOTTOM_LEFT, content: ClrPosition.TOP_LEFT };
const { origin, content } = (type === ClrPopoverType.DROPDOWN ? DROPDOWN_ORIGINS[key] : STANDARD_ORIGINS[key]) ?? defaultPosition;
return {
...getOriginPosition(origin),
...getContentPosition(content),
...offset,
panelClass: key,
};
}
function getOriginPosition(key) {
switch (key) {
// TOP Positions
case ClrPosition.TOP_LEFT:
return {
originX: 'start',
originY: 'top',
};
case ClrPosition.TOP_CENTER:
return {
originX: 'center',
originY: 'top',
};
case ClrPosition.TOP_RIGHT:
return {
originX: 'end',
originY: 'top',
};
// LEFT Positions
case ClrPosition.LEFT_TOP:
return {
originX: 'start',
originY: 'top',
};
case ClrPosition.LEFT_CENTER:
return {
originX: 'start',
originY: 'center',
};
case ClrPosition.LEFT_BOTTOM:
return {
originX: 'start',
originY: 'bottom',
};
// RIGHT Positions
case ClrPosition.RIGHT_TOP:
return {
originX: 'end',
originY: 'top',
};
case ClrPosition.RIGHT_CENTER:
return {
originX: 'end',
originY: 'center',
};
case ClrPosition.RIGHT_BOTTOM:
return {
originX: 'end',
originY: 'bottom',
};
// BOTTOM positions and default
case ClrPosition.BOTTOM_LEFT:
return {
originX: 'end',
originY: 'bottom',
};
case ClrPosition.BOTTOM_CENTER:
return {
originX: 'center',
originY: 'bottom',
};
case ClrPosition.BOTTOM_RIGHT:
default:
return {
originX: 'start',
originY: 'bottom',
};
}
}
function getContentPosition(key) {
switch (key) {
// TOP Positions
case ClrPosition.TOP_LEFT:
return {
overlayX: 'start',
overlayY: 'top',
};
case ClrPosition.TOP_CENTER:
return {
overlayX: 'center',
overlayY: 'top',
};
case ClrPosition.TOP_RIGHT:
return {
overlayX: 'end',
overlayY: 'top',
};
// LEFT Positions
case ClrPosition.LEFT_TOP:
return {
overlayX: 'start',
overlayY: 'top',
};
case ClrPosition.LEFT_CENTER:
return {
overlayX: 'start',
overlayY: 'center',
};
case ClrPosition.LEFT_BOTTOM:
return {
overlayX: 'start',
overlayY: 'bottom',
};
// RIGHT Positions
case ClrPosition.RIGHT_TOP:
return {
overlayX: 'end',
overlayY: 'top',
};
case ClrPosition.RIGHT_CENTER:
return {
overlayX: 'end',
overlayY: 'center',
};
case ClrPosition.RIGHT_BOTTOM:
return {
overlayX: 'end',
overlayY: 'bottom',
};
// BOTTOM positions and default
case ClrPosition.BOTTOM_LEFT:
return {
overlayX: 'start',
overlayY: 'bottom',
};
case ClrPosition.BOTTOM_CENTER:
return {
overlayX: 'center',
overlayY: 'bottom',
};
case ClrPosition.BOTTOM_RIGHT:
default:
return {
overlayX: 'end',
overlayY: 'bottom',
};
}
}
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
/** @dynamic */
class ClrPopoverContent {
constructor(element, container, template, overlayContainer, parent, overlay, popoverService, zone, platformId) {
this.container = container;
this.template = template;
this.parent = parent;
this.overlay = overlay;
this.popoverService = popoverService;
this.zone = zone;
this.platformId = platformId;
this._outsideClickClose = true;
this._scrollToClose = false;
this.popoverType = ClrPopoverType.DEFAULT;
this._availablePositions = [];
this._position = ClrPopoverPosition.BOTTOM_LEFT;
this.subscriptions = [];
this.preferredPositionIsSet = false;
this.availablePositionsAreSet = false;
this._preferredPosition = {
originX: 'start',
originY: 'top',
overlayX: 'end',
overlayY: 'top',
panelClass: ClrPopoverPosition.LEFT_TOP,
};
popoverService.panelClass.push('clr-popover-content');
overlayContainer.getContainerElement().classList.add('clr-overlay-container');
if (!template) {
this.elementRef = element;
}
}
set open(value) {
this.popoverService.open = !!value;
}
get contentAt() {
return this.preferredPositionIsSet ? this._preferredPosition : this._position;
}
set contentAt(position) {
if (typeof position === 'string') {
if (!position || Object.values(ClrPopoverPosition).indexOf(position) === -1) {
return;
}
// set the popover values based on menu position
this._position = position;
this.popoverService.positionChange(this._position);
}
else {
this.preferredPositionIsSet = true;
this._preferredPosition = position;
}
}
set availablePositions(positions) {
this.availablePositionsAreSet = true;
this._availablePositions = positions;
}
set contentType(type) {
this.popoverType = type;
if (!this.availablePositionsAreSet) {
this._availablePositions = getConnectedPositions(type);
}
}
get outsideClickClose() {
return this._outsideClickClose;
}
set outsideClickClose(clickToClose) {
this._outsideClickClose = !!clickToClose;
}
get scrollToClose() {
return this._scrollToClose;
}
set scrollToClose(scrollToClose) {
this._scrollToClose = !!scrollToClose;
}
set contentOrigin(origin) {
if (origin instanceof Element) {
this.popoverService.origin = new ElementRef(origin);
}
else {
this.popoverService.origin = origin;
}
}
get positionStrategy() {
return this.overlay
.position()
.flexibleConnectedTo(this.popoverService.origin)
.setOrigin(this.popoverService.origin)
.withPush(true)
.withPositions([this.preferredPosition, ...this._availablePositions])
.withFlexibleDimensions(true);
}
get preferredPosition() {
if (this.preferredPositionIsSet) {
return this._preferredPosition;
}
// Default position is "bottom-left"
return mapPopoverKeyToPosition(this._position, this.popoverType);
}
ngAfterViewInit() {
if (this.popoverService.open) {
this.showOverlay();
}
this.openCloseSubscription = this.popoverService.openChange.subscribe(change => {
if (change) {
this.showOverlay();
}
else {
this.closePopover();
}
});
}
ngOnDestroy() {
this.removeOverlay();
this.openCloseSubscription?.unsubscribe();
}
_createOverlayRef() {
this.overlayRef = this.overlay.create(new OverlayConfig({
// This is where we can pass externally facing inputs into the angular overlay API, and essentially proxy behaviors our users want directly to the CDK if they have them.
positionStrategy: this.positionStrategy,
// the scrolling behaviour is controlled by this popover content directive
scrollStrategy: this.overlay.scrollStrategies.noop(),
panelClass: this.popoverService.panelClass,
hasBackdrop: false,
}));
this.subscriptions.push(merge(this.popoverService.resetPositionsChange, this.popoverService.getPositionChange()).subscribe(() => {
this.resetPosition();
}), this.popoverService.updatePositionChange().subscribe(() => {
this.overlayRef?.updatePosition();
}), this.overlayRef.keydownEvents().subscribe(event => {
if (event && event.key && event.key === Keys.Escape && !hasModifierKey(event)) {
event.preventDefault();
this.closePopover();
}
}), this.popoverService.originPoint
? this.createPointBasedOutsideClickSubscription()
: this.createElementBasedOutsideClickSubscription());
}
/**
* Point-based origins (context menus) delay the subscription to avoid the
* mouseup from the same right-click that opened the popover.
*/
createPointBasedOutsideClickSubscription() {
return timer(500)
.pipe(switchMap(() => this.overlayRef.outsidePointerEvents()))
.subscribe(event => {
if (this.elementRef?.nativeElement?.contains(event.target)) {
return;
}
if (this._outsideClickClose) {
this.closePopover();
}
});
}
/**
* Element-based origins close on outside clicks and suppress toggle-button
* re-clicks so the popover doesn't immediately reopen.
*/
createElementBasedOutsideClickSubscription() {
return this.overlayRef.outsidePointerEvents().subscribe(event => {
// web components (cds-icon) register as outside pointer events, so if the event target is inside the content panel return early
if (this.elementRef?.nativeElement?.contains(event.target)) {
return;
}
// Check if the same element that opened the popover is the same element triggering the outside pointer events (toggle button)
const isToggleButton = this.popoverService.openEvent &&
(this.popoverService.openEvent.target.contains(event.target) ||
this.popoverService.openEvent.target.parentElement.contains(event.target) ||
this.popoverService.openEvent.target === event.target);
if (isToggleButton) {
event.stopPropagation();
}
if (this._outsideClickClose || isToggleButton) {
this.closePopover();
}
});
}
resetPosition() {
if (this.overlayRef) {
this.overlayRef.updatePositionStrategy(this.positionStrategy);
this.overlayRef.updatePosition();
}
}
closePopover() {
if (!this.overlayRef) {
return;
}
this.removeOverlay();
this.popoverService.open = false;
if (this.popoverService.originElement) {
const shouldFocusTrigger = this.popoverType !== ClrPopoverType.TOOLTIP &&
(document.activeElement === document.body ||
document.activeElement === this.popoverService.originElement.nativeElement);
if (shouldFocusTrigger) {
this.popoverService.focusOrigin();
}
}
}
showOverlay() {
if (!this.overlayRef) {
this._createOverlayRef();
}
if (!this.view && this.template) {
this.view = this.container.createEmbeddedView(this.template);
if (!this.elementRef) {
const [rootNode] = this.view.rootNodes;
this.elementRef = new ElementRef(rootNode); // So we know where/what to set close focus on
}
}
if (!this.domPortal) {
this.domPortal = new DomPortal(this.elementRef);
this.overlayRef.attach(this.domPortal);
}
if (this.popoverService.originElement) {
this.popoverService.originElement.nativeElement.scrollIntoView({
behavior: 'instant',
block: 'nearest',
inline: 'nearest',
});
this.setupIntersectionObserver();
}
setTimeout(() => {
// Get Scrollable Parents
this.listenToScrollEvents();
this.popoverService.popoverVisibleEmit(true);
if (this.elementRef?.nativeElement?.focus) {
this.elementRef.nativeElement.focus();
}
});
}
removeOverlay() {
this.subscriptions.forEach(s => s.unsubscribe());
this.subscriptions = [];
if (this.overlayRef?.hasAttached()) {
this.overlayRef.detach();
this.overlayRef.dispose();
}
if (this.domPortal?.isAttached) {
this.domPortal.detach();
}
if (this.view) {
this.view.destroy();
}
this.overlayRef = null;
this.domPortal = null;
if (this.template) {
this.elementRef = null;
}
this.view = null;
this.intersectionObserver?.disconnect();
this.intersectionObserver = null;
this.popoverService.popoverVisibleEmit(false);
}
getScrollableParents(node) {
let parent = node;
const overflowScrollKeys = ['auto', 'scroll', 'clip'];
const scrollableParents = [window.document];
while (parent && !(parent instanceof HTMLHtmlElement)) {
if (parent instanceof ShadowRoot) {
parent = parent.host;
}
const { overflowY, overflowX } = window.getComputedStyle(parent);
if (overflowScrollKeys.includes(overflowY) || overflowScrollKeys.includes(overflowX)) {
scrollableParents.push(parent);
}
parent = parent.parentNode;
}
return scrollableParents;
}
/**
* Uses IntersectionObserver to detect when the origin element leaves the screen.
* This handles the "Close on Scroll" logic much cheaper than getBoundingClientRect.
*/
setupIntersectionObserver() {
if (!this.popoverService.originElement || this.intersectionObserver) {
return;
}
this.intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
// If the origin is no longer visible (scrolled out of view)
if (!entry.isIntersecting && this.popoverService.open) {
this.zone.run(() => this.closePopover());
}
});
}, { root: null, threshold: 0.8 });
this.intersectionObserver.observe(this.popoverService.originElement.nativeElement);
}
listenToScrollEvents() {
if (!isPlatformBrowser(this.platformId)) {
return;
}
const originEl = this.popoverService.originPoint
? this.popoverService.pointTargetElement
: this.getRootPopover(this)?.popoverService?.originElement?.nativeElement;
this.listenToScrollForElementOrigin(originEl);
}
// Element origins track ancestor scroll containers to reposition or close.
listenToScrollForElementOrigin(originEl) {
const scrollableParents = this.getScrollableParents(originEl);
this.zone.runOutsideAngular(() => {
this.subscriptions.push(merge(...scrollableParents.map(parent => fromEvent(parent, 'scroll', { passive: true }))).subscribe(() => {
if (this._scrollToClose) {
this.zone.run(() => this.closePopover());
return;
}
this.overlayRef?.updatePosition();
}));
});
}
getRootPopover(popover) {
if (popover && popover.parent) {
return this.getRootPopover(popover.parent);
}
return popover;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverContent, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }, { token: i0.TemplateRef, optional: true }, { token: i1.OverlayContainer }, { token: ClrPopoverContent, optional: true, skipSelf: true }, { token: i1.Overlay }, { token: ClrPopoverService }, { token: i0.NgZone }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrPopoverContent, isStandalone: true, selector: "[clrPopoverContent]", inputs: { open: ["clrPopoverContent", "open"], contentAt: ["clrPopoverContentAt", "contentAt"], availablePositions: ["clrPopoverContentAvailablePositions", "availablePositions"], contentType: ["clrPopoverContentType", "contentType"], outsideClickClose: ["clrPopoverContentOutsideClickToClose", "outsideClickClose"], scrollToClose: ["clrPopoverContentScrollToClose", "scrollToClose"], contentOrigin: ["clrPopoverContentOrigin", "contentOrigin"] }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverContent, decorators: [{
type: Directive,
args: [{
selector: '[clrPopoverContent]',
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }, { type: i0.TemplateRef, decorators: [{
type: Optional
}] }, { type: i1.OverlayContainer }, { type: ClrPopoverContent, decorators: [{
type: Optional
}, {
type: SkipSelf
}] }, { type: i1.Overlay }, { type: ClrPopoverService, decorators: [{
type: Inject,
args: [ClrPopoverService]
}] }, { type: i0.NgZone }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }], propDecorators: { open: [{
type: Input,
args: ['clrPopoverContent']
}], contentAt: [{
type: Input,
args: ['clrPopoverContentAt']
}], availablePositions: [{
type: Input,
args: ['clrPopoverContentAvailablePositions']
}], contentType: [{
type: Input,
args: ['clrPopoverContentType']
}], outsideClickClose: [{
type: Input,
args: ['clrPopoverContentOutsideClickToClose']
}], scrollToClose: [{
type: Input,
args: ['clrPopoverContentScrollToClose']
}], contentOrigin: [{
type: Input,
args: ['clrPopoverContentOrigin']
}] } });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ClrPopoverOrigin {
constructor(popoverService, element) {
popoverService.origin = element;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverOrigin, deps: [{ token: ClrPopoverService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrPopoverOrigin, isStandalone: false, selector: "[clrPopoverOrigin]", host: { properties: { "class.clr-popover-origin": "true" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverOrigin, decorators: [{
type: Directive,
args: [{
selector: '[clrPopoverOrigin]',
host: {
'[class.clr-popover-origin]': 'true',
},
standalone: false,
}]
}], ctorParameters: () => [{ type: ClrPopoverService }, { type: i0.ElementRef }] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
const POPOVER_HOST_ORIGIN = new InjectionToken('POPOVER_HOST_ORIGIN');
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ClrStopEscapePropagationDirective {
constructor(popoverService) {
this.popoverService = popoverService;
this.lastOpenChange = null;
}
ngOnInit() {
this.subscription = this.popoverService.openChange.subscribe(open => {
this.lastOpenChange = open;
});
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
onEscapeKey(event) {
if (this.lastOpenChange !== null) {
if (this.lastOpenChange === false) {
event.stopPropagation();
}
this.lastOpenChange = null;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrStopEscapePropagationDirective, deps: [{ token: ClrPopoverService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrStopEscapePropagationDirective, isStandalone: true, host: { listeners: { "keyup.escape": "onEscapeKey($event)" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrStopEscapePropagationDirective, decorators: [{
type: Directive,
args: [{
standalone: true,
}]
}], ctorParameters: () => [{ type: ClrPopoverService }], propDecorators: { onEscapeKey: [{
type: HostListener,
args: ['keyup.escape', ['$event']]
}] } });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ClrPopoverHostDirective {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverHostDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrPopoverHostDirective, isStandalone: true, providers: [ClrPopoverService, { provide: POPOVER_HOST_ORIGIN, useExisting: ElementRef }], hostDirectives: [{ directive: ClrStopEscapePropagationDirective }], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverHostDirective, decorators: [{
type: Directive,
args: [{
standalone: true,
providers: [ClrPopoverService, { provide: POPOVER_HOST_ORIGIN, useExisting: ElementRef }],
hostDirectives: [ClrStopEscapePropagationDirective],
}]
}] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
/**********
*
* @class ClrIfOpen
*
* @description
* A structural directive that controls whether or not the associated TemplateRef is instantiated or not.
* It makes use of a Component instance level service: ClrPopoverService to maintain state between itself and the component
* using it in the component template.
*
*/
class ClrIfOpen {
constructor(popoverService, template, container) {
this.popoverService = popoverService;
this.template = template;
this.container = container;
/**********
* @property openChange
*
* @description
* An event emitter that emits when the open property is set to allow for 2way binding when the directive is
* used with de-structured / de-sugared syntax.
*/
this.openChange = new EventEmitter(false);
this.subscriptions = [];
this.subscriptions.push(popoverService.openChange.subscribe(change => {
// OPEN before overlay is built
if (change) {
container.createEmbeddedView(template);
this.openChange.emit(change);
}
}), popoverService.popoverVisible.subscribe(change => {
// CLOSE after overlay is destroyed
if (!change) {
container.clear();
this.openChange.emit(change);
}
}));
}
/**
* @description
* A property that gets/sets ClrPopoverService.open with value.
*/
get open() {
return this.popoverService.open;
}
set open(value) {
this.popoverService.open = value;
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
/**
* @description
* Function that takes a boolean value and either created an embedded view for the associated ViewContainerRef or,
* Clears all views from the ViewContainerRef
*
* @param value
*/
updateView(value) {
if (value) {
this.container.createEmbeddedView(this.template);
}
else {
this.container.clear();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrIfOpen, deps: [{ token: ClrPopoverService }, { token: i0.TemplateRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrIfOpen, isStandalone: true, selector: "[clrIfOpen]", inputs: { open: ["clrIfOpen", "open"] }, outputs: { openChange: "clrIfOpenChange" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrIfOpen, decorators: [{
type: Directive,
args: [{
selector: '[clrIfOpen]',
}]
}], ctorParameters: () => [{ type: ClrPopoverService }, { type: i0.TemplateRef }, { type: i0.ViewContainerRef }], propDecorators: { openChange: [{
type: Output,
args: ['clrIfOpenChange']
}], open: [{
type: Input,
args: ['clrIfOpen']
}] } });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ClrPopoverCloseButton {
constructor(elementRef, popoverService) {
this.elementRef = elementRef;
this.popoverService = popoverService;
this.closeChange = new EventEmitter();
this.subscriptions = [];
this.subscriptions.push(popoverService.openChange.pipe(filter(value => !value)).subscribe(() => {
this.closeChange.emit();
}));
}
handleClick(event) {
this.popoverService.toggleWithEvent(event);
this.popoverService.focusOrigin();
}
ngAfterViewInit() {
this.popoverService.closeButtonRef = this.elementRef;
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverCloseButton, deps: [{ token: i0.ElementRef }, { token: ClrPopoverService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrPopoverCloseButton, isStandalone: false, selector: "[clrPopoverCloseButton]", outputs: { closeChange: "clrPopoverOnCloseChange" }, host: { listeners: { "click": "handleClick($event)" }, properties: { "class.clr-smart-close-button": "true" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverCloseButton, decorators: [{
type: Directive,
args: [{
selector: '[clrPopoverCloseButton]',
host: {
'[class.clr-smart-close-button]': 'true',
},
standalone: false,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: ClrPopoverService }], propDecorators: { closeChange: [{
type: Output,
args: ['clrPopoverOnCloseChange']
}], handleClick: [{
type: HostListener,
args: ['click', ['$event']]
}] } });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ClrPopoverOpenCloseButton {
constructor(popoverService) {
this.popoverService = popoverService;
this.openCloseChange = new EventEmitter();
this.subscriptions = [];
this.subscriptions.push(popoverService.openChange.subscribe(change => {
this.openCloseChange.emit(change);
}));
}
handleClick(event) {
this.popoverService.toggleWithEvent(event);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverOpenCloseButton, deps: [{ token: ClrPopoverService }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: ClrPopoverOpenCloseButton, isStandalone: false, selector: "[clrPopoverOpenCloseButton]", outputs: { openCloseChange: "clrPopoverOpenCloseChange" }, host: { listeners: { "click": "handleClick($event)" }, properties: { "class.clr-smart-open-close": "true" } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverOpenCloseButton, decorators: [{
type: Directive,
args: [{
selector: '[clrPopoverOpenCloseButton]',
host: {
'[class.clr-smart-open-close]': 'true',
},
standalone: false,
}]
}], ctorParameters: () => [{ type: ClrPopoverService }], propDecorators: { openCloseChange: [{
type: Output,
args: ['clrPopoverOpenCloseChange']
}], handleClick: [{
type: HostListener,
args: ['click', ['$event']]
}] } });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
class ClrPopoverModuleNext {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverModuleNext, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverModuleNext, declarations: [ClrPopoverOrigin, ClrPopoverCloseButton, ClrPopoverOpenCloseButton], imports: [ClrPopoverContent, ClrIfOpen], exports: [ClrPopoverOrigin, ClrPopoverCloseButton, ClrPopoverOpenCloseButton, ClrPopoverContent, ClrIfOpen] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverModuleNext }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ClrPopoverModuleNext, decorators: [{
type: NgModule,
args: [{
imports: [ClrPopoverContent, ClrIfOpen],
declarations: [ClrPopoverOrigin, ClrPopoverCloseButton, ClrPopoverOpenCloseButton],
exports: [ClrPopoverOrigin, ClrPopoverCloseButton, ClrPopoverOpenCloseButton, ClrPopoverContent, ClrIfOpen],
}]
}] });
/*
* Copyright (c) 2016-2026 Broadcom. All Rights Reserved.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
* This software is released under MIT license.
* The full license information can be found in LICENSE in the root directory of this project.
*/
/**
* Generated bundle index. Do not edit.
*/
export { ClrIfOpen, ClrPopoverCloseButton, ClrPopoverContent, ClrPopoverHostDirective, ClrPopoverModuleNext, ClrPopoverOpenCloseButton, ClrPopoverOrigin, ClrPopoverPosition, ClrPopoverService, ClrPopoverType, ClrStopEscapePropagationDirective, DROPDOWN_POSITIONS, POPOVER_HOST_ORIGIN, SIGNPOST_POSITIONS, TOOLTIP_POSITIONS, getConnectedPositions, getContentPosition, getOriginPosition, getPositionsArray, mapPopoverKeyToPosition };
//# sourceMappingURL=clr-angular-popover-common.mjs.map