@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.
- package/README.md +3 -3
- package/fesm2022/cocoar-ui-overlay.mjs +888 -976
- package/fesm2022/cocoar-ui-overlay.mjs.map +1 -1
- package/package.json +1 -1
- package/types/cocoar-ui-overlay.d.ts +84 -89
|
@@ -1,123 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken,
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
421
|
-
return this.
|
|
329
|
+
getHoverTreeDismissConfig() {
|
|
330
|
+
return this.spec.dismiss.hoverTree;
|
|
422
331
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
ref.close();
|
|
426
|
-
}
|
|
332
|
+
getPanelElement() {
|
|
333
|
+
return this.panel;
|
|
427
334
|
}
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
471
|
-
|
|
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
|
-
|
|
495
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (typeof document === 'undefined')
|
|
385
|
+
installHoverTreeDismissIfEnabled() {
|
|
386
|
+
const hoverTree = this.spec.dismiss.hoverTree;
|
|
387
|
+
if (!hoverTree?.enabled)
|
|
516
388
|
return;
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
522
|
-
if (!this.
|
|
523
|
-
return;
|
|
524
|
-
if (this.openOverlays.size > 0)
|
|
416
|
+
cancelHoverClose() {
|
|
417
|
+
if (!this.hoverCloseTimer)
|
|
525
418
|
return;
|
|
526
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
430
|
+
this.cancelHoverClose();
|
|
431
|
+
this.hoverCloseTimer = setTimeout(() => {
|
|
432
|
+
this.hoverCloseTimer = null;
|
|
433
|
+
this.close();
|
|
434
|
+
}, delayMs);
|
|
531
435
|
}
|
|
532
|
-
|
|
533
|
-
|
|
436
|
+
scheduleHoverCloseUpTree(delayMs) {
|
|
437
|
+
this.scheduleHoverClose(delayMs);
|
|
438
|
+
this.parent?.scheduleHoverCloseUpTree(delayMs);
|
|
534
439
|
}
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
|
447
|
+
return this.spec.dismiss.escapeKey !== false;
|
|
553
448
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
460
|
+
return false;
|
|
563
461
|
}
|
|
564
|
-
|
|
565
|
-
if (!
|
|
566
|
-
return
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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.
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
772
|
-
|
|
592
|
+
catch {
|
|
593
|
+
// noop
|
|
594
|
+
}
|
|
595
|
+
}
|
|
773
596
|
}
|
|
774
597
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
787
|
-
this.updatePosition();
|
|
617
|
+
return maxMs;
|
|
788
618
|
}
|
|
789
|
-
|
|
790
|
-
const
|
|
791
|
-
if (!
|
|
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
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
824
|
-
|
|
695
|
+
applyA11y() {
|
|
696
|
+
const a11y = this.spec.a11y;
|
|
697
|
+
if (!a11y)
|
|
825
698
|
return;
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
this.
|
|
831
|
-
this.
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
714
|
+
this.host.removeAttribute(name);
|
|
855
715
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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 (
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
if (this.rafPending)
|
|
743
|
+
installRepositionTriggers() {
|
|
744
|
+
const strategy = this.spec.scroll.strategy;
|
|
745
|
+
if (strategy === 'noop')
|
|
905
746
|
return;
|
|
906
|
-
this.
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
941
|
-
if (
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
993
|
-
this.
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
this.
|
|
1017
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1048
|
-
return Number.isFinite(n) ? n : 0;
|
|
982
|
+
return current;
|
|
1049
983
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
if (typeof value === 'number')
|
|
1077
|
-
return value;
|
|
1031
|
+
}
|
|
1032
|
+
getInternalRefOrNull(ref) {
|
|
1033
|
+
if (!ref)
|
|
1078
1034
|
return null;
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
if (!a11y)
|
|
1048
|
+
uninstallGlobalListenersIfIdle() {
|
|
1049
|
+
if (!this.globalListenersInstalled)
|
|
1118
1050
|
return;
|
|
1119
|
-
if (
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
return candidates.filter((el) => !this.isHidden(el));
|
|
1059
|
+
getOpenOverlaysInOrder() {
|
|
1060
|
+
return Array.from(this.openOverlays);
|
|
1152
1061
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
-
|
|
1097
|
+
return {
|
|
1098
|
+
...content.defaults,
|
|
1099
|
+
...inputs,
|
|
1100
|
+
};
|
|
1225
1101
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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 =
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
1205
|
+
},
|
|
1206
|
+
attachment: { strategy: 'body' },
|
|
1289
1207
|
};
|
|
1290
|
-
const coarModalPreset =
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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 =
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
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,
|
|
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
|