@firestitch/popover 18.0.2 → 18.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,17 @@
1
1
  import { OverlayConfig } from '@angular/cdk/overlay';
2
2
  import { createPopupPositionStrategy } from './create-position-strategy';
3
- export function createOverlayRef(el, overlay, position) {
4
- const config = {
3
+ /**
4
+ * Hover and click popovers have no backdrop. A panel gets a transparent backdrop, so a click
5
+ * anywhere outside it closes it, and the `fs-popover-panel` class on its pane.
6
+ */
7
+ export function createOverlayRef(el, overlay, position, panel = false) {
8
+ const overlayConfig = new OverlayConfig({
5
9
  positionStrategy: createPopupPositionStrategy(el, overlay, position),
6
10
  scrollStrategy: overlay.scrollStrategies.reposition(),
7
- hasBackdrop: false
8
- };
9
- const overlayConfig = new OverlayConfig(config);
11
+ hasBackdrop: panel,
12
+ backdropClass: panel ? 'cdk-overlay-transparent-backdrop' : undefined,
13
+ panelClass: panel ? 'fs-popover-panel' : undefined,
14
+ });
10
15
  return overlay.create(overlayConfig);
11
16
  }
12
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLW92ZXJsYXktcmVmLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2FwcC9oZWxwZXJzL2NyZWF0ZS1vdmVybGF5LXJlZi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQVcsYUFBYSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDOUQsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFJekUsTUFBTSxVQUFVLGdCQUFnQixDQUFDLEVBQWMsRUFBRSxPQUFnQixFQUFFLFFBQWtCO0lBQ25GLE1BQU0sTUFBTSxHQUFHO1FBQ2IsZ0JBQWdCLEVBQUUsMkJBQTJCLENBQUMsRUFBRSxFQUFFLE9BQU8sRUFBRSxRQUFRLENBQUM7UUFDcEUsY0FBYyxFQUFFLE9BQU8sQ0FBQyxnQkFBZ0IsQ0FBQyxVQUFVLEVBQUU7UUFDckQsV0FBVyxFQUFFLEtBQUs7S0FDbkIsQ0FBQztJQUVGLE1BQU0sYUFBYSxHQUFHLElBQUksYUFBYSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRWhELE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsQ0FBQztBQUN2QyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgRWxlbWVudFJlZiB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuaW1wb3J0IHsgT3ZlcmxheSwgT3ZlcmxheUNvbmZpZyB9IGZyb20gJ0Bhbmd1bGFyL2Nkay9vdmVybGF5JztcbmltcG9ydCB7IGNyZWF0ZVBvcHVwUG9zaXRpb25TdHJhdGVneSB9IGZyb20gJy4vY3JlYXRlLXBvc2l0aW9uLXN0cmF0ZWd5JztcbmltcG9ydCB7IFBvc2l0aW9uIH0gZnJvbSAnLi8uLi9lbnVtcy9wb3NpdGlvbic7XG5cblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU92ZXJsYXlSZWYoZWw6IEVsZW1lbnRSZWYsIG92ZXJsYXk6IE92ZXJsYXksIHBvc2l0aW9uOiBQb3NpdGlvbikge1xuICBjb25zdCBjb25maWcgPSB7XG4gICAgcG9zaXRpb25TdHJhdGVneTogY3JlYXRlUG9wdXBQb3NpdGlvblN0cmF0ZWd5KGVsLCBvdmVybGF5LCBwb3NpdGlvbiksXG4gICAgc2Nyb2xsU3RyYXRlZ3k6IG92ZXJsYXkuc2Nyb2xsU3RyYXRlZ2llcy5yZXBvc2l0aW9uKCksXG4gICAgaGFzQmFja2Ryb3A6IGZhbHNlXG4gIH07XG5cbiAgY29uc3Qgb3ZlcmxheUNvbmZpZyA9IG5ldyBPdmVybGF5Q29uZmlnKGNvbmZpZyk7XG5cbiAgcmV0dXJuIG92ZXJsYXkuY3JlYXRlKG92ZXJsYXlDb25maWcpO1xufVxuIl19
17
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlLW92ZXJsYXktcmVmLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2FwcC9oZWxwZXJzL2NyZWF0ZS1vdmVybGF5LXJlZi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFFQSxPQUFPLEVBQVcsYUFBYSxFQUFjLE1BQU0sc0JBQXNCLENBQUM7QUFHMUUsT0FBTyxFQUFFLDJCQUEyQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFHekU7OztHQUdHO0FBQ0gsTUFBTSxVQUFVLGdCQUFnQixDQUM5QixFQUFjLEVBQ2QsT0FBZ0IsRUFDaEIsUUFBa0IsRUFDbEIsS0FBSyxHQUFHLEtBQUs7SUFFYixNQUFNLGFBQWEsR0FBRyxJQUFJLGFBQWEsQ0FBQztRQUN0QyxnQkFBZ0IsRUFBRSwyQkFBMkIsQ0FBQyxFQUFFLEVBQUUsT0FBTyxFQUFFLFFBQVEsQ0FBQztRQUNwRSxjQUFjLEVBQUUsT0FBTyxDQUFDLGdCQUFnQixDQUFDLFVBQVUsRUFBRTtRQUNyRCxXQUFXLEVBQUUsS0FBSztRQUNsQixhQUFhLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxrQ0FBa0MsQ0FBQyxDQUFDLENBQUMsU0FBUztRQUNyRSxVQUFVLEVBQUUsS0FBSyxDQUFDLENBQUMsQ0FBQyxrQkFBa0IsQ0FBQyxDQUFDLENBQUMsU0FBUztLQUNuRCxDQUFDLENBQUM7SUFFSCxPQUFPLE9BQU8sQ0FBQyxNQUFNLENBQUMsYUFBYSxDQUFDLENBQUM7QUFDdkMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEVsZW1lbnRSZWYgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcblxuaW1wb3J0IHsgT3ZlcmxheSwgT3ZlcmxheUNvbmZpZywgT3ZlcmxheVJlZiB9IGZyb20gJ0Bhbmd1bGFyL2Nkay9vdmVybGF5JztcblxuaW1wb3J0IHsgUG9zaXRpb24gfSBmcm9tICcuLy4uL2VudW1zL3Bvc2l0aW9uJztcbmltcG9ydCB7IGNyZWF0ZVBvcHVwUG9zaXRpb25TdHJhdGVneSB9IGZyb20gJy4vY3JlYXRlLXBvc2l0aW9uLXN0cmF0ZWd5JztcblxuXG4vKipcbiAqIEhvdmVyIGFuZCBjbGljayBwb3BvdmVycyBoYXZlIG5vIGJhY2tkcm9wLiBBIHBhbmVsIGdldHMgYSB0cmFuc3BhcmVudCBiYWNrZHJvcCwgc28gYSBjbGlja1xuICogYW55d2hlcmUgb3V0c2lkZSBpdCBjbG9zZXMgaXQsIGFuZCB0aGUgYGZzLXBvcG92ZXItcGFuZWxgIGNsYXNzIG9uIGl0cyBwYW5lLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlT3ZlcmxheVJlZihcbiAgZWw6IEVsZW1lbnRSZWYsXG4gIG92ZXJsYXk6IE92ZXJsYXksXG4gIHBvc2l0aW9uOiBQb3NpdGlvbixcbiAgcGFuZWwgPSBmYWxzZSxcbik6IE92ZXJsYXlSZWYge1xuICBjb25zdCBvdmVybGF5Q29uZmlnID0gbmV3IE92ZXJsYXlDb25maWcoe1xuICAgIHBvc2l0aW9uU3RyYXRlZ3k6IGNyZWF0ZVBvcHVwUG9zaXRpb25TdHJhdGVneShlbCwgb3ZlcmxheSwgcG9zaXRpb24pLFxuICAgIHNjcm9sbFN0cmF0ZWd5OiBvdmVybGF5LnNjcm9sbFN0cmF0ZWdpZXMucmVwb3NpdGlvbigpLFxuICAgIGhhc0JhY2tkcm9wOiBwYW5lbCxcbiAgICBiYWNrZHJvcENsYXNzOiBwYW5lbCA/ICdjZGstb3ZlcmxheS10cmFuc3BhcmVudC1iYWNrZHJvcCcgOiB1bmRlZmluZWQsXG4gICAgcGFuZWxDbGFzczogcGFuZWwgPyAnZnMtcG9wb3Zlci1wYW5lbCcgOiB1bmRlZmluZWQsXG4gIH0pO1xuXG4gIHJldHVybiBvdmVybGF5LmNyZWF0ZShvdmVybGF5Q29uZmlnKTtcbn1cbiJdfQ==
@@ -0,0 +1,24 @@
1
+ /**
2
+ * True when an overlay with a backdrop was attached after this one and still shows content:
3
+ * a calendar, a select, a menu or a dialog opened from inside it. Overlays without a backdrop
4
+ * (tooltips, toasts, hover popovers) don't count.
5
+ *
6
+ * CDK inserts each backdrop right before its overlay's host, and later overlays after earlier
7
+ * ones, so everything after this overlay's host was attached later.
8
+ */
9
+ export function hasOverlayAbove(overlayRef) {
10
+ let element = overlayRef.hostElement?.nextElementSibling;
11
+ while (element) {
12
+ if (isShowingBackdropOverlay(element)) {
13
+ return true;
14
+ }
15
+ element = element.nextElementSibling;
16
+ }
17
+ return false;
18
+ }
19
+ function isShowingBackdropOverlay(host) {
20
+ const pane = host.querySelector(':scope > .cdk-overlay-pane');
21
+ return !!pane?.childElementCount
22
+ && !!host.previousElementSibling?.classList.contains('cdk-overlay-backdrop');
23
+ }
24
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGFzLW92ZXJsYXktYWJvdmUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvYXBwL2hlbHBlcnMvaGFzLW92ZXJsYXktYWJvdmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBR0E7Ozs7Ozs7R0FPRztBQUNILE1BQU0sVUFBVSxlQUFlLENBQUMsVUFBc0I7SUFDcEQsSUFBSSxPQUFPLEdBQUcsVUFBVSxDQUFDLFdBQVcsRUFBRSxrQkFBa0IsQ0FBQztJQUV6RCxPQUFPLE9BQU8sRUFBRSxDQUFDO1FBQ2YsSUFBSSx3QkFBd0IsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDO1lBQ3RDLE9BQU8sSUFBSSxDQUFDO1FBQ2QsQ0FBQztRQUVELE9BQU8sR0FBRyxPQUFPLENBQUMsa0JBQWtCLENBQUM7SUFDdkMsQ0FBQztJQUVELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELFNBQVMsd0JBQXdCLENBQUMsSUFBYTtJQUM3QyxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsYUFBYSxDQUFDLDRCQUE0QixDQUFDLENBQUM7SUFFOUQsT0FBTyxDQUFDLENBQUMsSUFBSSxFQUFFLGlCQUFpQjtXQUMzQixDQUFDLENBQUMsSUFBSSxDQUFDLHNCQUFzQixFQUFFLFNBQVMsQ0FBQyxRQUFRLENBQUMsc0JBQXNCLENBQUMsQ0FBQztBQUNqRixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgT3ZlcmxheVJlZiB9IGZyb20gJ0Bhbmd1bGFyL2Nkay9vdmVybGF5JztcblxuXG4vKipcbiAqIFRydWUgd2hlbiBhbiBvdmVybGF5IHdpdGggYSBiYWNrZHJvcCB3YXMgYXR0YWNoZWQgYWZ0ZXIgdGhpcyBvbmUgYW5kIHN0aWxsIHNob3dzIGNvbnRlbnQ6XG4gKiBhIGNhbGVuZGFyLCBhIHNlbGVjdCwgYSBtZW51IG9yIGEgZGlhbG9nIG9wZW5lZCBmcm9tIGluc2lkZSBpdC4gT3ZlcmxheXMgd2l0aG91dCBhIGJhY2tkcm9wXG4gKiAodG9vbHRpcHMsIHRvYXN0cywgaG92ZXIgcG9wb3ZlcnMpIGRvbid0IGNvdW50LlxuICpcbiAqIENESyBpbnNlcnRzIGVhY2ggYmFja2Ryb3AgcmlnaHQgYmVmb3JlIGl0cyBvdmVybGF5J3MgaG9zdCwgYW5kIGxhdGVyIG92ZXJsYXlzIGFmdGVyIGVhcmxpZXJcbiAqIG9uZXMsIHNvIGV2ZXJ5dGhpbmcgYWZ0ZXIgdGhpcyBvdmVybGF5J3MgaG9zdCB3YXMgYXR0YWNoZWQgbGF0ZXIuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBoYXNPdmVybGF5QWJvdmUob3ZlcmxheVJlZjogT3ZlcmxheVJlZik6IGJvb2xlYW4ge1xuICBsZXQgZWxlbWVudCA9IG92ZXJsYXlSZWYuaG9zdEVsZW1lbnQ/Lm5leHRFbGVtZW50U2libGluZztcblxuICB3aGlsZSAoZWxlbWVudCkge1xuICAgIGlmIChpc1Nob3dpbmdCYWNrZHJvcE92ZXJsYXkoZWxlbWVudCkpIHtcbiAgICAgIHJldHVybiB0cnVlO1xuICAgIH1cblxuICAgIGVsZW1lbnQgPSBlbGVtZW50Lm5leHRFbGVtZW50U2libGluZztcbiAgfVxuXG4gIHJldHVybiBmYWxzZTtcbn1cblxuZnVuY3Rpb24gaXNTaG93aW5nQmFja2Ryb3BPdmVybGF5KGhvc3Q6IEVsZW1lbnQpOiBib29sZWFuIHtcbiAgY29uc3QgcGFuZSA9IGhvc3QucXVlcnlTZWxlY3RvcignOnNjb3BlID4gLmNkay1vdmVybGF5LXBhbmUnKTtcblxuICByZXR1cm4gISFwYW5lPy5jaGlsZEVsZW1lbnRDb3VudFxuICAgICYmICEhaG9zdC5wcmV2aW91c0VsZW1lbnRTaWJsaW5nPy5jbGFzc0xpc3QuY29udGFpbnMoJ2Nkay1vdmVybGF5LWJhY2tkcm9wJyk7XG59XG4iXX0=
@@ -1,18 +1,21 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, NgZone, Renderer2, Component, ChangeDetectionStrategy, ViewChild, Injector, TemplateRef, Injectable, Directive, Input, HostBinding, NgModule } from '@angular/core';
2
+ import { inject, ElementRef, NgZone, Renderer2, Component, ChangeDetectionStrategy, ViewChild, Injector, TemplateRef, Injectable, input, booleanAttribute, output, Directive, Input, HostBinding, NgModule } from '@angular/core';
3
+ import { NgClass, AsyncPipe, DOCUMENT } from '@angular/common';
3
4
  import { Router, NavigationEnd } from '@angular/router';
4
- import { guid } from '@firestitch/common';
5
- import { BehaviorSubject, combineLatest, Subject, fromEvent, timer, pipe, merge } from 'rxjs';
6
- import { map, filter, takeUntil, debounceTime, delay, tap, switchMap, mapTo, startWith, distinctUntilChanged, withLatestFrom, finalize } from 'rxjs/operators';
5
+ import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y';
6
+ import { hasModifierKey } from '@angular/cdk/keycodes';
7
7
  import { OverlayConfig, Overlay } from '@angular/cdk/overlay';
8
8
  import { BasePortalOutlet, CdkPortalOutlet, TemplatePortal, ComponentPortal } from '@angular/cdk/portal';
9
- import { NgClass, AsyncPipe } from '@angular/common';
9
+ import { guid } from '@firestitch/common';
10
+ import { BehaviorSubject, Subject, combineLatest, fromEvent, timer, pipe, merge } from 'rxjs';
11
+ import { map, filter, takeUntil, debounceTime, delay, tap, switchMap, mapTo, startWith, distinctUntilChanged, withLatestFrom, finalize, take } from 'rxjs/operators';
10
12
 
11
13
  class FsPopoverRef {
12
14
  overlayRef;
13
15
  _autoShow$ = new BehaviorSubject(true);
14
16
  _autoClose$ = new BehaviorSubject(true);
15
17
  _componentLoading$ = new BehaviorSubject(true);
18
+ _closed$ = new Subject();
16
19
  _maxWidth;
17
20
  _maxHeight;
18
21
  _wrapperClass;
@@ -76,6 +79,13 @@ class FsPopoverRef {
76
79
  get maxHeight() {
77
80
  return this._maxHeight;
78
81
  }
82
+ /**
83
+ * Emits after each close(). A panel listens to it, so content that calls
84
+ * `popover.close()` closes the panel the same way Escape or an outside click does.
85
+ */
86
+ get closed$() {
87
+ return this._closed$.asObservable();
88
+ }
79
89
  show() {
80
90
  setTimeout(() => {
81
91
  this._componentLoading$.next(false);
@@ -87,6 +97,7 @@ class FsPopoverRef {
87
97
  close() {
88
98
  this.overlayRef.detach();
89
99
  this._componentLoading$.next(true);
100
+ this._closed$.next();
90
101
  }
91
102
  updatePosition() {
92
103
  this.overlayRef.updatePosition();
@@ -104,18 +115,6 @@ class FsPopoverRef {
104
115
  }
105
116
  }
106
117
 
107
- var Position;
108
- (function (Position) {
109
- Position["North"] = "north";
110
- Position["South"] = "south";
111
- Position["East"] = "east";
112
- Position["West"] = "west";
113
- })(Position || (Position = {}));
114
-
115
- function pointInRect(x1, y1, x2, y2, px, py) {
116
- return (px > x1 && px < x2) && (py > y1 && py < y2);
117
- }
118
-
119
118
  class FsPopoverWrapperComponent extends BasePortalOutlet {
120
119
  popoverRef = inject(FsPopoverRef);
121
120
  _el = inject(ElementRef);
@@ -237,6 +236,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
237
236
  args: ['spinner']
238
237
  }] } });
239
238
 
239
+ var Position;
240
+ (function (Position) {
241
+ Position["North"] = "north";
242
+ Position["South"] = "south";
243
+ Position["East"] = "east";
244
+ Position["West"] = "west";
245
+ })(Position || (Position = {}));
246
+
247
+ function createInjector(popoverRef, parentInjector) {
248
+ const providers = [
249
+ { provide: FsPopoverRef, useValue: popoverRef },
250
+ ];
251
+ return Injector.create({
252
+ providers,
253
+ parent: parentInjector,
254
+ });
255
+ }
256
+
240
257
  const POSITIONS = {
241
258
  [Position.North]: {
242
259
  originX: 'center',
@@ -296,30 +313,53 @@ function createBasePopupPositionStrategy(el, overlay) {
296
313
  .withViewportMargin(4);
297
314
  }
298
315
 
299
- function createOverlayRef(el, overlay, position) {
300
- const config = {
316
+ /**
317
+ * Hover and click popovers have no backdrop. A panel gets a transparent backdrop, so a click
318
+ * anywhere outside it closes it, and the `fs-popover-panel` class on its pane.
319
+ */
320
+ function createOverlayRef(el, overlay, position, panel = false) {
321
+ const overlayConfig = new OverlayConfig({
301
322
  positionStrategy: createPopupPositionStrategy(el, overlay, position),
302
323
  scrollStrategy: overlay.scrollStrategies.reposition(),
303
- hasBackdrop: false
304
- };
305
- const overlayConfig = new OverlayConfig(config);
306
- return overlay.create(overlayConfig);
307
- }
308
-
309
- function createInjector(popoverRef, parentInjector) {
310
- const providers = [
311
- { provide: FsPopoverRef, useValue: popoverRef },
312
- ];
313
- return Injector.create({
314
- providers,
315
- parent: parentInjector,
324
+ hasBackdrop: panel,
325
+ backdropClass: panel ? 'cdk-overlay-transparent-backdrop' : undefined,
326
+ panelClass: panel ? 'fs-popover-panel' : undefined,
316
327
  });
328
+ return overlay.create(overlayConfig);
317
329
  }
318
330
 
319
331
  function createTempatePortal(template, popoverRef, data) {
320
332
  return new TemplatePortal(template, void 0, { data: data, popover: popoverRef });
321
333
  }
322
334
 
335
+ /**
336
+ * True when an overlay with a backdrop was attached after this one and still shows content:
337
+ * a calendar, a select, a menu or a dialog opened from inside it. Overlays without a backdrop
338
+ * (tooltips, toasts, hover popovers) don't count.
339
+ *
340
+ * CDK inserts each backdrop right before its overlay's host, and later overlays after earlier
341
+ * ones, so everything after this overlay's host was attached later.
342
+ */
343
+ function hasOverlayAbove(overlayRef) {
344
+ let element = overlayRef.hostElement?.nextElementSibling;
345
+ while (element) {
346
+ if (isShowingBackdropOverlay(element)) {
347
+ return true;
348
+ }
349
+ element = element.nextElementSibling;
350
+ }
351
+ return false;
352
+ }
353
+ function isShowingBackdropOverlay(host) {
354
+ const pane = host.querySelector(':scope > .cdk-overlay-pane');
355
+ return !!pane?.childElementCount
356
+ && !!host.previousElementSibling?.classList.contains('cdk-overlay-backdrop');
357
+ }
358
+
359
+ function pointInRect(x1, y1, x2, y2, px, py) {
360
+ return (px > x1 && px < x2) && (py > y1 && py < y2);
361
+ }
362
+
323
363
  class FsPopoverService {
324
364
  _injector = inject(Injector);
325
365
  _overlay = inject(Overlay);
@@ -394,6 +434,17 @@ class FsPopoverDirective {
394
434
  size = 'normal';
395
435
  trigger = 'mouseover';
396
436
  enabled = true;
437
+ /**
438
+ * Panel mode: a host click or open() opens it; it stays open until close(), an outside
439
+ * click, Escape (only when no later overlay with a backdrop sits above it) or `popover.close()`
440
+ * from the content. Focus is trapped inside and returns on close to the element that had it
441
+ * when the panel opened. `trigger`, `showDelay`, `leaveDelay` and `autoClose` don't apply.
442
+ */
443
+ panel = input(false, { transform: booleanAttribute });
444
+ /**
445
+ * Emits each time the panel closes.
446
+ */
447
+ closed = output();
397
448
  _initialized = false;
398
449
  _popoverRef;
399
450
  _wrapperElement;
@@ -408,6 +459,15 @@ class FsPopoverDirective {
408
459
  _ngZone = inject(NgZone);
409
460
  _router = inject(Router, { optional: true });
410
461
  _guid = guid('xxxxxxx');
462
+ _overlay = inject(Overlay);
463
+ _injector = inject(Injector);
464
+ _focusTrapFactory = inject(ConfigurableFocusTrapFactory);
465
+ _document = inject(DOCUMENT);
466
+ _panelOverlayRef;
467
+ _panelFocusTrap;
468
+ _panelOpener;
469
+ _panelTopmostKeydown;
470
+ _panelClosed$ = new Subject();
411
471
  constructor() {
412
472
  this._mouseEnter$ = fromEvent(this._elRef.nativeElement, 'mouseenter');
413
473
  this._mouseMove$ = fromEvent(document, 'mousemove', { passive: true });
@@ -432,9 +492,33 @@ class FsPopoverDirective {
432
492
  }
433
493
  }
434
494
  ngOnDestroy() {
495
+ if (this._panelOverlayRef) {
496
+ this._finishPanel(false);
497
+ }
435
498
  this._popoverClosed$.next(null);
436
499
  this._destroy$.next(null);
437
500
  }
501
+ /**
502
+ * Opens the panel on this element. Panel mode only.
503
+ */
504
+ open() {
505
+ if (!this.panel() || !this.enabled || !this._initialized || this._panelOverlayRef) {
506
+ return;
507
+ }
508
+ this._ngZone.run(() => this._openPanel());
509
+ }
510
+ /**
511
+ * Closes the panel, or in hover and click modes the open popover.
512
+ */
513
+ close() {
514
+ if (!this.panel()) {
515
+ this._closePopover();
516
+ return;
517
+ }
518
+ if (this._panelOverlayRef) {
519
+ this._ngZone.run(() => this._popoverRef.close());
520
+ }
521
+ }
438
522
  _closePopover() {
439
523
  this._popoverClosed$.next(null);
440
524
  this._ngZone.run(() => {
@@ -460,7 +544,10 @@ class FsPopoverDirective {
460
544
  theme: this.theme,
461
545
  });
462
546
  this._ngZone.runOutsideAngular(() => {
463
- if (this.trigger === 'click') {
547
+ if (this.panel()) {
548
+ this._listenPanelHostClick();
549
+ }
550
+ else if (this.trigger === 'click') {
464
551
  this._listenMouseHostClick();
465
552
  }
466
553
  else {
@@ -477,7 +564,7 @@ class FsPopoverDirective {
477
564
  }), pipe(takeUntil(this._destroy$
478
565
  .pipe(delay(200)))))
479
566
  .subscribe(() => {
480
- this._closePopover();
567
+ this.close();
481
568
  });
482
569
  }
483
570
  _listenMouseHostClick() {
@@ -536,13 +623,108 @@ class FsPopoverDirective {
536
623
  const pointInPopoverRect = pointInRect(popoverBounds.x, popoverBounds.y, popoverBounds.x + popoverBounds.width, popoverBounds.y + popoverBounds.height, event.x, event.y);
537
624
  return !pointInHostRect && !pointInPopoverRect;
538
625
  }
626
+ _listenPanelHostClick() {
627
+ fromEvent(this._elRef.nativeElement, 'click')
628
+ .pipe(filter(() => this.enabled), tap((event) => {
629
+ event.stopPropagation();
630
+ this.open();
631
+ }), takeUntil(this._destroy$))
632
+ .subscribe();
633
+ }
634
+ _openPanel() {
635
+ this._panelOpener = this._document.activeElement;
636
+ this._panelOverlayRef = createOverlayRef(this._elRef, this._overlay, this.position, true);
637
+ this._popoverRef.overlayRef = this._panelOverlayRef;
638
+ const wrapperElement = this._attachPanelContent(this._panelOverlayRef);
639
+ this._trapPanelFocus(wrapperElement);
640
+ this._listenPanelClose(this._panelOverlayRef);
641
+ this._listenPanelEscape(this._panelOverlayRef);
642
+ }
643
+ _attachPanelContent(overlayRef) {
644
+ const wrapperRef = overlayRef.attach(new ComponentPortal(FsPopoverWrapperComponent, null, createInjector(this._popoverRef, this._injector)));
645
+ if (this.template) {
646
+ wrapperRef.instance
647
+ .attachTemplatePortal(createTempatePortal(this.template, this._popoverRef, this.data));
648
+ }
649
+ else {
650
+ wrapperRef.instance.setTextualContent(this.text);
651
+ }
652
+ return wrapperRef.location.nativeElement;
653
+ }
654
+ /**
655
+ * The configurable trap also pulls focus back when it lands outside the panel, for
656
+ * example on the first Tab after a calendar inside closed and took the focused element
657
+ * with it. Focus inside other overlays (a select, a calendar) is left alone.
658
+ */
659
+ _trapPanelFocus(wrapperElement) {
660
+ this._panelFocusTrap = this._focusTrapFactory.create(wrapperElement);
661
+ this._panelFocusTrap.focusInitialElementWhenReady()
662
+ .then((focused) => {
663
+ // Nothing inside takes focus (text only): the panel itself becomes the one
664
+ // tab stop, so the opener behind the backdrop doesn't keep focus.
665
+ if (!focused && wrapperElement.isConnected) {
666
+ wrapperElement.setAttribute('tabindex', '0');
667
+ wrapperElement.style.outline = 'none';
668
+ wrapperElement.focus();
669
+ }
670
+ });
671
+ }
672
+ _listenPanelClose(overlayRef) {
673
+ overlayRef.backdropClick()
674
+ .pipe(tap(() => this.close()), takeUntil(this._panelClosed$))
675
+ .subscribe();
676
+ // Every close (close(), the backdrop, Escape, or `popover.close()` from the
677
+ // content) goes through FsPopoverRef.close(), which detaches and then emits.
678
+ this._popoverRef.closed$
679
+ .pipe(take(1), tap(() => this._finishPanel(true)), takeUntil(this._panelClosed$))
680
+ .subscribe();
681
+ }
682
+ /**
683
+ * Escape closes the panel only when no later overlay with a backdrop sits above it, so
684
+ * Escape in a calendar, select or menu opened from the panel closes only that. The check
685
+ * runs in the capture phase, before any handler inside closes its own overlay; fs-datepicker,
686
+ * for one, closes its calendar from the input's keydown and from a document listener.
687
+ */
688
+ _listenPanelEscape(overlayRef) {
689
+ this._ngZone.runOutsideAngular(() => {
690
+ fromEvent(this._document, 'keydown', { capture: true })
691
+ .pipe(tap((event) => {
692
+ this._panelTopmostKeydown = hasOverlayAbove(overlayRef) ? null : event;
693
+ }), takeUntil(this._panelClosed$))
694
+ .subscribe();
695
+ });
696
+ overlayRef.keydownEvents()
697
+ .pipe(filter((event) => event.key === 'Escape' && !hasModifierKey(event)), filter((event) => event === this._panelTopmostKeydown), tap(() => this.close()), takeUntil(this._panelClosed$))
698
+ .subscribe();
699
+ }
700
+ _finishPanel(emit) {
701
+ const overlayRef = this._panelOverlayRef;
702
+ this._panelOverlayRef = null;
703
+ this._panelTopmostKeydown = null;
704
+ this._panelClosed$.next();
705
+ this._panelFocusTrap?.destroy();
706
+ this._panelFocusTrap = null;
707
+ overlayRef.dispose();
708
+ this._restorePanelFocus();
709
+ if (emit) {
710
+ this.closed.emit();
711
+ }
712
+ }
713
+ _restorePanelFocus() {
714
+ const opener = this._panelOpener;
715
+ this._panelOpener = null;
716
+ if (opener?.isConnected && typeof opener.focus === 'function') {
717
+ opener.focus();
718
+ }
719
+ }
539
720
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsPopoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
540
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.7", type: FsPopoverDirective, isStandalone: true, selector: "[fsPopover]", inputs: { text: "text", template: "template", data: "data", leaveDelay: "leaveDelay", showDelay: "showDelay", maxWidth: "maxWidth", maxHeight: "maxHeight", wrapperClass: "wrapperClass", autoShow: "autoShow", autoClose: "autoClose", loadingDiameter: "loadingDiameter", loading: "loading", indication: "indication", position: "position", theme: "theme", size: "size", trigger: "trigger", enabled: "enabled" }, host: { properties: { "class.indication": "this.indication", "class.fs-popover-enabled": "this.enabled" }, classAttribute: "fs-popover" }, usesOnChanges: true, ngImport: i0 });
721
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.7", type: FsPopoverDirective, isStandalone: true, selector: "[fsPopover]", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: false, isRequired: false, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: false, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: false, isRequired: false, transformFunction: null }, leaveDelay: { classPropertyName: "leaveDelay", publicName: "leaveDelay", isSignal: false, isRequired: false, transformFunction: null }, showDelay: { classPropertyName: "showDelay", publicName: "showDelay", isSignal: false, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: false, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: false, isRequired: false, transformFunction: null }, wrapperClass: { classPropertyName: "wrapperClass", publicName: "wrapperClass", isSignal: false, isRequired: false, transformFunction: null }, autoShow: { classPropertyName: "autoShow", publicName: "autoShow", isSignal: false, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: false, isRequired: false, transformFunction: null }, loadingDiameter: { classPropertyName: "loadingDiameter", publicName: "loadingDiameter", isSignal: false, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: false, isRequired: false, transformFunction: null }, indication: { classPropertyName: "indication", publicName: "indication", isSignal: false, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: false, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: false, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: false, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: false, isRequired: false, transformFunction: null }, enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: false, isRequired: false, transformFunction: null }, panel: { classPropertyName: "panel", publicName: "panel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "closed" }, host: { properties: { "class.indication": "this.indication", "class.fs-popover-enabled": "this.enabled" }, classAttribute: "fs-popover" }, exportAs: ["fsPopover"], usesOnChanges: true, ngImport: i0 });
541
722
  }
542
723
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsPopoverDirective, decorators: [{
543
724
  type: Directive,
544
725
  args: [{
545
726
  selector: '[fsPopover]',
727
+ exportAs: 'fsPopover',
546
728
  host: {
547
729
  'class': 'fs-popover',
548
730
  },
@@ -594,11 +776,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImpor
594
776
 
595
777
  class FsPopoverComponent extends FsPopoverDirective {
596
778
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsPopoverComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
597
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: FsPopoverComponent, isStandalone: true, selector: "fs-popover", host: { classAttribute: "fs-popover-component" }, usesInheritance: true, ngImport: i0, template: "<span [ngClass]=\"{'popover-container': enabled}\">\n <ng-content></ng-content>\n</span>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
779
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.7", type: FsPopoverComponent, isStandalone: true, selector: "fs-popover", host: { classAttribute: "fs-popover-component" }, exportAs: ["fsPopover"], usesInheritance: true, ngImport: i0, template: "<span [ngClass]=\"{'popover-container': enabled}\">\n <ng-content></ng-content>\n</span>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
598
780
  }
599
781
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsPopoverComponent, decorators: [{
600
782
  type: Component,
601
- args: [{ selector: 'fs-popover', changeDetection: ChangeDetectionStrategy.OnPush, host: {
783
+ args: [{ selector: 'fs-popover', exportAs: 'fsPopover', changeDetection: ChangeDetectionStrategy.OnPush, host: {
602
784
  'class': 'fs-popover-component',
603
785
  }, standalone: true, imports: [NgClass], template: "<span [ngClass]=\"{'popover-container': enabled}\">\n <ng-content></ng-content>\n</span>\n" }]
604
786
  }] });