@cocoar/ui-overlay 0.1.0-beta.80 → 0.1.0-beta.82

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,123 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, ApplicationRef, EnvironmentInjector, createComponent, Injectable, Injector, createEnvironmentInjector } from '@angular/core';
2
+ import { InjectionToken, Injector, createEnvironmentInjector, createComponent, inject, ApplicationRef, EnvironmentInjector, Injectable } from '@angular/core';
3
3
  import { Subject } from 'rxjs';
4
4
 
5
- class ContentBuilder {
6
- fromComponent(component) {
7
- return { kind: 'component', component };
8
- }
9
- fromTemplate(template) {
10
- return { kind: 'template', template };
11
- }
12
- fromText() {
13
- return { kind: 'text' };
14
- }
15
- }
16
-
17
- function deepFreeze(value) {
18
- if (value == null)
19
- return value;
20
- if (typeof value !== 'object')
21
- return value;
22
- // Only freeze plain JSON-like objects/arrays.
23
- // Framework objects (e.g. Angular TemplateRef) can have internal mutable state.
24
- const proto = Object.getPrototypeOf(value);
25
- const isPlainObject = proto === Object.prototype || proto === null;
26
- if (!isPlainObject && !Array.isArray(value)) {
27
- return value;
28
- }
29
- if (Object.isFrozen(value))
30
- return value;
31
- Object.freeze(value);
32
- if (Array.isArray(value)) {
33
- for (const entry of value) {
34
- deepFreeze(entry);
35
- }
36
- return value;
37
- }
38
- for (const key of Object.keys(value)) {
39
- deepFreeze(value[key]);
40
- }
41
- return value;
42
- }
43
-
44
- class OverlayBuilder {
45
- draft;
46
- constructor(seed) {
47
- this.draft = { ...(seed ?? {}) };
48
- }
49
- backdrop(cfg) {
50
- if (cfg === 'none') {
51
- this.draft.backdrop = { kind: 'none' };
52
- return this;
53
- }
54
- if (cfg === 'modal') {
55
- this.draft.backdrop = { kind: 'modal', closeOnBackdropClick: true };
56
- return this;
57
- }
58
- this.draft.backdrop = cfg;
59
- return this;
60
- }
61
- anchor(cfg) {
62
- this.draft.anchor = cfg;
63
- return this;
64
- }
65
- position(cfg) {
66
- this.draft.position = cfg;
67
- return this;
68
- }
69
- size(cfg) {
70
- this.draft.size = cfg;
71
- return this;
72
- }
73
- scroll(cfg) {
74
- this.draft.scroll = cfg;
75
- return this;
76
- }
77
- dismiss(cfg) {
78
- this.draft.dismiss = cfg;
79
- return this;
80
- }
81
- focus(cfg) {
82
- this.draft.focus = cfg;
83
- return this;
84
- }
85
- a11y(cfg) {
86
- this.draft.a11y = cfg;
87
- return this;
88
- }
89
- attachment(cfg) {
90
- this.draft.attachment = cfg;
91
- return this;
92
- }
93
- content(fn) {
94
- const contentBuilder = new ContentBuilder();
95
- this.draft.content = fn(contentBuilder);
96
- return this;
97
- }
98
- freeze() {
99
- return deepFreeze({ ...this.draft });
100
- }
101
- }
102
-
103
- class CoarOverlay {
104
- static define(fn, ...presets) {
105
- const builder = new OverlayBuilder();
106
- for (const preset of presets) {
107
- preset(builder);
108
- }
109
- fn(builder);
110
- return builder.freeze();
111
- }
112
- static fork(base, fn) {
113
- const builder = new OverlayBuilder(base);
114
- fn(builder);
115
- return builder.freeze();
116
- }
117
- }
118
- /** Alias kept for spec parity (`Overlay.define(...)`). */
119
- const Overlay = CoarOverlay;
120
-
121
5
  /**
122
6
  * Multi-provider token. Resolvers are applied in registration order.
123
7
  */
@@ -138,6 +22,20 @@ const COAR_OVERLAY_DEFAULTS = {
138
22
  attachment: { strategy: 'body' },
139
23
  };
140
24
 
25
+ /**
26
+ * The OverlayRef for the overlay currently rendering this content.
27
+ *
28
+ * Provided automatically by CoarOverlayService for overlay content so components
29
+ * can open true child overlays without DOM lookups.
30
+ */
31
+ const COAR_OVERLAY_REF = new InjectionToken('COAR_OVERLAY_REF');
32
+ /**
33
+ * Parent overlay reference for menu hierarchies.
34
+ * Each overlay provides this token pointing to itself, enabling child menu items
35
+ * to close siblings by calling parent.closeChildren().
36
+ */
37
+ const COAR_MENU_PARENT = new InjectionToken('COAR_MENU_PARENT');
38
+
141
39
  function getViewportRect() {
142
40
  const docEl = document.documentElement;
143
41
  const width = docEl?.clientWidth || window.innerWidth;
@@ -351,984 +249,998 @@ function clamp(value, min, max) {
351
249
  return value;
352
250
  }
353
251
 
354
- /**
355
- * The OverlayRef for the overlay currently rendering this content.
356
- *
357
- * Provided automatically by CoarOverlayService for overlay content so components
358
- * can open true child overlays without DOM lookups.
359
- */
360
- const COAR_OVERLAY_REF = new InjectionToken('COAR_OVERLAY_REF');
361
- /**
362
- * Parent overlay reference for menu hierarchies.
363
- * Each overlay provides this token pointing to itself, enabling child menu items
364
- * to close siblings by calling parent.closeChildren().
365
- */
366
- const COAR_MENU_PARENT = new InjectionToken('COAR_MENU_PARENT');
367
-
368
- class CoarOverlayService {
369
- appRef = inject(ApplicationRef);
370
- environmentInjector = inject(EnvironmentInjector);
371
- specResolvers = inject(COAR_OVERLAY_SPEC_RESOLVERS, { optional: true }) ??
372
- [];
373
- openOverlays = new Set();
374
- globalListenersInstalled = false;
375
- onDocumentPointerDown = (event) => {
376
- const overlays = this.getOpenOverlaysInOrder();
377
- if (overlays.length === 0)
378
- return;
379
- const target = event.target;
380
- const topmostContaining = this.getTopmostOverlayContainingTarget(target);
381
- if (topmostContaining) {
382
- // When interacting with an overlay, close any child overlays (submenus) that may be open.
383
- topmostContaining.closeChildren();
384
- return;
385
- }
386
- const topmost = this.getTopmostDismissableOverlay('outsideClick');
387
- if (!topmost)
388
- return;
389
- // For overlay trees (e.g. menus + submenus), outside-click should close the entire tree.
390
- const root = topmost.getRoot();
391
- if (root !== topmost && root.isDismissable('outsideClick')) {
392
- root.close();
393
- return;
394
- }
395
- topmost.close();
396
- };
397
- onDocumentKeyDown = (event) => {
398
- if (event.key === 'Escape') {
399
- const topmost = this.getTopmostDismissableOverlay('escapeKey');
400
- if (!topmost)
401
- return;
402
- event.preventDefault();
403
- event.stopPropagation();
404
- topmost.close();
405
- return;
252
+ class CoarOverlayRef {
253
+ appRef;
254
+ environmentInjector;
255
+ spec;
256
+ inputs;
257
+ stackIndex;
258
+ parent;
259
+ onClosed;
260
+ afterClosedSubject = new Subject();
261
+ afterClosed$ = this.afterClosedSubject.asObservable();
262
+ get isClosed() {
263
+ return this.closed;
264
+ }
265
+ host;
266
+ panel;
267
+ backdropElement = null;
268
+ destroyContent = null;
269
+ resizeObserver = null;
270
+ cleanupFns = [];
271
+ closed = false;
272
+ lastResult;
273
+ rafPending = false;
274
+ presented = false;
275
+ closeFinalized = false;
276
+ restoreFocusTarget;
277
+ children = new Set();
278
+ hoverCloseTimer = null;
279
+ contentInjector;
280
+ contentEnvironmentInjector;
281
+ shouldAnimateMenu;
282
+ constructor(appRef, environmentInjector, spec, inputs, stackIndex, parent, onClosed) {
283
+ this.appRef = appRef;
284
+ this.environmentInjector = environmentInjector;
285
+ this.spec = spec;
286
+ this.inputs = inputs;
287
+ this.stackIndex = stackIndex;
288
+ this.parent = parent;
289
+ this.onClosed = onClosed;
290
+ this.host = document.createElement('div');
291
+ this.host.className = 'coar-overlay-host';
292
+ this.panel = document.createElement('div');
293
+ this.panel.className = 'coar-overlay-panel';
294
+ this.host.appendChild(this.panel);
295
+ this.shouldAnimateMenu = this.spec.a11y.role === 'menu';
296
+ this.contentInjector = Injector.create({
297
+ providers: [
298
+ { provide: COAR_OVERLAY_REF, useValue: this },
299
+ { provide: COAR_MENU_PARENT, useValue: this },
300
+ ],
301
+ parent: this.environmentInjector,
302
+ });
303
+ this.contentEnvironmentInjector = createEnvironmentInjector([
304
+ { provide: COAR_OVERLAY_REF, useValue: this },
305
+ { provide: COAR_MENU_PARENT, useValue: this },
306
+ ], this.environmentInjector);
307
+ Object.assign(this.host.style, {
308
+ position: 'fixed',
309
+ top: '0px',
310
+ left: '0px',
311
+ transform: 'translate3d(0px, 0px, 0px)',
312
+ zIndex: `calc(var(--coar-z-overlay, 1000) + ${this.stackIndex * 2})`,
313
+ opacity: '1',
314
+ pointerEvents: 'none',
315
+ });
316
+ if (this.shouldAnimateMenu) {
317
+ Object.assign(this.panel.style, {
318
+ opacity: '0',
319
+ transition: 'opacity var(--coar-duration-slower) var(--coar-ease-out)',
320
+ willChange: 'opacity',
321
+ });
406
322
  }
407
- if (event.key === 'Tab') {
408
- const topmost = this.getTopmostFocusTrappingOverlay();
409
- if (!topmost)
410
- return;
411
- if (topmost.handleTabKey(event)) {
412
- event.preventDefault();
413
- }
323
+ this.restoreFocusTarget =
324
+ (typeof document !== 'undefined' ? document.activeElement : null) ?? null;
325
+ if (this.parent) {
326
+ this.parent.children.add(this);
414
327
  }
415
- };
416
- open(spec, inputs, options) {
417
- const resolved = this.resolveSpec(spec);
418
- return this.attach(resolved, inputs, options);
419
328
  }
420
- openChild(parent, spec, inputs, options) {
421
- return this.open(spec, inputs, { parent, closeSiblings: options?.closeSiblings });
329
+ getHoverTreeDismissConfig() {
330
+ return this.spec.dismiss.hoverTree;
422
331
  }
423
- closeAll() {
424
- for (const ref of Array.from(this.openOverlays)) {
425
- ref.close();
426
- }
332
+ getPanelElement() {
333
+ return this.panel;
427
334
  }
428
- resolveSpec(spec) {
429
- const resolvedSpec = this.applySpecResolvers(spec);
430
- const content = resolvedSpec.content;
431
- if (!content) {
432
- throw new Error('OverlaySpec missing content');
433
- }
434
- return {
435
- content,
436
- anchor: resolvedSpec.anchor ?? COAR_OVERLAY_DEFAULTS.anchor,
437
- position: resolvedSpec.position ?? COAR_OVERLAY_DEFAULTS.position,
438
- size: resolvedSpec.size ?? { mode: 'content' },
439
- backdrop: resolvedSpec.backdrop ?? COAR_OVERLAY_DEFAULTS.backdrop,
440
- scroll: resolvedSpec.scroll ?? COAR_OVERLAY_DEFAULTS.scroll,
441
- dismiss: resolvedSpec.dismiss ?? COAR_OVERLAY_DEFAULTS.dismiss,
442
- focus: resolvedSpec.focus ?? COAR_OVERLAY_DEFAULTS.focus,
443
- a11y: resolvedSpec.a11y ?? COAR_OVERLAY_DEFAULTS.a11y,
444
- attachment: resolvedSpec.attachment ?? COAR_OVERLAY_DEFAULTS.attachment,
445
- };
335
+ getRoot() {
336
+ return this.parent?.getRoot() ?? this;
446
337
  }
447
- applySpecResolvers(spec) {
448
- if (this.specResolvers.length === 0)
449
- return spec;
450
- let current = spec;
451
- for (const resolver of this.specResolvers) {
452
- const next = resolver(current);
453
- current = (next ?? current);
338
+ closeChildren(exclude) {
339
+ for (const child of Array.from(this.children)) {
340
+ if (child !== exclude) {
341
+ child.closeChildren();
342
+ child.close();
343
+ }
454
344
  }
455
- return current;
456
345
  }
457
- attach(spec, inputs, options) {
458
- const parent = this.getInternalRefOrNull(options?.parent);
459
- // Close sibling overlays if requested (close all existing children of parent)
460
- if (options?.closeSiblings && parent) {
461
- parent.closeChildren();
346
+ open() {
347
+ const backdrop = this.spec.backdrop;
348
+ if (backdrop.kind === 'modal') {
349
+ const backdropEl = document.createElement('div');
350
+ backdropEl.className = 'coar-overlay-backdrop';
351
+ Object.assign(backdropEl.style, {
352
+ position: 'fixed',
353
+ top: '0px',
354
+ left: '0px',
355
+ right: '0px',
356
+ bottom: '0px',
357
+ background: 'color-mix(in srgb, var(--coar-color-black) 40%, transparent)',
358
+ zIndex: `calc(var(--coar-z-overlay-backdrop, 999) + ${this.stackIndex * 2})`,
359
+ });
360
+ document.body.appendChild(backdropEl);
361
+ this.backdropElement = backdropEl;
362
+ if (backdrop.closeOnBackdropClick !== false) {
363
+ const onClick = (e) => {
364
+ if (e.target === backdropEl) {
365
+ this.close();
366
+ }
367
+ };
368
+ backdropEl.addEventListener('click', onClick);
369
+ this.cleanupFns.push(() => backdropEl.removeEventListener('click', onClick));
370
+ }
462
371
  }
463
- const stackIndex = this.openOverlays.size;
464
- const effectiveSpec = this.inheritDismissFromParent(spec, parent);
465
- const ref = new CoarOverlayRef(this.appRef, this.environmentInjector, effectiveSpec, this.mergeInputs(spec.content, inputs), stackIndex, parent, () => {
466
- this.openOverlays.delete(ref);
467
- this.uninstallGlobalListenersIfIdle();
468
- });
469
- this.openOverlays.add(ref);
470
- this.installGlobalListenersIfNeeded();
471
- ref.open();
472
- return ref;
473
- }
474
- inheritDismissFromParent(spec, parent) {
475
- if (!parent)
476
- return spec;
477
- const parentHoverTree = parent.getHoverTreeDismissConfig();
478
- if (!parentHoverTree?.enabled)
479
- return spec;
480
- const childHoverTree = spec.dismiss.hoverTree;
481
- // Explicit disable in child wins.
482
- if (childHoverTree?.enabled === false)
483
- return spec;
484
- let mergedHoverTree;
485
- if (!childHoverTree) {
486
- mergedHoverTree = { ...parentHoverTree };
487
- }
488
- else {
489
- mergedHoverTree = {
490
- enabled: true,
491
- delayMs: childHoverTree.delayMs ?? parentHoverTree.delayMs,
492
- };
372
+ const attachment = this.spec.attachment;
373
+ const attachmentParent = attachment.strategy === 'parent' ? attachment.container : document.body;
374
+ attachmentParent.appendChild(this.host);
375
+ this.installHoverTreeDismissIfEnabled();
376
+ this.applyA11y();
377
+ this.destroyContent = this.renderContent(this.spec.content, this.panel, this.inputs);
378
+ this.applySize();
379
+ if (this.spec.focus.trap) {
380
+ this.installFocusTrap();
493
381
  }
494
- // No change.
495
- if (mergedHoverTree === childHoverTree)
496
- return spec;
497
- return {
498
- ...spec,
499
- dismiss: {
500
- ...spec.dismiss,
501
- hoverTree: mergedHoverTree,
502
- },
503
- };
504
- }
505
- getInternalRefOrNull(ref) {
506
- if (!ref)
507
- return null;
508
- if (ref instanceof CoarOverlayRef)
509
- return ref;
510
- return null;
382
+ this.installRepositionTriggers();
383
+ this.updatePosition();
511
384
  }
512
- installGlobalListenersIfNeeded() {
513
- if (this.globalListenersInstalled)
514
- return;
515
- if (typeof document === 'undefined')
385
+ installHoverTreeDismissIfEnabled() {
386
+ const hoverTree = this.spec.dismiss.hoverTree;
387
+ if (!hoverTree?.enabled)
516
388
  return;
517
- document.addEventListener('pointerdown', this.onDocumentPointerDown, { capture: true });
518
- document.addEventListener('keydown', this.onDocumentKeyDown, { capture: true });
519
- this.globalListenersInstalled = true;
389
+ const delayMs = typeof hoverTree.delayMs === 'number' ? hoverTree.delayMs : 300;
390
+ const onEnter = () => {
391
+ this.cancelHoverCloseUpTree();
392
+ };
393
+ const onHostLeave = () => {
394
+ this.scheduleHoverCloseUpTree(delayMs);
395
+ };
396
+ const onAnchorLeave = () => {
397
+ this.scheduleHoverClose(delayMs);
398
+ };
399
+ this.host.addEventListener('pointerenter', onEnter);
400
+ this.host.addEventListener('pointerleave', onHostLeave);
401
+ this.cleanupFns.push(() => {
402
+ this.host.removeEventListener('pointerenter', onEnter);
403
+ this.host.removeEventListener('pointerleave', onHostLeave);
404
+ this.cancelHoverClose();
405
+ });
406
+ if (this.spec.anchor.kind === 'element') {
407
+ const el = this.spec.anchor.element;
408
+ el.addEventListener('pointerenter', onEnter);
409
+ el.addEventListener('pointerleave', onAnchorLeave);
410
+ this.cleanupFns.push(() => {
411
+ el.removeEventListener('pointerenter', onEnter);
412
+ el.removeEventListener('pointerleave', onAnchorLeave);
413
+ });
414
+ }
520
415
  }
521
- uninstallGlobalListenersIfIdle() {
522
- if (!this.globalListenersInstalled)
523
- return;
524
- if (this.openOverlays.size > 0)
416
+ cancelHoverClose() {
417
+ if (!this.hoverCloseTimer)
525
418
  return;
526
- if (typeof document === 'undefined')
419
+ clearTimeout(this.hoverCloseTimer);
420
+ this.hoverCloseTimer = null;
421
+ }
422
+ cancelHoverCloseUpTree() {
423
+ this.cancelHoverClose();
424
+ this.parent?.cancelHoverCloseUpTree();
425
+ }
426
+ scheduleHoverClose(delayMs) {
427
+ const hoverTree = this.spec.dismiss.hoverTree;
428
+ if (!hoverTree?.enabled)
527
429
  return;
528
- document.removeEventListener('pointerdown', this.onDocumentPointerDown, { capture: true });
529
- document.removeEventListener('keydown', this.onDocumentKeyDown, { capture: true });
530
- this.globalListenersInstalled = false;
430
+ this.cancelHoverClose();
431
+ this.hoverCloseTimer = setTimeout(() => {
432
+ this.hoverCloseTimer = null;
433
+ this.close();
434
+ }, delayMs);
531
435
  }
532
- getOpenOverlaysInOrder() {
533
- return Array.from(this.openOverlays);
436
+ scheduleHoverCloseUpTree(delayMs) {
437
+ this.scheduleHoverClose(delayMs);
438
+ this.parent?.scheduleHoverCloseUpTree(delayMs);
534
439
  }
535
- getTopmostOverlayContainingTarget(target) {
536
- const overlays = this.getOpenOverlaysInOrder();
537
- for (let i = overlays.length - 1; i >= 0; i -= 1) {
538
- const overlay = overlays[i];
539
- if (overlay.containsEventTarget(target))
540
- return overlay;
541
- }
542
- return null;
440
+ hasFocusTrap() {
441
+ return this.spec.focus.trap === true;
543
442
  }
544
- getTopmostDismissableOverlay(kind) {
545
- const overlays = this.getOpenOverlaysInOrder();
546
- for (let i = overlays.length - 1; i >= 0; i -= 1) {
547
- const overlay = overlays[i];
548
- if (overlay.isDismissable(kind)) {
549
- return overlay;
550
- }
443
+ isDismissable(kind) {
444
+ if (kind === 'outsideClick') {
445
+ return this.spec.dismiss.outsideClick !== false;
551
446
  }
552
- return null;
447
+ return this.spec.dismiss.escapeKey !== false;
553
448
  }
554
- getTopmostFocusTrappingOverlay() {
555
- const overlays = this.getOpenOverlaysInOrder();
556
- for (let i = overlays.length - 1; i >= 0; i -= 1) {
557
- const overlay = overlays[i];
558
- if (overlay.hasFocusTrap()) {
559
- return overlay;
560
- }
449
+ containsEventTarget(target) {
450
+ if (!(target instanceof Node))
451
+ return false;
452
+ if (this.host.contains(target))
453
+ return true;
454
+ if (this.backdropElement?.contains(target))
455
+ return true;
456
+ if (this.spec.anchor.kind === 'element') {
457
+ if (this.spec.anchor.element.contains(target))
458
+ return true;
561
459
  }
562
- return null;
460
+ return false;
563
461
  }
564
- mergeInputs(content, inputs) {
565
- if (!content.defaults)
566
- return inputs;
567
- if (inputs == null || typeof inputs !== 'object') {
568
- return { ...content.defaults };
462
+ handleTabKey(event) {
463
+ if (!this.spec.focus.trap)
464
+ return false;
465
+ const focusables = this.getFocusableElements();
466
+ if (focusables.length === 0) {
467
+ this.focusElement(this.host);
468
+ return true;
569
469
  }
570
- return {
571
- ...content.defaults,
572
- ...inputs,
573
- };
574
- }
575
- renderContent(content, host, inputs) {
576
- switch (content.kind) {
577
- case 'text': {
578
- const text = inputs?.text;
579
- if (typeof text !== 'string') {
580
- throw new Error('Text overlay requires inputs: { text: string }');
581
- }
582
- host.textContent = text;
583
- return () => {
584
- host.textContent = '';
585
- };
586
- }
587
- case 'template': {
588
- const template = content.template;
589
- if (!template)
590
- throw new Error('Template overlay requires a template');
591
- // For templates: automatically add $implicit so templates can use let-variable without property name
592
- // This allows both: let-context (uses $implicit) and let-prop="prop" (uses specific property)
593
- let templateContext = inputs;
594
- if (inputs != null && typeof inputs === 'object' && !('$implicit' in inputs)) {
595
- templateContext = {
596
- ...inputs,
597
- $implicit: inputs,
598
- };
599
- }
600
- const viewRef = template.createEmbeddedView(templateContext);
601
- this.appRef.attachView(viewRef);
602
- viewRef.detectChanges();
603
- for (const node of viewRef.rootNodes) {
604
- host.appendChild(node);
605
- }
606
- return () => {
607
- this.appRef.detachView(viewRef);
608
- viewRef.destroy();
609
- };
610
- }
611
- case 'component': {
612
- const component = content.component;
613
- if (!component)
614
- throw new Error('Component overlay requires a component');
615
- const componentRef = createComponent(component, {
616
- environmentInjector: this.environmentInjector,
617
- hostElement: host,
618
- });
619
- if (inputs && typeof inputs === 'object') {
620
- for (const [key, value] of Object.entries(inputs)) {
621
- componentRef.setInput(key, value);
622
- }
623
- }
624
- this.appRef.attachView(componentRef.hostView);
625
- componentRef.changeDetectorRef.detectChanges();
626
- return () => {
627
- this.appRef.detachView(componentRef.hostView);
628
- componentRef.destroy();
629
- };
470
+ const first = focusables[0];
471
+ const last = focusables[focusables.length - 1];
472
+ const active = document.activeElement;
473
+ const isActiveInside = active instanceof Node && (this.host.contains(active) || active === this.host);
474
+ if (!isActiveInside) {
475
+ this.focusElement(event.shiftKey ? last : first);
476
+ return true;
477
+ }
478
+ if (event.shiftKey) {
479
+ if (active === first || active === this.host) {
480
+ this.focusElement(last);
481
+ return true;
630
482
  }
483
+ return false;
631
484
  }
485
+ if (active === last) {
486
+ this.focusElement(first);
487
+ return true;
488
+ }
489
+ return false;
632
490
  }
633
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
634
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, providedIn: 'root' });
635
- }
636
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, decorators: [{
637
- type: Injectable,
638
- args: [{ providedIn: 'root' }]
639
- }] });
640
- class CoarOverlayRef {
641
- appRef;
642
- environmentInjector;
643
- spec;
644
- inputs;
645
- stackIndex;
646
- parent;
647
- onClosed;
648
- afterClosedSubject = new Subject();
649
- afterClosed$ = this.afterClosedSubject.asObservable();
650
- get isClosed() {
651
- return this.closed;
652
- }
653
- host;
654
- panel;
655
- backdropElement = null;
656
- destroyContent = null;
657
- resizeObserver = null;
658
- cleanupFns = [];
659
- closed = false;
660
- lastResult;
661
- rafPending = false;
662
- presented = false;
663
- closeFinalized = false;
664
- restoreFocusTarget;
665
- children = new Set();
666
- hoverCloseTimer = null;
667
- contentInjector;
668
- contentEnvironmentInjector;
669
- shouldAnimateMenu;
670
- constructor(appRef, environmentInjector, spec, inputs, stackIndex, parent, onClosed) {
671
- this.appRef = appRef;
672
- this.environmentInjector = environmentInjector;
673
- this.spec = spec;
674
- this.inputs = inputs;
675
- this.stackIndex = stackIndex;
676
- this.parent = parent;
677
- this.onClosed = onClosed;
678
- this.host = document.createElement('div');
679
- this.host.className = 'coar-overlay-host';
680
- this.panel = document.createElement('div');
681
- this.panel.className = 'coar-overlay-panel';
682
- this.host.appendChild(this.panel);
683
- this.shouldAnimateMenu = this.spec.a11y.role === 'menu';
684
- this.contentInjector = Injector.create({
685
- providers: [
686
- { provide: COAR_OVERLAY_REF, useValue: this },
687
- { provide: COAR_MENU_PARENT, useValue: this },
688
- ],
689
- parent: this.environmentInjector,
690
- });
691
- this.contentEnvironmentInjector = createEnvironmentInjector([
692
- { provide: COAR_OVERLAY_REF, useValue: this },
693
- { provide: COAR_MENU_PARENT, useValue: this },
694
- ], this.environmentInjector);
695
- Object.assign(this.host.style, {
696
- position: 'fixed',
697
- top: '0px',
698
- left: '0px',
699
- transform: 'translate3d(0px, 0px, 0px)',
700
- zIndex: `calc(var(--coar-z-overlay, 1000) + ${this.stackIndex * 2})`,
701
- opacity: '1',
702
- pointerEvents: 'none',
491
+ updatePosition() {
492
+ if (this.closed)
493
+ return;
494
+ if (this.rafPending)
495
+ return;
496
+ this.rafPending = true;
497
+ this.schedule(() => {
498
+ this.rafPending = false;
499
+ if (this.closed)
500
+ return;
501
+ const viewport = getViewportRect();
502
+ const anchorRect = getAnchorRect(this.spec.anchor, viewport);
503
+ const rect = this.host.getBoundingClientRect();
504
+ const overlaySize = {
505
+ width: rect.width,
506
+ height: rect.height,
507
+ };
508
+ const attachment = this.spec.attachment;
509
+ const boundaryRect = attachment.strategy === 'parent' ? getContainerRect(attachment.container) : undefined;
510
+ const coords = computeOverlayCoordinates(anchorRect, overlaySize, this.spec.position, viewport, boundaryRect);
511
+ this.host.style.transform = `translate3d(${Math.round(coords.left)}px, ${Math.round(coords.top)}px, 0px)`;
512
+ this.present();
703
513
  });
514
+ }
515
+ present() {
516
+ if (this.presented)
517
+ return;
518
+ this.presented = true;
704
519
  if (this.shouldAnimateMenu) {
705
- Object.assign(this.panel.style, {
706
- opacity: '0',
707
- transition: 'opacity var(--coar-duration-slower) var(--coar-ease-out)',
708
- willChange: 'opacity',
709
- });
520
+ void this.panel.getBoundingClientRect();
710
521
  }
711
- this.restoreFocusTarget =
712
- (typeof document !== 'undefined' ? document.activeElement : null) ?? null;
713
- if (this.parent) {
714
- this.parent.children.add(this);
522
+ this.host.style.pointerEvents = 'auto';
523
+ if (this.shouldAnimateMenu) {
524
+ this.panel.style.opacity = '1';
715
525
  }
716
526
  }
717
- getHoverTreeDismissConfig() {
718
- return this.spec.dismiss.hoverTree;
719
- }
720
- getPanelElement() {
721
- return this.panel;
722
- }
723
- getRoot() {
724
- return this.parent?.getRoot() ?? this;
725
- }
726
- closeChildren(exclude) {
727
- for (const child of Array.from(this.children)) {
728
- if (child !== exclude) {
729
- // Recursively close descendants first (depth-first)
730
- child.closeChildren();
731
- // Then close this child
732
- child.close();
733
- }
527
+ close(result) {
528
+ if (this.closed)
529
+ return;
530
+ this.closed = true;
531
+ this.lastResult = result;
532
+ this.cancelHoverClose();
533
+ this.closeChildren();
534
+ for (const cleanup of this.cleanupFns) {
535
+ cleanup();
734
536
  }
735
- }
736
- /**
737
- * Close all sibling overlays (other children of the same parent).
738
- * Used when opening a new child overlay with closeSiblings option.
739
- */
740
- closeSiblings() {
741
- if (this.parent) {
742
- for (const sibling of Array.from(this.parent.children)) {
743
- if (sibling !== this) {
744
- sibling.close();
537
+ this.cleanupFns = [];
538
+ this.resizeObserver?.disconnect();
539
+ this.resizeObserver = null;
540
+ if (this.shouldAnimateMenu && this.presented) {
541
+ this.host.style.pointerEvents = 'none';
542
+ this.panel.style.opacity = '0';
543
+ const finalizeOnce = () => {
544
+ this.finalizeClose();
545
+ };
546
+ let fallbackTimer = null;
547
+ const onEnd = (e) => {
548
+ if (e.target === this.panel && e.propertyName === 'opacity') {
549
+ this.host.removeEventListener('transitionend', onEnd);
550
+ if (fallbackTimer) {
551
+ clearTimeout(fallbackTimer);
552
+ fallbackTimer = null;
553
+ }
554
+ finalizeOnce();
745
555
  }
556
+ };
557
+ this.host.addEventListener('transitionend', onEnd);
558
+ const fallbackMs = this.getMaxTransitionTimeMs();
559
+ if (fallbackMs === 0) {
560
+ this.host.removeEventListener('transitionend', onEnd);
561
+ finalizeOnce();
746
562
  }
563
+ else {
564
+ fallbackTimer = setTimeout(() => {
565
+ this.host.removeEventListener('transitionend', onEnd);
566
+ finalizeOnce();
567
+ }, fallbackMs);
568
+ }
569
+ return;
747
570
  }
571
+ this.finalizeClose();
748
572
  }
749
- open() {
750
- const backdrop = this.spec.backdrop;
751
- if (backdrop.kind === 'modal') {
752
- const backdropEl = document.createElement('div');
753
- backdropEl.className = 'coar-overlay-backdrop';
754
- Object.assign(backdropEl.style, {
755
- position: 'fixed',
756
- top: '0px',
757
- left: '0px',
758
- right: '0px',
759
- bottom: '0px',
760
- background: 'color-mix(in srgb, var(--coar-color-black) 40%, transparent)',
761
- zIndex: `calc(var(--coar-z-overlay-backdrop, 999) + ${this.stackIndex * 2})`,
762
- });
763
- document.body.appendChild(backdropEl);
764
- this.backdropElement = backdropEl;
765
- if (backdrop.closeOnBackdropClick !== false) {
766
- const onClick = (e) => {
767
- if (e.target === backdropEl) {
768
- this.close();
573
+ finalizeClose() {
574
+ if (this.closeFinalized)
575
+ return;
576
+ this.closeFinalized = true;
577
+ this.destroyContent?.();
578
+ this.destroyContent = null;
579
+ this.host.remove();
580
+ this.backdropElement?.remove();
581
+ this.backdropElement = null;
582
+ if (this.spec.focus.restore !== false) {
583
+ const el = this.restoreFocusTarget;
584
+ if (el && typeof el.focus === 'function') {
585
+ try {
586
+ el.focus({ preventScroll: true });
587
+ }
588
+ catch {
589
+ try {
590
+ el.focus();
769
591
  }
770
- };
771
- backdropEl.addEventListener('click', onClick);
772
- this.cleanupFns.push(() => backdropEl.removeEventListener('click', onClick));
592
+ catch {
593
+ // noop
594
+ }
595
+ }
773
596
  }
774
597
  }
775
- // Attach overlay to the appropriate parent based on attachment strategy
776
- const attachment = this.spec.attachment;
777
- const attachmentParent = attachment.strategy === 'parent' ? attachment.container : document.body;
778
- attachmentParent.appendChild(this.host);
779
- this.installHoverTreeDismissIfEnabled();
780
- this.applyA11y();
781
- this.destroyContent = this.renderContent(this.spec.content, this.panel, this.inputs);
782
- this.applySize();
783
- if (this.spec.focus.trap) {
784
- this.installFocusTrap();
598
+ this.afterClosedSubject.next(this.lastResult);
599
+ this.afterClosedSubject.complete();
600
+ this.parent?.children.delete(this);
601
+ this.onClosed();
602
+ }
603
+ getMaxTransitionTimeMs() {
604
+ if (typeof getComputedStyle === 'undefined')
605
+ return 0;
606
+ const style = getComputedStyle(this.panel);
607
+ const durations = style.transitionDuration.split(',').map((v) => v.trim());
608
+ const delays = style.transitionDelay.split(',').map((v) => v.trim());
609
+ const entries = Math.max(durations.length, delays.length);
610
+ let maxMs = 0;
611
+ for (let i = 0; i < entries; i++) {
612
+ const duration = durations[i] ?? durations[durations.length - 1] ?? '0ms';
613
+ const delay = delays[i] ?? delays[delays.length - 1] ?? '0ms';
614
+ const ms = this.parseCssTimeToMs(duration) + this.parseCssTimeToMs(delay);
615
+ maxMs = Math.max(maxMs, ms);
785
616
  }
786
- this.installRepositionTriggers();
787
- this.updatePosition();
617
+ return maxMs;
788
618
  }
789
- installHoverTreeDismissIfEnabled() {
790
- const hoverTree = this.spec.dismiss.hoverTree;
791
- if (!hoverTree?.enabled)
619
+ parseCssTimeToMs(value) {
620
+ const v = value.trim();
621
+ if (!v)
622
+ return 0;
623
+ if (v.endsWith('ms')) {
624
+ const n = Number(v.slice(0, -2));
625
+ return Number.isFinite(n) ? n : 0;
626
+ }
627
+ if (v.endsWith('s')) {
628
+ const n = Number(v.slice(0, -1));
629
+ return Number.isFinite(n) ? n * 1000 : 0;
630
+ }
631
+ const n = Number(v);
632
+ return Number.isFinite(n) ? n : 0;
633
+ }
634
+ installFocusTrap() {
635
+ this.host.tabIndex = -1;
636
+ this.schedule(() => {
637
+ if (this.closed)
638
+ return;
639
+ const focusables = this.getFocusableElements();
640
+ this.focusElement(focusables[0] ?? this.host);
641
+ });
642
+ }
643
+ applySize() {
644
+ const size = this.spec.size;
645
+ if (!size)
792
646
  return;
793
- const delayMs = typeof hoverTree.delayMs === 'number' ? hoverTree.delayMs : 300;
794
- const onEnter = () => {
795
- this.cancelHoverCloseUpTree();
796
- };
797
- const onHostLeave = () => {
798
- this.scheduleHoverCloseUpTree(delayMs);
647
+ const viewport = getViewportRect();
648
+ const resolveMin = (value, anchorSize) => {
649
+ if (value === 'anchor')
650
+ return anchorSize;
651
+ if (typeof value === 'number')
652
+ return value;
653
+ return null;
799
654
  };
800
- // Important: an overlay's anchor element can be *inside* its parent overlay (submenu items).
801
- // Leaving that anchor while still hovering the parent overlay should not schedule closing
802
- // the parent/grandparent overlays; only this (leaf) overlay should be eligible to close.
803
- const onAnchorLeave = () => {
804
- this.scheduleHoverClose(delayMs);
655
+ const resolveMax = (value, viewportSize) => {
656
+ if (value === 'viewport')
657
+ return viewportSize;
658
+ if (typeof value === 'number')
659
+ return value;
660
+ return null;
805
661
  };
806
- this.host.addEventListener('pointerenter', onEnter);
807
- this.host.addEventListener('pointerleave', onHostLeave);
808
- this.cleanupFns.push(() => {
809
- this.host.removeEventListener('pointerenter', onEnter);
810
- this.host.removeEventListener('pointerleave', onHostLeave);
811
- this.cancelHoverClose();
812
- });
813
- if (this.spec.anchor.kind === 'element') {
814
- const el = this.spec.anchor.element;
815
- el.addEventListener('pointerenter', onEnter);
816
- el.addEventListener('pointerleave', onAnchorLeave);
817
- this.cleanupFns.push(() => {
818
- el.removeEventListener('pointerenter', onEnter);
819
- el.removeEventListener('pointerleave', onAnchorLeave);
820
- });
662
+ const anchorRect = getAnchorRect(this.spec.anchor, viewport);
663
+ const minWidthPx = resolveMin(size.minWidth, anchorRect.width);
664
+ const minHeightPx = resolveMin(size.minHeight, anchorRect.height);
665
+ const maxWidthPx = resolveMax(size.maxWidth, viewport.width);
666
+ const maxHeightPx = resolveMax(size.maxHeight, viewport.height);
667
+ this.host.style.width = '';
668
+ this.host.style.height = '';
669
+ this.host.style.minWidth = '';
670
+ this.host.style.minHeight = '';
671
+ this.host.style.maxWidth = '';
672
+ this.host.style.maxHeight = '';
673
+ this.host.style.overflow = '';
674
+ if (minWidthPx != null && minWidthPx > 0)
675
+ this.host.style.minWidth = `${minWidthPx}px`;
676
+ if (minHeightPx != null && minHeightPx > 0)
677
+ this.host.style.minHeight = `${minHeightPx}px`;
678
+ if (size.mode === 'content') {
679
+ return;
680
+ }
681
+ if (size.mode === 'content-clamped') {
682
+ if (maxWidthPx != null)
683
+ this.host.style.maxWidth = `${maxWidthPx}px`;
684
+ if (maxHeightPx != null)
685
+ this.host.style.maxHeight = `${maxHeightPx}px`;
686
+ this.host.style.overflow = 'auto';
687
+ return;
821
688
  }
689
+ if (maxWidthPx != null)
690
+ this.host.style.width = `${maxWidthPx}px`;
691
+ if (maxHeightPx != null)
692
+ this.host.style.height = `${maxHeightPx}px`;
693
+ this.host.style.overflow = 'auto';
822
694
  }
823
- cancelHoverClose() {
824
- if (!this.hoverCloseTimer)
695
+ applyA11y() {
696
+ const a11y = this.spec.a11y;
697
+ if (!a11y)
825
698
  return;
826
- clearTimeout(this.hoverCloseTimer);
827
- this.hoverCloseTimer = null;
828
- }
829
- cancelHoverCloseUpTree() {
830
- this.cancelHoverClose();
831
- this.parent?.cancelHoverCloseUpTree();
699
+ if (a11y.role) {
700
+ this.host.setAttribute('role', a11y.role);
701
+ }
702
+ this.setAttrIfDefined('aria-label', a11y.label);
703
+ this.setAttrIfDefined('aria-labelledby', a11y.labelledBy);
704
+ this.setAttrIfDefined('aria-describedby', a11y.describedBy);
705
+ if (a11y.role === 'dialog' && this.spec.backdrop.kind === 'modal') {
706
+ this.host.setAttribute('aria-modal', 'true');
707
+ }
832
708
  }
833
- scheduleHoverClose(delayMs) {
834
- const hoverTree = this.spec.dismiss.hoverTree;
835
- if (!hoverTree?.enabled)
709
+ setAttrIfDefined(name, value) {
710
+ if (typeof value === 'string' && value.length > 0) {
711
+ this.host.setAttribute(name, value);
836
712
  return;
837
- this.cancelHoverClose();
838
- this.hoverCloseTimer = setTimeout(() => {
839
- this.hoverCloseTimer = null;
840
- this.close();
841
- }, delayMs);
842
- }
843
- scheduleHoverCloseUpTree(delayMs) {
844
- this.scheduleHoverClose(delayMs);
845
- this.parent?.scheduleHoverCloseUpTree(delayMs);
846
- }
847
- hasFocusTrap() {
848
- return this.spec.focus.trap === true;
849
- }
850
- isDismissable(kind) {
851
- if (kind === 'outsideClick') {
852
- return this.spec.dismiss.outsideClick !== false;
853
713
  }
854
- return this.spec.dismiss.escapeKey !== false;
714
+ this.host.removeAttribute(name);
855
715
  }
856
- containsEventTarget(target) {
857
- if (!(target instanceof Node))
858
- return false;
859
- if (this.host.contains(target))
860
- return true;
861
- if (this.backdropElement?.contains(target))
862
- return true;
863
- // Also check if click is on the anchor element (e.g., select trigger)
864
- // to prevent closing when clicking the trigger that opened the overlay
865
- if (this.spec.anchor.kind === 'element') {
866
- if (this.spec.anchor.element.contains(target))
867
- return true;
716
+ focusElement(el) {
717
+ try {
718
+ el.focus({ preventScroll: true });
719
+ }
720
+ catch {
721
+ try {
722
+ el.focus();
723
+ }
724
+ catch {
725
+ // noop
726
+ }
868
727
  }
869
- return false;
870
728
  }
871
- handleTabKey(event) {
872
- if (!this.spec.focus.trap)
873
- return false;
874
- const focusables = this.getFocusableElements();
875
- if (focusables.length === 0) {
876
- this.focusElement(this.host);
729
+ getFocusableElements() {
730
+ const candidates = Array.from(this.host.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[contenteditable="true"]'));
731
+ return candidates.filter((el) => !this.isHidden(el));
732
+ }
733
+ isHidden(el) {
734
+ if (el.hasAttribute('hidden'))
877
735
  return true;
878
- }
879
- const first = focusables[0];
880
- const last = focusables[focusables.length - 1];
881
- const active = document.activeElement;
882
- const isActiveInside = active instanceof Node && (this.host.contains(active) || active === this.host);
883
- // If focus is outside, bring it inside.
884
- if (!isActiveInside) {
885
- this.focusElement(event.shiftKey ? last : first);
736
+ if (el.getAttribute('aria-hidden') === 'true')
886
737
  return true;
887
- }
888
- if (event.shiftKey) {
889
- if (active === first || active === this.host) {
890
- this.focusElement(last);
891
- return true;
892
- }
893
- return false;
894
- }
895
- if (active === last) {
896
- this.focusElement(first);
738
+ const style = el.style;
739
+ if (style.display === 'none' || style.visibility === 'hidden')
897
740
  return true;
898
- }
899
741
  return false;
900
742
  }
901
- updatePosition() {
902
- if (this.closed)
903
- return;
904
- if (this.rafPending)
743
+ installRepositionTriggers() {
744
+ const strategy = this.spec.scroll.strategy;
745
+ if (strategy === 'noop')
905
746
  return;
906
- this.rafPending = true;
907
- this.schedule(() => {
908
- this.rafPending = false;
909
- if (this.closed)
910
- return;
911
- const viewport = getViewportRect();
912
- const anchorRect = getAnchorRect(this.spec.anchor, viewport);
913
- const rect = this.host.getBoundingClientRect();
914
- const overlaySize = {
915
- width: rect.width,
916
- height: rect.height,
917
- };
918
- // For parent-attached overlays, use container boundaries instead of viewport
919
- const attachment = this.spec.attachment;
920
- const boundaryRect = attachment.strategy === 'parent' ? getContainerRect(attachment.container) : undefined;
921
- const coords = computeOverlayCoordinates(anchorRect, overlaySize, this.spec.position, viewport, boundaryRect);
922
- this.host.style.transform = `translate3d(${Math.round(coords.left)}px, ${Math.round(coords.top)}px, 0px)`;
923
- this.present();
924
- });
925
- }
926
- present() {
927
- if (this.presented)
747
+ const scrollParents = this.spec.anchor.kind === 'element' ? getScrollParents(this.spec.anchor.element) : [window];
748
+ if (strategy === 'reposition') {
749
+ const onScroll = () => this.updatePosition();
750
+ const onResize = () => this.updatePosition();
751
+ for (const parent of scrollParents) {
752
+ if (parent === window) {
753
+ window.addEventListener('scroll', onScroll, { passive: true });
754
+ this.cleanupFns.push(() => window.removeEventListener('scroll', onScroll));
755
+ continue;
756
+ }
757
+ parent.addEventListener('scroll', onScroll, { passive: true });
758
+ this.cleanupFns.push(() => parent.removeEventListener('scroll', onScroll));
759
+ }
760
+ window.addEventListener('resize', onResize, { passive: true });
761
+ this.cleanupFns.push(() => window.removeEventListener('resize', onResize));
762
+ if (typeof ResizeObserver !== 'undefined') {
763
+ const ro = new ResizeObserver(() => this.updatePosition());
764
+ ro.observe(this.host);
765
+ this.resizeObserver = ro;
766
+ }
928
767
  return;
929
- this.presented = true;
930
- if (this.shouldAnimateMenu) {
931
- // Ensure the initial opacity=0 is committed before we flip to 1,
932
- // otherwise some browsers skip the transition on first paint.
933
- void this.panel.getBoundingClientRect();
934
768
  }
935
- this.host.style.pointerEvents = 'auto';
936
- if (this.shouldAnimateMenu) {
937
- this.panel.style.opacity = '1';
769
+ if (strategy === 'close') {
770
+ const onScroll = (e) => {
771
+ const target = e.target;
772
+ if (target instanceof Node && this.host.contains(target))
773
+ return;
774
+ this.close();
775
+ };
776
+ if (typeof document !== 'undefined') {
777
+ document.addEventListener('scroll', onScroll, { passive: true, capture: true });
778
+ this.cleanupFns.push(() => document.removeEventListener('scroll', onScroll, { capture: true }));
779
+ }
780
+ window.addEventListener('scroll', onScroll, { passive: true });
781
+ this.cleanupFns.push(() => window.removeEventListener('scroll', onScroll));
782
+ for (const parent of scrollParents) {
783
+ if (parent === window)
784
+ continue;
785
+ parent.addEventListener('scroll', onScroll, { passive: true });
786
+ this.cleanupFns.push(() => parent.removeEventListener('scroll', onScroll));
787
+ }
788
+ if (typeof ResizeObserver !== 'undefined') {
789
+ const ro = new ResizeObserver(() => this.updatePosition());
790
+ ro.observe(this.host);
791
+ this.resizeObserver = ro;
792
+ }
938
793
  }
939
794
  }
940
- close(result) {
941
- if (this.closed)
795
+ schedule(fn) {
796
+ if (typeof requestAnimationFrame !== 'undefined') {
797
+ requestAnimationFrame(() => fn());
942
798
  return;
943
- this.closed = true;
944
- this.lastResult = result;
945
- this.cancelHoverClose();
946
- // Ensure overlay trees (menus/submenus) close consistently.
947
- this.closeChildren();
948
- for (const cleanup of this.cleanupFns) {
949
- cleanup();
950
799
  }
951
- this.cleanupFns = [];
952
- this.resizeObserver?.disconnect();
953
- this.resizeObserver = null;
954
- // Begin visual close (menu overlays only). We keep the host in the DOM
955
- // briefly so opacity can transition to 0, then tear down.
956
- if (this.shouldAnimateMenu && this.presented) {
957
- this.host.style.pointerEvents = 'none';
958
- this.panel.style.opacity = '0';
959
- const finalizeOnce = () => {
960
- this.finalizeClose();
961
- };
962
- let fallbackTimer = null;
963
- const onEnd = (e) => {
964
- if (e.target === this.panel && e.propertyName === 'opacity') {
965
- this.host.removeEventListener('transitionend', onEnd);
966
- if (fallbackTimer) {
967
- clearTimeout(fallbackTimer);
968
- fallbackTimer = null;
800
+ setTimeout(() => fn(), 0);
801
+ }
802
+ renderContent(content, host, inputs) {
803
+ switch (content.kind) {
804
+ case 'text': {
805
+ const text = inputs?.text;
806
+ if (typeof text !== 'string') {
807
+ throw new Error('Text overlay requires inputs: { text: string }');
808
+ }
809
+ host.textContent = text;
810
+ return () => {
811
+ host.textContent = '';
812
+ };
813
+ }
814
+ case 'template': {
815
+ const template = content.template;
816
+ if (!template)
817
+ throw new Error('Template overlay requires a template');
818
+ let templateContext = inputs;
819
+ if (inputs != null && typeof inputs === 'object' && !('$implicit' in inputs)) {
820
+ templateContext = {
821
+ ...inputs,
822
+ $implicit: inputs,
823
+ };
824
+ }
825
+ const viewRef = template.createEmbeddedView(templateContext, this.contentInjector);
826
+ this.appRef.attachView(viewRef);
827
+ viewRef.detectChanges();
828
+ for (const node of viewRef.rootNodes) {
829
+ host.appendChild(node);
830
+ }
831
+ return () => {
832
+ this.appRef.detachView(viewRef);
833
+ viewRef.destroy();
834
+ };
835
+ }
836
+ case 'component': {
837
+ const component = content.component;
838
+ if (!component)
839
+ throw new Error('Component overlay requires a component');
840
+ const componentRef = createComponent(component, {
841
+ environmentInjector: this.contentEnvironmentInjector,
842
+ hostElement: host,
843
+ });
844
+ if (inputs && typeof inputs === 'object') {
845
+ for (const [key, value] of Object.entries(inputs)) {
846
+ componentRef.setInput(key, value);
969
847
  }
970
- finalizeOnce();
971
848
  }
972
- };
973
- this.host.addEventListener('transitionend', onEnd);
974
- const fallbackMs = this.getMaxTransitionTimeMs();
975
- if (fallbackMs === 0) {
976
- this.host.removeEventListener('transitionend', onEnd);
977
- finalizeOnce();
978
- }
979
- else {
980
- fallbackTimer = setTimeout(() => {
981
- this.host.removeEventListener('transitionend', onEnd);
982
- finalizeOnce();
983
- }, fallbackMs);
849
+ this.appRef.attachView(componentRef.hostView);
850
+ componentRef.changeDetectorRef.detectChanges();
851
+ return () => {
852
+ this.appRef.detachView(componentRef.hostView);
853
+ componentRef.destroy();
854
+ };
984
855
  }
985
- return;
986
856
  }
987
- this.finalizeClose();
988
857
  }
989
- finalizeClose() {
990
- if (this.closeFinalized)
858
+ }
859
+
860
+ class CoarOverlayService {
861
+ appRef = inject(ApplicationRef);
862
+ environmentInjector = inject(EnvironmentInjector);
863
+ specResolvers = inject(COAR_OVERLAY_SPEC_RESOLVERS, { optional: true }) ??
864
+ [];
865
+ openOverlays = new Set();
866
+ globalListenersInstalled = false;
867
+ onDocumentPointerDown = (event) => {
868
+ const overlays = this.getOpenOverlaysInOrder();
869
+ if (overlays.length === 0)
991
870
  return;
992
- this.closeFinalized = true;
993
- this.destroyContent?.();
994
- this.destroyContent = null;
995
- this.host.remove();
996
- this.backdropElement?.remove();
997
- this.backdropElement = null;
998
- if (this.spec.focus.restore !== false) {
999
- const el = this.restoreFocusTarget;
1000
- if (el && typeof el.focus === 'function') {
1001
- try {
1002
- el.focus({ preventScroll: true });
1003
- }
1004
- catch {
1005
- try {
1006
- el.focus();
1007
- }
1008
- catch {
1009
- // noop
1010
- }
1011
- }
871
+ const target = event.target;
872
+ const topmostContaining = this.getTopmostOverlayContainingTarget(target);
873
+ if (topmostContaining) {
874
+ // When interacting with an overlay, close any child overlays (submenus) that may be open.
875
+ topmostContaining.closeChildren();
876
+ return;
877
+ }
878
+ const topmost = this.getTopmostDismissableOverlay('outsideClick');
879
+ if (!topmost)
880
+ return;
881
+ // For overlay trees (e.g. menus + submenus), outside-click should close the entire tree.
882
+ const root = topmost.getRoot();
883
+ if (root !== topmost && root.isDismissable('outsideClick')) {
884
+ root.close();
885
+ return;
886
+ }
887
+ topmost.close();
888
+ };
889
+ onDocumentKeyDown = (event) => {
890
+ if (event.key === 'Escape') {
891
+ const topmost = this.getTopmostDismissableOverlay('escapeKey');
892
+ if (!topmost)
893
+ return;
894
+ event.preventDefault();
895
+ event.stopPropagation();
896
+ topmost.close();
897
+ return;
898
+ }
899
+ if (event.key === 'Tab') {
900
+ const topmost = this.getTopmostFocusTrappingOverlay();
901
+ if (!topmost)
902
+ return;
903
+ if (topmost.handleTabKey(event)) {
904
+ event.preventDefault();
1012
905
  }
1013
906
  }
1014
- this.afterClosedSubject.next(this.lastResult);
1015
- this.afterClosedSubject.complete();
1016
- this.parent?.children.delete(this);
1017
- this.onClosed();
907
+ };
908
+ openTemplate(template, settings, inputs) {
909
+ const resolved = this.resolveSpec({
910
+ ...settings,
911
+ content: { kind: 'template', template },
912
+ });
913
+ return this.attach(resolved, inputs, undefined);
1018
914
  }
1019
- getMaxTransitionTimeMs() {
1020
- if (typeof getComputedStyle === 'undefined')
1021
- return 0;
1022
- const style = getComputedStyle(this.panel);
1023
- const durations = style.transitionDuration.split(',').map((v) => v.trim());
1024
- const delays = style.transitionDelay.split(',').map((v) => v.trim());
1025
- const entries = Math.max(durations.length, delays.length);
1026
- let maxMs = 0;
1027
- for (let i = 0; i < entries; i++) {
1028
- const duration = durations[i] ?? durations[durations.length - 1] ?? '0ms';
1029
- const delay = delays[i] ?? delays[delays.length - 1] ?? '0ms';
1030
- const ms = this.parseCssTimeToMs(duration) + this.parseCssTimeToMs(delay);
1031
- maxMs = Math.max(maxMs, ms);
915
+ openComponent(component, settings, inputs) {
916
+ const resolved = this.resolveSpec({
917
+ ...settings,
918
+ content: { kind: 'component', component },
919
+ });
920
+ return this.attach(resolved, inputs, undefined);
921
+ }
922
+ openText(settings, inputs) {
923
+ const resolved = this.resolveSpec({
924
+ ...settings,
925
+ content: { kind: 'text' },
926
+ });
927
+ return this.attach(resolved, inputs, undefined);
928
+ }
929
+ openTemplateAsChild(parent, template, settings, inputs, options) {
930
+ const resolved = this.resolveSpec({
931
+ ...settings,
932
+ content: { kind: 'template', template },
933
+ });
934
+ return this.attach(resolved, inputs, { parent, closeSiblings: options?.closeSiblings });
935
+ }
936
+ openComponentAsChild(parent, component, settings, inputs, options) {
937
+ const resolved = this.resolveSpec({
938
+ ...settings,
939
+ content: { kind: 'component', component },
940
+ });
941
+ return this.attach(resolved, inputs, { parent, closeSiblings: options?.closeSiblings });
942
+ }
943
+ openTextAsChild(parent, settings, inputs, options) {
944
+ const resolved = this.resolveSpec({
945
+ ...settings,
946
+ content: { kind: 'text' },
947
+ });
948
+ return this.attach(resolved, inputs, { parent, closeSiblings: options?.closeSiblings });
949
+ }
950
+ closeAll() {
951
+ for (const ref of Array.from(this.openOverlays)) {
952
+ ref.close();
1032
953
  }
1033
- return maxMs;
1034
954
  }
1035
- parseCssTimeToMs(value) {
1036
- const v = value.trim();
1037
- if (!v)
1038
- return 0;
1039
- if (v.endsWith('ms')) {
1040
- const n = Number(v.slice(0, -2));
1041
- return Number.isFinite(n) ? n : 0;
955
+ resolveSpec(spec) {
956
+ const resolvedSpec = this.applySpecResolvers(spec);
957
+ const content = resolvedSpec.content;
958
+ if (!content) {
959
+ throw new Error('OverlaySpec missing content');
1042
960
  }
1043
- if (v.endsWith('s')) {
1044
- const n = Number(v.slice(0, -1));
1045
- return Number.isFinite(n) ? n * 1000 : 0;
961
+ return {
962
+ content,
963
+ anchor: resolvedSpec.anchor ?? COAR_OVERLAY_DEFAULTS.anchor,
964
+ position: resolvedSpec.position ?? COAR_OVERLAY_DEFAULTS.position,
965
+ size: resolvedSpec.size ?? { mode: 'content' },
966
+ backdrop: resolvedSpec.backdrop ?? COAR_OVERLAY_DEFAULTS.backdrop,
967
+ scroll: resolvedSpec.scroll ?? COAR_OVERLAY_DEFAULTS.scroll,
968
+ dismiss: resolvedSpec.dismiss ?? COAR_OVERLAY_DEFAULTS.dismiss,
969
+ focus: resolvedSpec.focus ?? COAR_OVERLAY_DEFAULTS.focus,
970
+ a11y: resolvedSpec.a11y ?? COAR_OVERLAY_DEFAULTS.a11y,
971
+ attachment: resolvedSpec.attachment ?? COAR_OVERLAY_DEFAULTS.attachment,
972
+ };
973
+ }
974
+ applySpecResolvers(spec) {
975
+ if (this.specResolvers.length === 0)
976
+ return spec;
977
+ let current = spec;
978
+ for (const resolver of this.specResolvers) {
979
+ const next = resolver(current);
980
+ current = (next ?? current);
1046
981
  }
1047
- const n = Number(v);
1048
- return Number.isFinite(n) ? n : 0;
982
+ return current;
1049
983
  }
1050
- installFocusTrap() {
1051
- // Ensure the host can receive programmatic focus as a fallback.
1052
- this.host.tabIndex = -1;
1053
- // Move focus into the overlay on open (best-effort).
1054
- this.schedule(() => {
1055
- if (this.closed)
1056
- return;
1057
- const focusables = this.getFocusableElements();
1058
- this.focusElement(focusables[0] ?? this.host);
984
+ attach(spec, inputs, options) {
985
+ const parent = this.getInternalRefOrNull(options?.parent);
986
+ // Close sibling overlays if requested (close all existing children of parent)
987
+ if (options?.closeSiblings && parent) {
988
+ parent.closeChildren();
989
+ }
990
+ const stackIndex = this.openOverlays.size;
991
+ const effectiveSpec = this.inheritDismissFromParent(spec, parent);
992
+ const ref = new CoarOverlayRef(this.appRef, this.environmentInjector, effectiveSpec, this.mergeInputs(spec.content, inputs), stackIndex, parent, () => {
993
+ this.openOverlays.delete(ref);
994
+ this.uninstallGlobalListenersIfIdle();
1059
995
  });
996
+ this.openOverlays.add(ref);
997
+ this.installGlobalListenersIfNeeded();
998
+ ref.open();
999
+ return ref;
1060
1000
  }
1061
- applySize() {
1062
- const size = this.spec.size;
1063
- if (!size)
1064
- return;
1065
- const viewport = getViewportRect();
1066
- const resolveMin = (value, anchorSize) => {
1067
- if (value === 'anchor')
1068
- return anchorSize;
1069
- if (typeof value === 'number')
1070
- return value;
1071
- return null;
1001
+ inheritDismissFromParent(spec, parent) {
1002
+ if (!parent)
1003
+ return spec;
1004
+ const parentHoverTree = parent.getHoverTreeDismissConfig();
1005
+ if (!parentHoverTree?.enabled)
1006
+ return spec;
1007
+ const childHoverTree = spec.dismiss.hoverTree;
1008
+ // Explicit disable in child wins.
1009
+ if (childHoverTree?.enabled === false)
1010
+ return spec;
1011
+ let mergedHoverTree;
1012
+ if (!childHoverTree) {
1013
+ mergedHoverTree = { ...parentHoverTree };
1014
+ }
1015
+ else {
1016
+ mergedHoverTree = {
1017
+ enabled: true,
1018
+ delayMs: childHoverTree.delayMs ?? parentHoverTree.delayMs,
1019
+ };
1020
+ }
1021
+ // No change.
1022
+ if (mergedHoverTree === childHoverTree)
1023
+ return spec;
1024
+ return {
1025
+ ...spec,
1026
+ dismiss: {
1027
+ ...spec.dismiss,
1028
+ hoverTree: mergedHoverTree,
1029
+ },
1072
1030
  };
1073
- const resolveMax = (value, viewportSize) => {
1074
- if (value === 'viewport')
1075
- return viewportSize;
1076
- if (typeof value === 'number')
1077
- return value;
1031
+ }
1032
+ getInternalRefOrNull(ref) {
1033
+ if (!ref)
1078
1034
  return null;
1079
- };
1080
- const anchorRect = getAnchorRect(this.spec.anchor, viewport);
1081
- const minWidthPx = resolveMin(size.minWidth, anchorRect.width);
1082
- const minHeightPx = resolveMin(size.minHeight, anchorRect.height);
1083
- const maxWidthPx = resolveMax(size.maxWidth, viewport.width);
1084
- const maxHeightPx = resolveMax(size.maxHeight, viewport.height);
1085
- // Reset in case an overlay host is re-used in the future.
1086
- this.host.style.width = '';
1087
- this.host.style.height = '';
1088
- this.host.style.minWidth = '';
1089
- this.host.style.minHeight = '';
1090
- this.host.style.maxWidth = '';
1091
- this.host.style.maxHeight = '';
1092
- this.host.style.overflow = '';
1093
- if (minWidthPx != null && minWidthPx > 0)
1094
- this.host.style.minWidth = `${minWidthPx}px`;
1095
- if (minHeightPx != null && minHeightPx > 0)
1096
- this.host.style.minHeight = `${minHeightPx}px`;
1097
- if (size.mode === 'content') {
1035
+ if (ref instanceof CoarOverlayRef)
1036
+ return ref;
1037
+ return null;
1038
+ }
1039
+ installGlobalListenersIfNeeded() {
1040
+ if (this.globalListenersInstalled)
1098
1041
  return;
1099
- }
1100
- if (size.mode === 'content-clamped') {
1101
- if (maxWidthPx != null)
1102
- this.host.style.maxWidth = `${maxWidthPx}px`;
1103
- if (maxHeightPx != null)
1104
- this.host.style.maxHeight = `${maxHeightPx}px`;
1105
- this.host.style.overflow = 'auto';
1042
+ if (typeof document === 'undefined')
1106
1043
  return;
1107
- }
1108
- // mode: 'fixed'
1109
- if (maxWidthPx != null)
1110
- this.host.style.width = `${maxWidthPx}px`;
1111
- if (maxHeightPx != null)
1112
- this.host.style.height = `${maxHeightPx}px`;
1113
- this.host.style.overflow = 'auto';
1044
+ document.addEventListener('pointerdown', this.onDocumentPointerDown, { capture: true });
1045
+ document.addEventListener('keydown', this.onDocumentKeyDown, { capture: true });
1046
+ this.globalListenersInstalled = true;
1114
1047
  }
1115
- applyA11y() {
1116
- const a11y = this.spec.a11y;
1117
- if (!a11y)
1048
+ uninstallGlobalListenersIfIdle() {
1049
+ if (!this.globalListenersInstalled)
1118
1050
  return;
1119
- if (a11y.role) {
1120
- this.host.setAttribute('role', a11y.role);
1121
- }
1122
- this.setAttrIfDefined('aria-label', a11y.label);
1123
- this.setAttrIfDefined('aria-labelledby', a11y.labelledBy);
1124
- this.setAttrIfDefined('aria-describedby', a11y.describedBy);
1125
- if (a11y.role === 'dialog' && this.spec.backdrop.kind === 'modal') {
1126
- this.host.setAttribute('aria-modal', 'true');
1127
- }
1128
- }
1129
- setAttrIfDefined(name, value) {
1130
- if (typeof value === 'string' && value.length > 0) {
1131
- this.host.setAttribute(name, value);
1051
+ if (this.openOverlays.size > 0)
1132
1052
  return;
1133
- }
1134
- this.host.removeAttribute(name);
1135
- }
1136
- focusElement(el) {
1137
- try {
1138
- el.focus({ preventScroll: true });
1139
- }
1140
- catch {
1141
- try {
1142
- el.focus();
1143
- }
1144
- catch {
1145
- // noop
1146
- }
1147
- }
1053
+ if (typeof document === 'undefined')
1054
+ return;
1055
+ document.removeEventListener('pointerdown', this.onDocumentPointerDown, { capture: true });
1056
+ document.removeEventListener('keydown', this.onDocumentKeyDown, { capture: true });
1057
+ this.globalListenersInstalled = false;
1148
1058
  }
1149
- getFocusableElements() {
1150
- const candidates = Array.from(this.host.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[contenteditable="true"]'));
1151
- return candidates.filter((el) => !this.isHidden(el));
1059
+ getOpenOverlaysInOrder() {
1060
+ return Array.from(this.openOverlays);
1152
1061
  }
1153
- isHidden(el) {
1154
- // Avoid using offsetParent as it behaves poorly in some environments (e.g. JSDOM).
1155
- if (el.hasAttribute('hidden'))
1156
- return true;
1157
- if (el.getAttribute('aria-hidden') === 'true')
1158
- return true;
1159
- const style = el.style;
1160
- if (style.display === 'none' || style.visibility === 'hidden')
1161
- return true;
1162
- return false;
1062
+ getTopmostOverlayContainingTarget(target) {
1063
+ const overlays = this.getOpenOverlaysInOrder();
1064
+ for (let i = overlays.length - 1; i >= 0; i -= 1) {
1065
+ const overlay = overlays[i];
1066
+ if (overlay.containsEventTarget(target))
1067
+ return overlay;
1068
+ }
1069
+ return null;
1163
1070
  }
1164
- installRepositionTriggers() {
1165
- const strategy = this.spec.scroll.strategy;
1166
- if (strategy === 'noop')
1167
- return;
1168
- const scrollParents = this.spec.anchor.kind === 'element' ? getScrollParents(this.spec.anchor.element) : [window];
1169
- if (strategy === 'reposition') {
1170
- const onScroll = () => this.updatePosition();
1171
- const onResize = () => this.updatePosition();
1172
- for (const parent of scrollParents) {
1173
- if (parent === window) {
1174
- window.addEventListener('scroll', onScroll, { passive: true });
1175
- this.cleanupFns.push(() => window.removeEventListener('scroll', onScroll));
1176
- continue;
1177
- }
1178
- parent.addEventListener('scroll', onScroll, { passive: true });
1179
- this.cleanupFns.push(() => parent.removeEventListener('scroll', onScroll));
1180
- }
1181
- window.addEventListener('resize', onResize, { passive: true });
1182
- this.cleanupFns.push(() => window.removeEventListener('resize', onResize));
1183
- if (typeof ResizeObserver !== 'undefined') {
1184
- const ro = new ResizeObserver(() => this.updatePosition());
1185
- ro.observe(this.host);
1186
- this.resizeObserver = ro;
1071
+ getTopmostDismissableOverlay(kind) {
1072
+ const overlays = this.getOpenOverlaysInOrder();
1073
+ for (let i = overlays.length - 1; i >= 0; i -= 1) {
1074
+ const overlay = overlays[i];
1075
+ if (overlay.isDismissable(kind)) {
1076
+ return overlay;
1187
1077
  }
1188
- return;
1189
1078
  }
1190
- if (strategy === 'close') {
1191
- const onScroll = (e) => {
1192
- const target = e.target;
1193
- if (target instanceof Node && this.host.contains(target))
1194
- return;
1195
- this.close();
1196
- };
1197
- if (typeof document !== 'undefined') {
1198
- document.addEventListener('scroll', onScroll, { passive: true, capture: true });
1199
- this.cleanupFns.push(() => document.removeEventListener('scroll', onScroll, { capture: true }));
1200
- }
1201
- // Fallback for environments where scroll is only observed on window.
1202
- window.addEventListener('scroll', onScroll, { passive: true });
1203
- this.cleanupFns.push(() => window.removeEventListener('scroll', onScroll));
1204
- // Also attach to explicit scroll parents for element anchors (helps non-standard event targets).
1205
- for (const parent of scrollParents) {
1206
- if (parent === window)
1207
- continue;
1208
- parent.addEventListener('scroll', onScroll, { passive: true });
1209
- this.cleanupFns.push(() => parent.removeEventListener('scroll', onScroll));
1210
- }
1211
- // Still reposition on size changes while open.
1212
- if (typeof ResizeObserver !== 'undefined') {
1213
- const ro = new ResizeObserver(() => this.updatePosition());
1214
- ro.observe(this.host);
1215
- this.resizeObserver = ro;
1079
+ return null;
1080
+ }
1081
+ getTopmostFocusTrappingOverlay() {
1082
+ const overlays = this.getOpenOverlaysInOrder();
1083
+ for (let i = overlays.length - 1; i >= 0; i -= 1) {
1084
+ const overlay = overlays[i];
1085
+ if (overlay.hasFocusTrap()) {
1086
+ return overlay;
1216
1087
  }
1217
1088
  }
1089
+ return null;
1218
1090
  }
1219
- schedule(fn) {
1220
- if (typeof requestAnimationFrame !== 'undefined') {
1221
- requestAnimationFrame(() => fn());
1222
- return;
1091
+ mergeInputs(content, inputs) {
1092
+ if (!content.defaults)
1093
+ return inputs;
1094
+ if (inputs == null || typeof inputs !== 'object') {
1095
+ return { ...content.defaults };
1223
1096
  }
1224
- setTimeout(() => fn(), 0);
1097
+ return {
1098
+ ...content.defaults,
1099
+ ...inputs,
1100
+ };
1225
1101
  }
1226
- renderContent(content, host, inputs) {
1227
- switch (content.kind) {
1228
- case 'text': {
1229
- const text = inputs?.text;
1230
- if (typeof text !== 'string') {
1231
- throw new Error('Text overlay requires inputs: { text: string }');
1232
- }
1233
- host.textContent = text;
1234
- return () => {
1235
- host.textContent = '';
1236
- };
1237
- }
1238
- case 'template': {
1239
- const template = content.template;
1240
- if (!template)
1241
- throw new Error('Template overlay requires a template');
1242
- const viewRef = template.createEmbeddedView(inputs, this.contentInjector);
1243
- this.appRef.attachView(viewRef);
1244
- viewRef.detectChanges();
1245
- for (const node of viewRef.rootNodes) {
1246
- host.appendChild(node);
1247
- }
1248
- return () => {
1249
- this.appRef.detachView(viewRef);
1250
- viewRef.destroy();
1251
- };
1252
- }
1253
- case 'component': {
1254
- const component = content.component;
1255
- if (!component)
1256
- throw new Error('Component overlay requires a component');
1257
- const componentRef = createComponent(component, {
1258
- environmentInjector: this.contentEnvironmentInjector,
1259
- hostElement: host,
1260
- });
1261
- if (inputs && typeof inputs === 'object') {
1262
- for (const [key, value] of Object.entries(inputs)) {
1263
- componentRef.setInput(key, value);
1264
- }
1265
- }
1266
- this.appRef.attachView(componentRef.hostView);
1267
- componentRef.changeDetectorRef.detectChanges();
1268
- return () => {
1269
- this.appRef.detachView(componentRef.hostView);
1270
- componentRef.destroy();
1271
- };
1272
- }
1273
- }
1102
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1103
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, providedIn: 'root' });
1104
+ }
1105
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, decorators: [{
1106
+ type: Injectable,
1107
+ args: [{ providedIn: 'root' }]
1108
+ }] });
1109
+
1110
+ class CoarOverlayOpenBuilder {
1111
+ service;
1112
+ settings;
1113
+ constructor(service, settings = {}) {
1114
+ this.service = service;
1115
+ this.settings = settings;
1116
+ }
1117
+ fork() {
1118
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings });
1119
+ }
1120
+ backdrop(value) {
1121
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, backdrop: value });
1122
+ }
1123
+ anchor(value) {
1124
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, anchor: value });
1125
+ }
1126
+ position(value) {
1127
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, position: value });
1128
+ }
1129
+ size(value) {
1130
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, size: value });
1131
+ }
1132
+ scroll(value) {
1133
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, scroll: value });
1134
+ }
1135
+ dismiss(value) {
1136
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, dismiss: value });
1137
+ }
1138
+ focus(value) {
1139
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, focus: value });
1140
+ }
1141
+ a11y(value) {
1142
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, a11y: value });
1143
+ }
1144
+ attachment(value) {
1145
+ return new CoarOverlayOpenBuilder(this.service, { ...this.settings, attachment: value });
1146
+ }
1147
+ /**
1148
+ * Applies baseline settings, without overriding fields already set on this builder.
1149
+ * Useful for applying a preset after you've already configured part of the builder.
1150
+ */
1151
+ withPreset(value) {
1152
+ const merged = { ...this.settings };
1153
+ merged.anchor ??= value.anchor;
1154
+ merged.position ??= value.position;
1155
+ merged.size ??= value.size;
1156
+ merged.backdrop ??= value.backdrop;
1157
+ merged.scroll ??= value.scroll;
1158
+ merged.dismiss ??= value.dismiss;
1159
+ merged.focus ??= value.focus;
1160
+ merged.a11y ??= value.a11y;
1161
+ merged.attachment ??= value.attachment;
1162
+ return new CoarOverlayOpenBuilder(this.service, merged);
1163
+ }
1164
+ fromTemplate(template) {
1165
+ const settingsSnapshot = { ...this.settings };
1166
+ return {
1167
+ open: (inputs) => this.service.openTemplate(template, settingsSnapshot, inputs),
1168
+ openAsChild: (parent, inputs, options) => this.service.openTemplateAsChild(parent, template, settingsSnapshot, inputs, options),
1169
+ };
1170
+ }
1171
+ fromComponent(component) {
1172
+ const settingsSnapshot = { ...this.settings };
1173
+ return {
1174
+ open: (inputs) => this.service.openComponent(component, settingsSnapshot, inputs),
1175
+ openAsChild: (parent, inputs, options) => this.service.openComponentAsChild(parent, component, settingsSnapshot, inputs, options),
1176
+ };
1177
+ }
1178
+ fromText() {
1179
+ const settingsSnapshot = { ...this.settings };
1180
+ return {
1181
+ open: (inputs) => this.service.openText(settingsSnapshot, inputs),
1182
+ openAsChild: (parent, inputs, options) => this.service.openTextAsChild(parent, settingsSnapshot, inputs, options),
1183
+ };
1184
+ }
1185
+ }
1186
+ function createOverlayBuilder(...initial) {
1187
+ const service = inject(CoarOverlayService);
1188
+ if (initial.length === 0) {
1189
+ return new CoarOverlayOpenBuilder(service);
1274
1190
  }
1191
+ const merged = Object.assign({}, ...initial);
1192
+ return new CoarOverlayOpenBuilder(service, merged);
1275
1193
  }
1276
1194
 
1277
- const coarTooltipPreset = (b) => {
1278
- b.backdrop('none');
1279
- b.scroll({ strategy: 'noop' });
1280
- b.dismiss({ outsideClick: false, escapeKey: false });
1281
- b.a11y({ role: 'tooltip' });
1282
- b.position({
1195
+ const coarTooltipPreset = {
1196
+ backdrop: { kind: 'none' },
1197
+ scroll: { strategy: 'noop' },
1198
+ dismiss: { outsideClick: false, escapeKey: false },
1199
+ a11y: { role: 'tooltip' },
1200
+ position: {
1283
1201
  placement: ['top', 'bottom', 'left', 'right'],
1284
1202
  offset: 8,
1285
1203
  flip: true,
1286
1204
  shift: true,
1287
- });
1288
- b.attachment({ strategy: 'body' });
1205
+ },
1206
+ attachment: { strategy: 'body' },
1289
1207
  };
1290
- const coarModalPreset = (b) => {
1291
- b.backdrop('modal');
1292
- b.anchor({ kind: 'virtual', placement: 'center' });
1293
- b.focus({ trap: true, restore: true });
1294
- b.size({ mode: 'content-clamped', maxWidth: 'viewport', maxHeight: 'viewport' });
1295
- b.position({ placement: 'center', offset: 0, flip: false, shift: true });
1296
- b.a11y({ role: 'dialog' });
1297
- b.attachment({ strategy: 'body' });
1208
+ const coarModalPreset = {
1209
+ backdrop: { kind: 'modal', closeOnBackdropClick: true },
1210
+ anchor: { kind: 'virtual', placement: 'center' },
1211
+ focus: { trap: true, restore: true },
1212
+ size: { mode: 'content-clamped', maxWidth: 'viewport', maxHeight: 'viewport' },
1213
+ position: { placement: 'center', offset: 0, flip: false, shift: true },
1214
+ a11y: { role: 'dialog' },
1215
+ attachment: { strategy: 'body' },
1298
1216
  };
1299
- const coarMenuPreset = (b) => {
1300
- b.backdrop('none');
1301
- b.scroll({ strategy: 'close' });
1302
- b.dismiss({ outsideClick: true, escapeKey: true });
1303
- b.focus({ trap: false, restore: true });
1304
- b.a11y({ role: 'menu' });
1305
- // Provide fallback placements so the overlay can choose a side that fits.
1306
- // Start/end variants help when the anchor is near the viewport edges.
1307
- b.position({
1217
+ const coarMenuPreset = {
1218
+ backdrop: { kind: 'none' },
1219
+ scroll: { strategy: 'close' },
1220
+ dismiss: { outsideClick: true, escapeKey: true },
1221
+ focus: { trap: false, restore: true },
1222
+ a11y: { role: 'menu' },
1223
+ position: {
1308
1224
  placement: ['bottom-start', 'bottom-end', 'top-start', 'top-end'],
1309
1225
  offset: 4,
1310
1226
  flip: true,
1311
1227
  shift: true,
1312
- });
1228
+ },
1313
1229
  };
1314
- /**
1315
- * Menu preset for hover-driven menus (context menus, cascading flyouts).
1316
- *
1317
- * Enables hoverTree dismissal so a parent overlay stays open while the pointer
1318
- * is inside any child overlay opened via openChild().
1319
- */
1320
- const coarHoverMenuPreset = (b) => {
1321
- coarMenuPreset(b);
1322
- b.dismiss({
1230
+ const coarHoverMenuPreset = {
1231
+ ...coarMenuPreset,
1232
+ dismiss: {
1323
1233
  outsideClick: true,
1324
1234
  escapeKey: true,
1325
1235
  hoverTree: { enabled: true, delayMs: 300 },
1326
- });
1236
+ },
1327
1237
  };
1328
1238
 
1239
+ // Public API: one way to create & open overlays.
1240
+
1329
1241
  /**
1330
1242
  * Generated bundle index. Do not edit.
1331
1243
  */
1332
1244
 
1333
- export { COAR_MENU_PARENT, COAR_OVERLAY_DEFAULTS, COAR_OVERLAY_REF, COAR_OVERLAY_SPEC_RESOLVERS, CoarOverlay, CoarOverlayService, ContentBuilder, Overlay, OverlayBuilder, CoarOverlayService as OverlayService, coarHoverMenuPreset, coarMenuPreset, coarModalPreset, coarTooltipPreset, computeOverlayCoordinates, getAnchorRect, getContainerRect, getScrollParents, getViewportRect, coarHoverMenuPreset as hoverMenuPreset, coarMenuPreset as menuPreset, coarModalPreset as modalPreset, rectFromDom, rectFromPoint, rectFromVirtual, coarTooltipPreset as tooltipPreset };
1245
+ export { COAR_MENU_PARENT, COAR_OVERLAY_DEFAULTS, COAR_OVERLAY_REF, COAR_OVERLAY_SPEC_RESOLVERS, CoarOverlayOpenBuilder, coarHoverMenuPreset, coarMenuPreset, coarModalPreset, coarTooltipPreset, createOverlayBuilder };
1334
1246
  //# sourceMappingURL=cocoar-ui-overlay.mjs.map