UNPKG

@ncstate/sat-popover

Version:
989 lines (978 loc) 48.6 kB
import * as i0 from '@angular/core'; import { inject, NgZone, ElementRef, Injectable, InjectionToken, ViewContainerRef, Input, Directive, EventEmitter, DOCUMENT, TemplateRef, ViewChild, Output, ViewEncapsulation, Component, HostListener, provideZoneChangeDetection, NgModule } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import { Overlay, OverlayConfig, ConnectionPositionPair, OverlayModule } from '@angular/cdk/overlay'; import { ConfigurableFocusTrapFactory, A11yModule } from '@angular/cdk/a11y'; import { Directionality, BidiModule } from '@angular/cdk/bidi'; import { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion'; import { trigger, state, style, transition, 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 { /** 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 = null; /** Right-to-left support, if needed */ _dir = inject(Directionality, { optional: true }); /** 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; _ngZone = inject(NgZone); /** Emits when the service is destroyed. */ _onDestroy = new Subject(); _overlay = inject(Overlay); 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) { return; } 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) { return; } 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() { if (!this._overlayRef) { return; } 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() { if (!this._overlayRef) { return; } 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() { if (!this._overlayRef) { return; } 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: "21.2.11", ngImport: i0, type: SatPopoverAnchoringService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SatPopoverAnchoringService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SatPopoverAnchoringService, decorators: [{ type: Injectable }] }); /** 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 = inject(ElementRef); viewContainerRef = inject(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; ngAfterViewInit() { if (!this.popover) { throw getInvalidSatPopoverAnchorError(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SatPopoverAnchorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.11", type: SatPopoverAnchorDirective, isStandalone: true, selector: "[satPopoverAnchor]", inputs: { popover: ["satPopoverAnchor", "popover"] }, exportAs: ["satPopoverAnchor"], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SatPopoverAnchorDirective, decorators: [{ type: Directive, args: [{ selector: '[satPopoverAnchor]', exportAs: 'satPopoverAnchor' }] }], propDecorators: { popover: [{ type: Input, args: ['satPopoverAnchor'] }] } }); class SatPopoverComponent { /** 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 = inject(DEFAULT_TRANSITION); /** Custom transition to use while closing. */ get closeTransition() { return this._closeTransition; } set closeTransition(val) { if (val) { this._closeTransition = val; } } _closeTransition = inject(DEFAULT_TRANSITION); /** 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 = {}; _defaultTransition = inject(DEFAULT_TRANSITION); _document = inject(DOCUMENT, { optional: true }); /** Whether the popover is presently open. */ _open = false; _state = 'enter'; /** @internal */ _anchoringService = inject(SatPopoverAnchoringService); /** 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; _focusTrapFactory = inject(ConfigurableFocusTrapFactory); _viewContainerRef = inject(ViewContainerRef); 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(event) { const toState = event?.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) { if (this._focusTrap) { 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) { toFocus.focus(); } this._previouslyFocusedElement = undefined; 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: "21.2.11", ngImport: i0, type: SatPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.11", 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}.cdk-overlay-popover{background:none;border:none;padding:0;outline:0;overflow:visible;position:fixed;pointer-events:none;white-space:normal;color:inherit;text-decoration:none;width:100%;height:100%;inset:0 auto auto 0}.cdk-overlay-popover::backdrop{display:none}.cdk-overlay-popover .cdk-overlay-backdrop{position:fixed;z-index:auto}.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: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], animations: [transformPopover], encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", 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}.cdk-overlay-popover{background:none;border:none;padding:0;outline:0;overflow:visible;position:fixed;pointer-events:none;white-space:normal;color:inherit;text-decoration:none;width:100%;height:100%;inset:0 auto auto 0}.cdk-overlay-popover::backdrop{display:none}.cdk-overlay-popover .cdk-overlay-backdrop{position:fixed;z-index:auto}.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"] }] }], 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 = inject(SatPopoverAnchorDirective); /** * 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(); 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: "21.2.11", ngImport: i0, type: SatPopoverHoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.11", type: SatPopoverHoverDirective, isStandalone: true, selector: "[satPopoverHover]", inputs: { satPopoverHover: "satPopoverHover" }, host: { listeners: { "mouseenter": "showPopover()", "mouseleave": "closePopover()" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: SatPopoverHoverDirective, decorators: [{ type: Directive, args: [{ selector: '[satPopoverHover]' }] }], 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: "21.2.11", ngImport: i0, type: SatPopoverModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.11", 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: "21.2.11", 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)' }, provideZoneChangeDetection() ], imports: [CommonModule, OverlayModule, A11yModule, BidiModule, SatPopoverComponent, BidiModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", 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)' }, provideZoneChangeDetection() ], 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