@ncstate/sat-popover
Version:
Popover component for Angular
1,008 lines (997 loc) • 49.2 kB
JavaScript
import * as i0 from '@angular/core';
import { ElementRef, Optional, Injectable, InjectionToken, Input, Directive, EventEmitter, DOCUMENT, TemplateRef, ViewChild, Output, Inject, ViewEncapsulation, Component, HostListener, NgModule } from '@angular/core';
import * as i3 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i1 from '@angular/cdk/overlay';
import { OverlayConfig, ConnectionPositionPair, OverlayModule } from '@angular/cdk/overlay';
import * as i1$1 from '@angular/cdk/a11y';
import { A11yModule } from '@angular/cdk/a11y';
import * as i2 from '@angular/cdk/bidi';
import { BidiModule } from '@angular/cdk/bidi';
import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
import { trigger, state, transition, style, animate } from '@angular/animations';
import { ESCAPE } from '@angular/cdk/keycodes';
import { TemplatePortal } from '@angular/cdk/portal';
import { Subject, of } from 'rxjs';
import { take, takeUntil, tap, filter, switchMap, delay } from 'rxjs/operators';
const transformPopover = trigger('transformPopover', [
state('enter', style({ opacity: 1, transform: 'scale(1)' }), { params: { startAtScale: 0.3 } }),
state('void, exit', style({ opacity: 0, transform: 'scale({{endAtScale}})' }), { params: { endAtScale: 0.5 } }),
transition('* => enter', [
style({ opacity: 0, transform: 'scale({{endAtScale}})' }),
animate('{{openTransition}}', style({ opacity: 1, transform: 'scale(1)' }))
]),
transition('* => void, * => exit', [
animate('{{closeTransition}}', style({ opacity: 0, transform: 'scale({{endAtScale}})' }))
])
]);
const VALID_SCROLL = ['noop', 'block', 'reposition', 'close'];
const VALID_HORIZ_ALIGN = ['before', 'start', 'center', 'end', 'after'];
const VALID_VERT_ALIGN = ['above', 'start', 'center', 'end', 'below'];
function getUnanchoredPopoverError() {
return Error('SatPopover does not have an anchor.');
}
function getInvalidPopoverAnchorError() {
return Error('SatPopover#anchor must be an instance of SatPopoverAnchor, ElementRef, or HTMLElement.');
}
function getInvalidPopoverError() {
return Error('SatPopoverAnchor#satPopoverAnchor must be an instance of SatPopover.');
}
function getInvalidSatPopoverAnchorError() {
return Error(`SatPopoverAnchor must be associated with a ` +
`SatPopover component. ` +
`Examples: <sat-popover [anchor]="satPopoverAnchorTemplateRef"> or ` +
`<button satPopoverAnchor [satPopoverAnchor]="satPopoverTemplateRef">`);
}
function getInvalidHorizontalAlignError(alignment) {
return Error(generateGenericError('horizontalAlign/xAlign', alignment, VALID_HORIZ_ALIGN));
}
function getInvalidVerticalAlignError(alignment) {
return Error(generateGenericError('verticalAlign/yAlign', alignment, VALID_VERT_ALIGN));
}
function getInvalidScrollStrategyError(strategy) {
return Error(generateGenericError('scrollStrategy', strategy, VALID_SCROLL));
}
function generateGenericError(apiName, invalid, valid) {
return `Invalid ${apiName}: '${invalid}'. Valid options are ${valid.map((v) => `'${v}'`).join(', ')}.`;
}
class SatPopoverAnchoringService {
_overlay;
_ngZone;
_dir;
/** Emits when the popover is opened. */
popoverOpened = new Subject();
/** Emits when the popover is closed. */
popoverClosed = new Subject();
/** Reference to the overlay containing the popover component. */
_overlayRef;
/** Reference to the target popover. */
_popover;
/** Reference to the view container for the popover template. */
_viewContainerRef;
/** Reference to the anchor element. */
_anchor;
/** Reference to a template portal where the overlay will be attached. */
_portal;
/** Single subscription to notifications service events. */
_notificationsSubscription;
/** Single subscription to position changes. */
_positionChangeSubscription;
/** Whether the popover is presently open. */
_popoverOpen = false;
/** Emits when the service is destroyed. */
_onDestroy = new Subject();
constructor(_overlay, _ngZone, _dir) {
this._overlay = _overlay;
this._ngZone = _ngZone;
this._dir = _dir;
}
ngOnDestroy() {
// Destroy popover before terminating subscriptions so that any resulting
// detachments update 'closed state'
this._destroyPopover();
// Terminate subscriptions
if (this._notificationsSubscription) {
this._notificationsSubscription.unsubscribe();
}
if (this._positionChangeSubscription) {
this._positionChangeSubscription.unsubscribe();
}
this._onDestroy.next();
this._onDestroy.complete();
this.popoverOpened.complete();
this.popoverClosed.complete();
}
/** Anchor a popover instance to a view and connection element. */
anchor(popover, viewContainerRef, anchor) {
// If we're just changing the anchor element and the overlayRef already exists,
// simply update the existing _overlayRef's anchor.
if (this._popover === popover && this._viewContainerRef === viewContainerRef && this._overlayRef) {
this._anchor = anchor instanceof ElementRef ? anchor.nativeElement : anchor;
const config = this._overlayRef.getConfig();
const strategy = config.positionStrategy;
strategy.setOrigin(this._anchor);
this._overlayRef.updatePosition();
return;
}
// Destroy any previous popovers
this._destroyPopover();
// Assign local refs
this._popover = popover;
this._viewContainerRef = viewContainerRef;
this._anchor = anchor instanceof ElementRef ? anchor.nativeElement : anchor;
}
/** Gets whether the popover is presently open. */
isPopoverOpen() {
return this._popoverOpen;
}
/** Toggles the popover between the open and closed states. */
togglePopover() {
return this._popoverOpen ? this.closePopover() : this.openPopover();
}
/** Opens the popover. */
openPopover(options = {}) {
if (!this._popoverOpen) {
this._applyOpenOptions(options);
this._createOverlay();
this._subscribeToBackdrop();
this._subscribeToEscape();
this._subscribeToDetachments();
this._saveOpenedState();
}
}
/** Closes the popover. */
closePopover(value) {
if (this._overlayRef) {
this._saveClosedState(value);
this._overlayRef.detach();
}
}
/** TODO: implement when the overlay's position can be dynamically changed */
repositionPopover() {
this.updatePopoverConfig();
}
/** TODO: when the overlay's position can be dynamically changed, do not destroy */
updatePopoverConfig() {
this._destroyPopoverOnceClosed();
}
/** Realign the popover to the anchor. */
realignPopoverToAnchor() {
if (this._overlayRef) {
const config = this._overlayRef.getConfig();
const strategy = config.positionStrategy;
strategy.reapplyLastPosition();
}
}
/** Get a reference to the anchor element. */
getAnchorElement() {
return this._anchor;
}
/** Apply behavior properties on the popover based on the open options. */
_applyOpenOptions(options) {
// Only override restoreFocus as `false` if the option is explicitly `false`
const restoreFocus = options.restoreFocus !== false;
this._popover._restoreFocusOverride = restoreFocus;
// Only override autoFocus as `false` if the option is explicitly `false`
const autoFocus = options.autoFocus !== false;
this._popover._autoFocusOverride = autoFocus;
}
/** Create an overlay to be attached to the portal. */
_createOverlay() {
// Create overlay if it doesn't yet exist
if (!this._overlayRef) {
this._portal = new TemplatePortal(this._popover._templateRef, this._viewContainerRef);
const popoverConfig = {
horizontalAlign: this._popover.horizontalAlign,
verticalAlign: this._popover.verticalAlign,
hasBackdrop: coerceBooleanProperty(this._popover.hasBackdrop),
backdropClass: this._popover.backdropClass,
scrollStrategy: this._popover.scrollStrategy,
forceAlignment: coerceBooleanProperty(this._popover.forceAlignment),
lockAlignment: coerceBooleanProperty(this._popover.lockAlignment),
panelClass: this._popover.panelClass
};
const overlayConfig = this._getOverlayConfig(popoverConfig, this._anchor);
this._subscribeToPositionChanges(overlayConfig.positionStrategy);
this._overlayRef = this._overlay.create(overlayConfig);
}
// Actually open the popover
this._overlayRef.attach(this._portal);
return this._overlayRef;
}
/** Removes the popover from the DOM. Does NOT update open state. */
_destroyPopover() {
if (this._overlayRef) {
this._overlayRef.dispose();
this._overlayRef = null;
}
}
/**
* Destroys the popover immediately if it is closed, or waits until it
* has been closed to destroy it.
*/
_destroyPopoverOnceClosed() {
if (this.isPopoverOpen() && this._overlayRef) {
this._overlayRef
.detachments()
.pipe(take(1), takeUntil(this._onDestroy))
.subscribe(() => this._destroyPopover());
}
else {
this._destroyPopover();
}
}
/** Close popover when backdrop is clicked. */
_subscribeToBackdrop() {
this._overlayRef
.backdropClick()
.pipe(tap(() => this._popover.backdropClicked.emit()), filter(() => this._popover.interactiveClose), takeUntil(this.popoverClosed), takeUntil(this._onDestroy))
.subscribe(() => this.closePopover());
}
/** Close popover when escape keydown event occurs. */
_subscribeToEscape() {
this._overlayRef
.keydownEvents()
.pipe(tap((event) => this._popover.overlayKeydown.emit(event)), filter((event) => event.keyCode === ESCAPE), filter(() => this._popover.interactiveClose), takeUntil(this.popoverClosed), takeUntil(this._onDestroy))
.subscribe(() => this.closePopover());
}
/** Set state back to closed when detached. */
_subscribeToDetachments() {
this._overlayRef
.detachments()
.pipe(takeUntil(this._onDestroy))
.subscribe(() => this._saveClosedState());
}
/** Save the opened state of the popover and emit. */
_saveOpenedState() {
if (!this._popoverOpen) {
this._popover._state = 'enter';
this._popover._open = this._popoverOpen = true;
this.popoverOpened.next();
this._popover.opened.emit();
}
}
/** Save the closed state of the popover and emit. */
_saveClosedState(value) {
if (this._popoverOpen) {
this._popover._open = this._popoverOpen = false;
this._popover._startExitAnimation();
this.popoverClosed.next(value);
this._popover.closed.emit(value);
}
}
/** Gets the text direction of the containing app. */
_getDirection() {
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}
/** Create and return a config for creating the overlay. */
_getOverlayConfig(config, anchor) {
return new OverlayConfig({
positionStrategy: this._getPositionStrategy(config.horizontalAlign, config.verticalAlign, config.forceAlignment, config.lockAlignment, anchor),
hasBackdrop: config.hasBackdrop,
backdropClass: config.backdropClass || 'cdk-overlay-transparent-backdrop',
scrollStrategy: this._getScrollStrategyInstance(config.scrollStrategy),
direction: this._getDirection(),
panelClass: config.panelClass
});
}
/**
* Listen to changes in the position of the overlay and set the correct alignment classes,
* ensuring that the animation origin is correct, even with a fallback position.
*/
_subscribeToPositionChanges(position) {
if (this._positionChangeSubscription) {
this._positionChangeSubscription.unsubscribe();
}
this._positionChangeSubscription = position.positionChanges.pipe(takeUntil(this._onDestroy)).subscribe((change) => {
// Position changes may occur outside the Angular zone
this._ngZone.run(() => {
this._popover._setAlignmentClasses(getHorizontalPopoverAlignment(change.connectionPair.overlayX), getVerticalPopoverAlignment(change.connectionPair.overlayY));
});
});
}
/** Map a scroll strategy string type to an instance of a scroll strategy. */
_getScrollStrategyInstance(strategy) {
switch (strategy) {
case 'block':
return this._overlay.scrollStrategies.block();
case 'reposition':
return this._overlay.scrollStrategies.reposition();
case 'close':
return this._overlay.scrollStrategies.close();
case 'noop':
default:
return this._overlay.scrollStrategies.noop();
}
}
/** Create and return a position strategy based on config provided to the component instance. */
_getPositionStrategy(horizontalTarget, verticalTarget, forceAlignment, lockAlignment, anchor) {
// Attach the overlay at the preferred position
const targetPosition = getPosition(horizontalTarget, verticalTarget);
const positions = [targetPosition];
const strategy = this._overlay
.position()
.flexibleConnectedTo(anchor)
.withFlexibleDimensions(false)
.withPush(false)
.withViewportMargin(0)
.withLockedPosition(lockAlignment);
// Unless the alignment is forced, add fallbacks based on the preferred positions
if (!forceAlignment) {
const fallbacks = this._getFallbacks(horizontalTarget, verticalTarget);
positions.push(...fallbacks);
}
return strategy.withPositions(positions);
}
/** Get fallback positions based around target alignments. */
_getFallbacks(hTarget, vTarget) {
// Determine if the target alignments overlap the anchor
const horizontalOverlapAllowed = hTarget !== 'before' && hTarget !== 'after';
const verticalOverlapAllowed = vTarget !== 'above' && vTarget !== 'below';
// If a target alignment doesn't cover the anchor, don't let any of the fallback alignments
// cover the anchor
const possibleHorizontalAlignments = horizontalOverlapAllowed
? ['before', 'start', 'center', 'end', 'after']
: ['before', 'after'];
const possibleVerticalAlignments = verticalOverlapAllowed
? ['above', 'start', 'center', 'end', 'below']
: ['above', 'below'];
// Create fallbacks for each allowed prioritized fallback alignment combo
const fallbacks = [];
prioritizeAroundTarget(hTarget, possibleHorizontalAlignments).forEach((h) => {
prioritizeAroundTarget(vTarget, possibleVerticalAlignments).forEach((v) => {
fallbacks.push(getPosition(h, v));
});
});
// Remove the first item since it will be the target alignment and isn't considered a fallback
return fallbacks.slice(1, fallbacks.length);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverAnchoringService, deps: [{ token: i1.Overlay }, { token: i0.NgZone }, { token: i2.Directionality, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverAnchoringService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverAnchoringService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.Overlay }, { type: i0.NgZone }, { type: i2.Directionality, decorators: [{
type: Optional
}] }] });
/** Helper function to get a cdk position pair from SatPopover alignments. */
function getPosition(h, v) {
const { originX, overlayX } = getHorizontalConnectionPosPair(h);
const { originY, overlayY } = getVerticalConnectionPosPair(v);
return new ConnectionPositionPair({ originX, originY }, { overlayX, overlayY });
}
/** Helper function to convert an overlay connection position to equivalent popover alignment. */
function getHorizontalPopoverAlignment(h) {
if (h === 'start') {
return 'after';
}
if (h === 'end') {
return 'before';
}
return 'center';
}
/** Helper function to convert an overlay connection position to equivalent popover alignment. */
function getVerticalPopoverAlignment(v) {
if (v === 'top') {
return 'below';
}
if (v === 'bottom') {
return 'above';
}
return 'center';
}
/** Helper function to convert alignment to origin/overlay position pair. */
function getHorizontalConnectionPosPair(h) {
switch (h) {
case 'before':
return { originX: 'start', overlayX: 'end' };
case 'start':
return { originX: 'start', overlayX: 'start' };
case 'end':
return { originX: 'end', overlayX: 'end' };
case 'after':
return { originX: 'end', overlayX: 'start' };
default:
return { originX: 'center', overlayX: 'center' };
}
}
/** Helper function to convert alignment to origin/overlay position pair. */
function getVerticalConnectionPosPair(v) {
switch (v) {
case 'above':
return { originY: 'top', overlayY: 'bottom' };
case 'start':
return { originY: 'top', overlayY: 'top' };
case 'end':
return { originY: 'bottom', overlayY: 'bottom' };
case 'below':
return { originY: 'bottom', overlayY: 'top' };
default:
return { originY: 'center', overlayY: 'center' };
}
}
/**
* Helper function that takes an ordered array options and returns a reorderded
* array around the target item. e.g.:
*
* target: 3; options: [1, 2, 3, 4, 5, 6, 7];
*
* return: [3, 4, 2, 5, 1, 6, 7]
*/
function prioritizeAroundTarget(target, options) {
const targetIndex = options.indexOf(target);
// Set the first item to be the target
const reordered = [target];
// Make left and right stacks where the highest priority item is last
const left = options.slice(0, targetIndex);
const right = options.slice(targetIndex + 1, options.length).reverse();
// Alternate between stacks until one is empty
while (left.length && right.length) {
reordered.push(right.pop());
reordered.push(left.pop());
}
// Flush out right side
while (right.length) {
reordered.push(right.pop());
}
// Flush out left side
while (left.length) {
reordered.push(left.pop());
}
return reordered;
}
// See http://cubic-bezier.com/#.25,.8,.25,1 for reference.
// const DEFAULT_TRANSITION = '200ms cubic-bezier(0.25, 0.8, 0.25, 1)';
const DEFAULT_TRANSITION = new InjectionToken('DefaultTransition');
const DEFAULT_OPEN_ANIMATION_START_SCALE = 0.3;
const DEFAULT_CLOSE_ANIMATION_END_SCALE = 0.5;
class SatPopoverAnchorDirective {
elementRef;
viewContainerRef;
get popover() {
return this._popover;
}
set popover(val) {
if (val instanceof SatPopoverComponent) {
val.anchor = this;
}
else {
// when a directive is added with no arguments,
// angular assigns `''` as the argument
if (val !== '') {
throw getInvalidPopoverError();
}
}
}
/** @internal */
_popover;
constructor(elementRef, viewContainerRef) {
this.elementRef = elementRef;
this.viewContainerRef = viewContainerRef;
}
ngAfterViewInit() {
if (!this.popover) {
throw getInvalidSatPopoverAnchorError();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverAnchorDirective, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.2", type: SatPopoverAnchorDirective, isStandalone: true, selector: "[satPopoverAnchor]", inputs: { popover: ["satPopoverAnchor", "popover"] }, exportAs: ["satPopoverAnchor"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverAnchorDirective, decorators: [{
type: Directive,
args: [{
selector: '[satPopoverAnchor]',
exportAs: 'satPopoverAnchor'
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }], propDecorators: { popover: [{
type: Input,
args: ['satPopoverAnchor']
}] } });
class SatPopoverComponent {
_focusTrapFactory;
_viewContainerRef;
_defaultTransition;
_document;
/** Anchor element. */
get anchor() {
return this._anchor;
}
set anchor(val) {
if (val instanceof SatPopoverAnchorDirective) {
val._popover = this;
this._anchoringService.anchor(this, val.viewContainerRef, val.elementRef);
this._anchor = val;
}
else if (val instanceof ElementRef || val instanceof HTMLElement) {
this._anchoringService.anchor(this, this._viewContainerRef, val);
this._anchor = val;
}
else if (val) {
throw getInvalidPopoverAnchorError();
}
}
_anchor;
/** Alignment of the popover on the horizontal axis. */
get horizontalAlign() {
return this._horizontalAlign;
}
set horizontalAlign(val) {
this._validateHorizontalAlign(val);
if (this._horizontalAlign !== val) {
this._horizontalAlign = val;
this._anchoringService.repositionPopover();
}
}
_horizontalAlign = 'center';
/** Alignment of the popover on the x axis. Alias for `horizontalAlign`. */
get xAlign() {
return this.horizontalAlign;
}
set xAlign(val) {
this.horizontalAlign = val;
}
/** Alignment of the popover on the vertical axis. */
get verticalAlign() {
return this._verticalAlign;
}
set verticalAlign(val) {
this._validateVerticalAlign(val);
if (this._verticalAlign !== val) {
this._verticalAlign = val;
this._anchoringService.repositionPopover();
}
}
_verticalAlign = 'center';
/** Alignment of the popover on the y axis. Alias for `verticalAlign`. */
get yAlign() {
return this.verticalAlign;
}
set yAlign(val) {
this.verticalAlign = val;
}
/** Whether the popover always opens with the specified alignment. */
get forceAlignment() {
return this._forceAlignment;
}
set forceAlignment(val) {
const coercedVal = coerceBooleanProperty(val);
if (this._forceAlignment !== coercedVal) {
this._forceAlignment = coercedVal;
this._anchoringService.repositionPopover();
}
}
_forceAlignment = false;
/**
* Whether the popover's alignment is locked after opening. This prevents the popover
* from changing its alignement when scrolling or changing the size of the viewport.
*/
get lockAlignment() {
return this._lockAlignment;
}
set lockAlignment(val) {
const coercedVal = coerceBooleanProperty(val);
if (this._lockAlignment !== coercedVal) {
this._lockAlignment = coerceBooleanProperty(val);
this._anchoringService.repositionPopover();
}
}
_lockAlignment = false;
/** Whether the first focusable element should be focused on open. */
get autoFocus() {
return this._autoFocus && this._autoFocusOverride;
}
set autoFocus(val) {
this._autoFocus = coerceBooleanProperty(val);
}
_autoFocus = true;
_autoFocusOverride = true;
/** Whether the popover should return focus to the previously focused element after closing. */
get restoreFocus() {
return this._restoreFocus && this._restoreFocusOverride;
}
set restoreFocus(val) {
this._restoreFocus = coerceBooleanProperty(val);
}
_restoreFocus = true;
_restoreFocusOverride = true;
/** How the popover should handle scrolling. */
get scrollStrategy() {
return this._scrollStrategy;
}
set scrollStrategy(val) {
this._validateScrollStrategy(val);
if (this._scrollStrategy !== val) {
this._scrollStrategy = val;
this._anchoringService.updatePopoverConfig();
}
}
_scrollStrategy = 'reposition';
/** Whether the popover should have a backdrop (includes closing on click). */
get hasBackdrop() {
return this._hasBackdrop;
}
set hasBackdrop(val) {
this._hasBackdrop = coerceBooleanProperty(val);
}
_hasBackdrop = false;
/** Whether the popover should close when the user clicks the backdrop or presses ESC. */
get interactiveClose() {
return this._interactiveClose;
}
set interactiveClose(val) {
this._interactiveClose = coerceBooleanProperty(val);
}
_interactiveClose = true;
/** Custom transition to use while opening. */
get openTransition() {
return this._openTransition;
}
set openTransition(val) {
if (val) {
this._openTransition = val;
}
}
_openTransition;
/** Custom transition to use while closing. */
get closeTransition() {
return this._closeTransition;
}
set closeTransition(val) {
if (val) {
this._closeTransition = val;
}
}
_closeTransition;
/** Scale value at the start of the :enter animation. */
get openAnimationStartAtScale() {
return this._openAnimationStartAtScale;
}
set openAnimationStartAtScale(val) {
const coercedVal = coerceNumberProperty(val);
if (!isNaN(coercedVal)) {
this._openAnimationStartAtScale = coercedVal;
}
}
_openAnimationStartAtScale = DEFAULT_OPEN_ANIMATION_START_SCALE;
/** Scale value at the end of the :leave animation */
get closeAnimationEndAtScale() {
return this._closeAnimationEndAtScale;
}
set closeAnimationEndAtScale(val) {
const coercedVal = coerceNumberProperty(val);
if (!isNaN(coercedVal)) {
this._closeAnimationEndAtScale = coercedVal;
}
}
_closeAnimationEndAtScale = DEFAULT_CLOSE_ANIMATION_END_SCALE;
/** Optional backdrop class. */
backdropClass = '';
/** Optional custom class to add to the overlay pane. */
panelClass = '';
/** Emits when the popover is opened. */
opened = new EventEmitter();
/** Emits when the popover is closed. */
closed = new EventEmitter();
/** Emits when the popover has finished opening. */
afterOpen = new EventEmitter();
/** Emits when the popover has finished closing. */
afterClose = new EventEmitter();
/** Emits when the backdrop is clicked. */
backdropClicked = new EventEmitter();
/** Emits when a keydown event is targeted to this popover's overlay. */
overlayKeydown = new EventEmitter();
/** Reference to template so it can be placed within a portal. */
_templateRef;
/** Classes to be added to the popover for setting the correct transform origin. */
_classList = {};
/** Whether the popover is presently open. */
_open = false;
_state = 'enter';
/** @internal */
_anchoringService;
/** Reference to the element to build a focus trap around. */
_focusTrapElement;
/** Reference to the element that was focused before opening. */
_previouslyFocusedElement;
/** Reference to a focus trap around the popover. */
_focusTrap;
constructor(_focusTrapFactory, _anchoringService, _viewContainerRef, _defaultTransition, _document = document) {
this._focusTrapFactory = _focusTrapFactory;
this._viewContainerRef = _viewContainerRef;
this._defaultTransition = _defaultTransition;
this._document = _document;
// `@internal` stripping doesn't seem to work if the property is
// declared inside the constructor
this._anchoringService = _anchoringService;
this._openTransition = _defaultTransition;
this._closeTransition = _defaultTransition;
}
ngOnInit() {
this._setAlignmentClasses();
}
/** Open this popover. */
open(options = {}) {
if (this._anchor) {
this._anchoringService.openPopover(options);
return;
}
throw getUnanchoredPopoverError();
}
/** Close this popover. */
close(value) {
this._anchoringService.closePopover(value);
}
/** Toggle this popover open or closed. */
toggle() {
this._anchoringService.togglePopover();
}
/** Realign the popover to the anchor. */
realign() {
this._anchoringService.realignPopoverToAnchor();
}
/** Gets whether the popover is presently open. */
isOpen() {
return this._open;
}
/** Allows programmatically setting a custom anchor. */
setCustomAnchor(viewContainer, el) {
this._anchor = el;
this._anchoringService.anchor(this, viewContainer, el);
}
/** Gets an animation config with customized (or default) transition values. */
get state() {
return this._state;
}
get params() {
return {
openTransition: this.openTransition,
closeTransition: this.closeTransition,
startAtScale: this.openAnimationStartAtScale,
endAtScale: this.closeAnimationEndAtScale
};
}
/** Callback for when the popover is finished animating in or out. */
_onAnimationDone({ toState }) {
if (toState === 'enter') {
this._trapFocus();
this.afterOpen.emit();
}
else if (toState === 'exit' || toState === 'void') {
this._restoreFocusAndDestroyTrap();
this.afterClose.emit();
}
}
/** Starts the dialog exit animation. */
_startExitAnimation() {
this._state = 'exit';
}
/** Apply alignment classes based on alignment inputs. */
_setAlignmentClasses(horizAlign = this.horizontalAlign, vertAlign = this.verticalAlign) {
this._classList['sat-popover-before'] = horizAlign === 'before' || horizAlign === 'end';
this._classList['sat-popover-after'] = horizAlign === 'after' || horizAlign === 'start';
this._classList['sat-popover-above'] = vertAlign === 'above' || vertAlign === 'end';
this._classList['sat-popover-below'] = vertAlign === 'below' || vertAlign === 'start';
this._classList['sat-popover-center'] = horizAlign === 'center' || vertAlign === 'center';
}
/** Move the focus inside the focus trap and remember where to return later. */
_trapFocus() {
this._savePreviouslyFocusedElement();
// There won't be a focus trap element if the close animation starts before open finishes
if (!this._focusTrapElement) {
return;
}
if (!this._focusTrap && this._focusTrapElement) {
this._focusTrap = this._focusTrapFactory.create(this._focusTrapElement.nativeElement);
}
if (this.autoFocus) {
this._focusTrap.focusInitialElementWhenReady();
}
}
/** Restore focus to the element focused before the popover opened. Also destroy trap. */
_restoreFocusAndDestroyTrap() {
const toFocus = this._previouslyFocusedElement;
// Must check active element is focusable for IE sake
if (toFocus && 'focus' in toFocus && this.restoreFocus) {
this._previouslyFocusedElement.focus();
}
this._previouslyFocusedElement = null;
if (this._focusTrap) {
this._focusTrap.destroy();
this._focusTrap = undefined;
}
}
/** Save a reference to the element focused before the popover was opened. */
_savePreviouslyFocusedElement() {
if (this._document) {
this._previouslyFocusedElement = this._document.activeElement;
}
}
/** Throws an error if the alignment is not a valid horizontalAlign. */
_validateHorizontalAlign(pos) {
if (VALID_HORIZ_ALIGN.indexOf(pos) === -1) {
throw getInvalidHorizontalAlignError(pos);
}
}
/** Throws an error if the alignment is not a valid verticalAlign. */
_validateVerticalAlign(pos) {
if (VALID_VERT_ALIGN.indexOf(pos) === -1) {
throw getInvalidVerticalAlignError(pos);
}
}
/** Throws an error if the scroll strategy is not a valid strategy. */
_validateScrollStrategy(strategy) {
if (VALID_SCROLL.indexOf(strategy) === -1) {
throw getInvalidScrollStrategyError(strategy);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverComponent, deps: [{ token: i1$1.ConfigurableFocusTrapFactory }, { token: SatPopoverAnchoringService }, { token: i0.ViewContainerRef }, { token: DEFAULT_TRANSITION }, { token: DOCUMENT, optional: true }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.2", type: SatPopoverComponent, isStandalone: true, selector: "sat-popover", inputs: { anchor: "anchor", horizontalAlign: "horizontalAlign", xAlign: "xAlign", verticalAlign: "verticalAlign", yAlign: "yAlign", forceAlignment: "forceAlignment", lockAlignment: "lockAlignment", autoFocus: "autoFocus", restoreFocus: "restoreFocus", scrollStrategy: "scrollStrategy", hasBackdrop: "hasBackdrop", interactiveClose: "interactiveClose", openTransition: "openTransition", closeTransition: "closeTransition", openAnimationStartAtScale: "openAnimationStartAtScale", closeAnimationEndAtScale: "closeAnimationEndAtScale", backdropClass: "backdropClass", panelClass: "panelClass" }, outputs: { opened: "opened", closed: "closed", afterOpen: "afterOpen", afterClose: "afterClose", backdropClicked: "backdropClicked", overlayKeydown: "overlayKeydown" }, providers: [SatPopoverAnchoringService], viewQueries: [{ propertyName: "_templateRef", first: true, predicate: TemplateRef, descendants: true, static: true }, { propertyName: "_focusTrapElement", first: true, predicate: ["focusTrapElement"], descendants: true }], ngImport: i0, template: "<ng-template>\n <div\n class=\"sat-popover-container\"\n #focusTrapElement\n [ngClass]=\"_classList\"\n [@transformPopover]=\"{ value: state, params: params }\"\n (@transformPopover.done)=\"_onAnimationDone($event)\"\n >\n <ng-content></ng-content>\n </div>\n</ng-template>\n", styles: [".cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;display:flex;max-width:100%;max-height:100%;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;pointer-events:auto;-webkit-tap-highlight-color:transparent;opacity:0;touch-action:manipulation;z-index:1000;transition:opacity .4s cubic-bezier(.25,.8,.25,1)}@media (prefers-reduced-motion){.cdk-overlay-backdrop{transition-duration:1ms}}.cdk-overlay-backdrop-showing{opacity:1}@media (forced-colors: active){.cdk-overlay-backdrop-showing{opacity:.6}}.cdk-overlay-dark-backdrop{background:#00000052}.cdk-overlay-transparent-backdrop{transition:visibility 1ms linear,opacity 1ms linear;visibility:hidden;opacity:1}.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing,.cdk-high-contrast-active .cdk-overlay-transparent-backdrop{opacity:0;visibility:visible}.cdk-overlay-backdrop-noop-animation{transition:none}.cdk-overlay-connected-position-bounding-box{position:absolute;display:flex;flex-direction:column;min-width:1px;min-height:1px;z-index:1000}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}.sat-popover-container.sat-popover-before.sat-popover-above{transform-origin:right bottom}[dir=rtl] .sat-popover-container.sat-popover-before.sat-popover-above{transform-origin:left bottom}.sat-popover-container.sat-popover-before.sat-popover-center{transform-origin:right center}[dir=rtl] .sat-popover-container.sat-popover-before.sat-popover-center{transform-origin:left center}.sat-popover-container.sat-popover-before.sat-popover-below{transform-origin:right top}[dir=rtl] .sat-popover-container.sat-popover-before.sat-popover-below{transform-origin:left top}.sat-popover-container.sat-popover-center.sat-popover-above{transform-origin:center bottom}.sat-popover-container.sat-popover-center.sat-popover-below{transform-origin:center top}.sat-popover-container.sat-popover-after.sat-popover-above{transform-origin:left bottom}[dir=rtl] .sat-popover-container.sat-popover-after.sat-popover-above{transform-origin:right bottom}.sat-popover-container.sat-popover-after.sat-popover-center{transform-origin:left center}[dir=rtl] .sat-popover-container.sat-popover-after.sat-popover-center{transform-origin:right center}.sat-popover-container.sat-popover-after.sat-popover-below{transform-origin:left top}[dir=rtl] .sat-popover-container.sat-popover-after.sat-popover-below{transform-origin:right top}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], animations: [transformPopover], encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverComponent, decorators: [{
type: Component,
args: [{ animations: [transformPopover], encapsulation: ViewEncapsulation.None, imports: [CommonModule], providers: [SatPopoverAnchoringService], selector: 'sat-popover', template: "<ng-template>\n <div\n class=\"sat-popover-container\"\n #focusTrapElement\n [ngClass]=\"_classList\"\n [@transformPopover]=\"{ value: state, params: params }\"\n (@transformPopover.done)=\"_onAnimationDone($event)\"\n >\n <ng-content></ng-content>\n </div>\n</ng-template>\n", styles: [".cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed;z-index:1000}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{display:flex;position:absolute;z-index:1000}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;display:flex;max-width:100%;max-height:100%;z-index:1000}.cdk-overlay-backdrop{position:absolute;inset:0;pointer-events:auto;-webkit-tap-highlight-color:transparent;opacity:0;touch-action:manipulation;z-index:1000;transition:opacity .4s cubic-bezier(.25,.8,.25,1)}@media (prefers-reduced-motion){.cdk-overlay-backdrop{transition-duration:1ms}}.cdk-overlay-backdrop-showing{opacity:1}@media (forced-colors: active){.cdk-overlay-backdrop-showing{opacity:.6}}.cdk-overlay-dark-backdrop{background:#00000052}.cdk-overlay-transparent-backdrop{transition:visibility 1ms linear,opacity 1ms linear;visibility:hidden;opacity:1}.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing,.cdk-high-contrast-active .cdk-overlay-transparent-backdrop{opacity:0;visibility:visible}.cdk-overlay-backdrop-noop-animation{transition:none}.cdk-overlay-connected-position-bounding-box{position:absolute;display:flex;flex-direction:column;min-width:1px;min-height:1px;z-index:1000}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}.sat-popover-container.sat-popover-before.sat-popover-above{transform-origin:right bottom}[dir=rtl] .sat-popover-container.sat-popover-before.sat-popover-above{transform-origin:left bottom}.sat-popover-container.sat-popover-before.sat-popover-center{transform-origin:right center}[dir=rtl] .sat-popover-container.sat-popover-before.sat-popover-center{transform-origin:left center}.sat-popover-container.sat-popover-before.sat-popover-below{transform-origin:right top}[dir=rtl] .sat-popover-container.sat-popover-before.sat-popover-below{transform-origin:left top}.sat-popover-container.sat-popover-center.sat-popover-above{transform-origin:center bottom}.sat-popover-container.sat-popover-center.sat-popover-below{transform-origin:center top}.sat-popover-container.sat-popover-after.sat-popover-above{transform-origin:left bottom}[dir=rtl] .sat-popover-container.sat-popover-after.sat-popover-above{transform-origin:right bottom}.sat-popover-container.sat-popover-after.sat-popover-center{transform-origin:left center}[dir=rtl] .sat-popover-container.sat-popover-after.sat-popover-center{transform-origin:right center}.sat-popover-container.sat-popover-after.sat-popover-below{transform-origin:left top}[dir=rtl] .sat-popover-container.sat-popover-after.sat-popover-below{transform-origin:right top}\n"] }]
}], ctorParameters: () => [{ type: i1$1.ConfigurableFocusTrapFactory }, { type: SatPopoverAnchoringService }, { type: i0.ViewContainerRef }, { type: undefined, decorators: [{
type: Inject,
args: [DEFAULT_TRANSITION]
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [DOCUMENT]
}] }], propDecorators: { anchor: [{
type: Input
}], horizontalAlign: [{
type: Input
}], xAlign: [{
type: Input
}], verticalAlign: [{
type: Input
}], yAlign: [{
type: Input
}], forceAlignment: [{
type: Input
}], lockAlignment: [{
type: Input
}], autoFocus: [{
type: Input
}], restoreFocus: [{
type: Input
}], scrollStrategy: [{
type: Input
}], hasBackdrop: [{
type: Input
}], interactiveClose: [{
type: Input
}], openTransition: [{
type: Input
}], closeTransition: [{
type: Input
}], openAnimationStartAtScale: [{
type: Input
}], closeAnimationEndAtScale: [{
type: Input
}], backdropClass: [{
type: Input
}], panelClass: [{
type: Input
}], opened: [{
type: Output
}], closed: [{
type: Output
}], afterOpen: [{
type: Output
}], afterClose: [{
type: Output
}], backdropClicked: [{
type: Output
}], overlayKeydown: [{
type: Output
}], _templateRef: [{
type: ViewChild,
args: [TemplateRef, { static: true }]
}], _focusTrapElement: [{
type: ViewChild,
args: ['focusTrapElement']
}] } });
class SatPopoverHoverDirective {
anchor;
/**
* Amount of time to delay (ms) after hovering starts before
* the popover opens. Defaults to 0ms.
*/
get satPopoverHover() {
return this._satPopoverHover;
}
set satPopoverHover(val) {
this._satPopoverHover = coerceNumberProperty(val);
}
_satPopoverHover = 0;
/** Emits when the directive is destroyed. */
_onDestroy = new Subject();
/** Emits when the user's mouse enters the element. */
_onMouseEnter = new Subject();
/** Emits when the user's mouse leaves the element. */
_onMouseLeave = new Subject();
constructor(anchor) {
this.anchor = anchor;
}
ngAfterViewInit() {
// Whenever the user hovers this host element, delay the configured
// amount of time and open the popover. Terminate if the mouse leaves
// the host element before the delay is complete.
this._onMouseEnter
.pipe(switchMap(() => {
return of(null).pipe(delay(this._satPopoverHover || 0), takeUntil(this._onMouseLeave));
}), takeUntil(this._onDestroy))
.subscribe(() => this.anchor.popover.open());
}
ngOnDestroy() {
this._onDestroy.next();
this._onDestroy.complete();
}
showPopover() {
this._onMouseEnter.next();
}
closePopover() {
this._onMouseLeave.next();
this.anchor.popover.close();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverHoverDirective, deps: [{ token: SatPopoverAnchorDirective }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.2", type: SatPopoverHoverDirective, isStandalone: true, selector: "[satPopoverHover]", inputs: { satPopoverHover: "satPopoverHover" }, host: { listeners: { "mouseenter": "showPopover()", "mouseleave": "closePopover()" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverHoverDirective, decorators: [{
type: Directive,
args: [{
selector: '[satPopoverHover]'
}]
}], ctorParameters: () => [{ type: SatPopoverAnchorDirective }], propDecorators: { satPopoverHover: [{
type: Input
}], showPopover: [{
type: HostListener,
args: ['mouseenter']
}], closePopover: [{
type: HostListener,
args: ['mouseleave']
}] } });
class SatPopoverModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverModule, imports: [CommonModule,
OverlayModule,
A11yModule,
BidiModule,
SatPopoverComponent,
SatPopoverAnchorDirective,
SatPopoverHoverDirective], exports: [SatPopoverComponent, SatPopoverAnchorDirective, SatPopoverHoverDirective, BidiModule] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverModule, providers: [
// See http://cubic-bezier.com/#.25,.8,.25,1 for reference.
{ provide: DEFAULT_TRANSITION, useValue: '200ms cubic-bezier(0.25, 0.8, 0.25, 1)' }
], imports: [CommonModule,
OverlayModule,
A11yModule,
BidiModule,
SatPopoverComponent, BidiModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.2", ngImport: i0, type: SatPopoverModule, decorators: [{
type: NgModule,
args: [{
imports: [
CommonModule,
OverlayModule,
A11yModule,
BidiModule,
SatPopoverComponent,
SatPopoverAnchorDirective,
SatPopoverHoverDirective
],
providers: [
// See http://cubic-bezier.com/#.25,.8,.25,1 for reference.
{ provide: DEFAULT_TRANSITION, useValue: '200ms cubic-bezier(0.25, 0.8, 0.25, 1)' }
],
exports: [SatPopoverComponent, SatPopoverAnchorDirective, SatPopoverHoverDirective, BidiModule]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { DEFAULT_TRANSITION, SatPopoverAnchorDirective, SatPopoverAnchoringService, SatPopoverComponent, SatPopoverHoverDirective, SatPopoverModule };
//# sourceMappingURL=ncstate-sat-popover.mjs.map