@cocoar/ui 0.1.0-beta.99 → 0.1.1
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 +21 -11
- package/fesm2022/cocoar-ui-components.mjs +9549 -0
- package/fesm2022/cocoar-ui-components.mjs.map +1 -0
- package/fesm2022/cocoar-ui-menu.mjs +1082 -0
- package/fesm2022/cocoar-ui-menu.mjs.map +1 -0
- package/fesm2022/cocoar-ui-overlay.mjs +1284 -0
- package/fesm2022/cocoar-ui-overlay.mjs.map +1 -0
- package/fesm2022/cocoar-ui.mjs +8 -0
- package/fesm2022/cocoar-ui.mjs.map +1 -0
- package/llms-full.txt +2303 -0
- package/llms.txt +82 -0
- package/package.json +38 -19
- package/styles/all.css +9 -0
- package/styles/components.css +127 -0
- package/styles/tokens/all.css +38 -0
- package/styles/tokens/code-block.css +72 -0
- package/styles/tokens/colors-primitives-dark.css +84 -0
- package/styles/tokens/colors-primitives-light.css +75 -0
- package/styles/tokens/colors-usage.css +272 -0
- package/styles/tokens/components-shared.css +42 -0
- package/styles/tokens/elevation.css +30 -0
- package/styles/tokens/focus.css +30 -0
- package/styles/tokens/layers.css +17 -0
- package/styles/tokens/menu.css +53 -0
- package/styles/tokens/motion.css +93 -0
- package/styles/tokens/new-components.css +104 -0
- package/styles/tokens/radius.css +15 -0
- package/styles/tokens/select-overlay.css +40 -0
- package/styles/tokens/shadows.css +38 -0
- package/styles/tokens/sidebar.css +67 -0
- package/styles/tokens/spacing.css +16 -0
- package/styles/tokens/stroke-width.css +12 -0
- package/styles/tokens/type-primitives.css +23 -0
- package/styles/tokens/typography-responsive.css +44 -0
- package/styles/tokens/typography.css +41 -0
- package/types/cocoar-ui-components.d.ts +3719 -0
- package/types/cocoar-ui-menu.d.ts +326 -0
- package/types/cocoar-ui-overlay.d.ts +301 -0
- package/types/cocoar-ui.d.ts +3 -0
- package/src/index.d.ts +0 -4
- package/src/index.d.ts.map +0 -1
- package/src/index.js +0 -5
- package/src/index.js.map +0 -1
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, Injector, createEnvironmentInjector, createComponent, inject, ApplicationRef, EnvironmentInjector, Injectable } from '@angular/core';
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Multi-provider token. Resolvers are applied in registration order.
|
|
7
|
+
*/
|
|
8
|
+
const COAR_OVERLAY_SPEC_RESOLVERS = new InjectionToken('COAR_OVERLAY_SPEC_RESOLVERS');
|
|
9
|
+
const COAR_OVERLAY_DEFAULTS = {
|
|
10
|
+
anchor: { kind: 'virtual', placement: 'center' },
|
|
11
|
+
position: {
|
|
12
|
+
placement: ['top', 'bottom', 'left', 'right'],
|
|
13
|
+
offset: 8,
|
|
14
|
+
flip: true,
|
|
15
|
+
shift: true,
|
|
16
|
+
},
|
|
17
|
+
backdrop: { kind: 'none' },
|
|
18
|
+
scroll: { strategy: 'reposition' },
|
|
19
|
+
dismiss: { outsideClick: true, escapeKey: true },
|
|
20
|
+
focus: { trap: false, restore: true },
|
|
21
|
+
a11y: {},
|
|
22
|
+
attachment: { strategy: 'body' },
|
|
23
|
+
};
|
|
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
|
+
|
|
39
|
+
function getViewportRect() {
|
|
40
|
+
const docEl = document.documentElement;
|
|
41
|
+
const width = docEl?.clientWidth || window.innerWidth;
|
|
42
|
+
const height = docEl?.clientHeight || window.innerHeight;
|
|
43
|
+
return { width, height };
|
|
44
|
+
}
|
|
45
|
+
function getContainerRect(container) {
|
|
46
|
+
return rectFromDom(container.getBoundingClientRect());
|
|
47
|
+
}
|
|
48
|
+
function rectFromDom(domRect) {
|
|
49
|
+
return {
|
|
50
|
+
left: domRect.left,
|
|
51
|
+
top: domRect.top,
|
|
52
|
+
right: domRect.right,
|
|
53
|
+
bottom: domRect.bottom,
|
|
54
|
+
width: domRect.width,
|
|
55
|
+
height: domRect.height,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function getAnchorRect(anchor, viewport) {
|
|
59
|
+
switch (anchor.kind) {
|
|
60
|
+
case 'element':
|
|
61
|
+
return rectFromDom(anchor.element.getBoundingClientRect());
|
|
62
|
+
case 'point':
|
|
63
|
+
return rectFromPoint(anchor, 0, 0);
|
|
64
|
+
case 'virtual':
|
|
65
|
+
return rectFromVirtual(anchor, viewport);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function rectFromPoint(point, width, height) {
|
|
69
|
+
const left = point.x;
|
|
70
|
+
const top = point.y;
|
|
71
|
+
return {
|
|
72
|
+
left,
|
|
73
|
+
top,
|
|
74
|
+
right: left + width,
|
|
75
|
+
bottom: top + height,
|
|
76
|
+
width,
|
|
77
|
+
height,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function rectFromVirtual(spec, viewport) {
|
|
81
|
+
if (spec.placement === 'center') {
|
|
82
|
+
const x = viewport.width / 2;
|
|
83
|
+
const y = viewport.height / 2;
|
|
84
|
+
return rectFromPoint({ x, y }, 0, 0);
|
|
85
|
+
}
|
|
86
|
+
if (spec.placement === 'top') {
|
|
87
|
+
const x = viewport.width / 2;
|
|
88
|
+
const y = 0;
|
|
89
|
+
return rectFromPoint({ x, y }, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
const x = viewport.width / 2;
|
|
92
|
+
const y = viewport.height;
|
|
93
|
+
return rectFromPoint({ x, y }, 0, 0);
|
|
94
|
+
}
|
|
95
|
+
function getScrollParents(element) {
|
|
96
|
+
const result = [];
|
|
97
|
+
let current = element;
|
|
98
|
+
while (current && current.parentElement) {
|
|
99
|
+
current = current.parentElement;
|
|
100
|
+
const style = getComputedStyle(current);
|
|
101
|
+
const overflowY = style.overflowY;
|
|
102
|
+
const overflowX = style.overflowX;
|
|
103
|
+
const scrollable = overflowY === 'auto' ||
|
|
104
|
+
overflowY === 'scroll' ||
|
|
105
|
+
overflowX === 'auto' ||
|
|
106
|
+
overflowX === 'scroll';
|
|
107
|
+
if (scrollable) {
|
|
108
|
+
result.push(current);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
result.push(window);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function computeOverlayCoordinates(anchorRect, overlaySize, position, viewport, boundaryRect) {
|
|
115
|
+
const placements = Array.isArray(position.placement)
|
|
116
|
+
? position.placement
|
|
117
|
+
: [position.placement];
|
|
118
|
+
const offset = position.offset ?? 0;
|
|
119
|
+
const allowFlip = position.flip ?? false;
|
|
120
|
+
const allowShift = position.shift ?? false;
|
|
121
|
+
// Use container boundaries if provided, otherwise use viewport
|
|
122
|
+
const boundary = boundaryRect ?? { left: 0, top: 0, right: viewport.width, bottom: viewport.height, width: viewport.width, height: viewport.height };
|
|
123
|
+
const candidates = placements.map((placement) => ({
|
|
124
|
+
placement,
|
|
125
|
+
coords: coordsForPlacement(anchorRect, overlaySize, placement, offset),
|
|
126
|
+
}));
|
|
127
|
+
if (allowFlip) {
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const fits = fitsInBoundary(candidate.coords, overlaySize, boundary);
|
|
130
|
+
if (fits) {
|
|
131
|
+
return allowShift
|
|
132
|
+
? { ...shiftIntoBoundary(candidate.coords, overlaySize, boundary), placement: candidate.placement }
|
|
133
|
+
: { ...candidate.coords, placement: candidate.placement };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Choose the candidate with the smallest total overflow.
|
|
138
|
+
let best = candidates[0];
|
|
139
|
+
let bestOverflow = Number.POSITIVE_INFINITY;
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
const overflow = totalOverflowFromBoundary(candidate.coords, overlaySize, boundary);
|
|
142
|
+
if (overflow < bestOverflow) {
|
|
143
|
+
best = candidate;
|
|
144
|
+
bestOverflow = overflow;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const chosen = best?.coords ??
|
|
148
|
+
coordsForPlacement(anchorRect, overlaySize, placements[0] ?? 'bottom', offset);
|
|
149
|
+
const shifted = allowShift ? shiftIntoBoundary(chosen, overlaySize, boundary) : chosen;
|
|
150
|
+
return { ...shifted, placement: best?.placement ?? (placements[0] ?? 'bottom') };
|
|
151
|
+
}
|
|
152
|
+
function coordsForPlacement(anchorRect, overlaySize, placement, offset) {
|
|
153
|
+
const anchorCenterX = anchorRect.left + anchorRect.width / 2;
|
|
154
|
+
const anchorCenterY = anchorRect.top + anchorRect.height / 2;
|
|
155
|
+
switch (placement) {
|
|
156
|
+
case 'center':
|
|
157
|
+
return {
|
|
158
|
+
left: anchorCenterX - overlaySize.width / 2,
|
|
159
|
+
top: anchorCenterY - overlaySize.height / 2,
|
|
160
|
+
};
|
|
161
|
+
case 'top':
|
|
162
|
+
return {
|
|
163
|
+
left: anchorCenterX - overlaySize.width / 2,
|
|
164
|
+
top: anchorRect.top - overlaySize.height - offset,
|
|
165
|
+
};
|
|
166
|
+
case 'top-start':
|
|
167
|
+
return {
|
|
168
|
+
left: anchorRect.left,
|
|
169
|
+
top: anchorRect.top - overlaySize.height - offset,
|
|
170
|
+
};
|
|
171
|
+
case 'top-end':
|
|
172
|
+
return {
|
|
173
|
+
left: anchorRect.right - overlaySize.width,
|
|
174
|
+
top: anchorRect.top - overlaySize.height - offset,
|
|
175
|
+
};
|
|
176
|
+
case 'bottom':
|
|
177
|
+
return {
|
|
178
|
+
left: anchorCenterX - overlaySize.width / 2,
|
|
179
|
+
top: anchorRect.bottom + offset,
|
|
180
|
+
};
|
|
181
|
+
case 'bottom-start':
|
|
182
|
+
return {
|
|
183
|
+
left: anchorRect.left,
|
|
184
|
+
top: anchorRect.bottom + offset,
|
|
185
|
+
};
|
|
186
|
+
case 'bottom-end':
|
|
187
|
+
return {
|
|
188
|
+
left: anchorRect.right - overlaySize.width,
|
|
189
|
+
top: anchorRect.bottom + offset,
|
|
190
|
+
};
|
|
191
|
+
case 'left':
|
|
192
|
+
return {
|
|
193
|
+
left: anchorRect.left - overlaySize.width - offset,
|
|
194
|
+
top: anchorCenterY - overlaySize.height / 2,
|
|
195
|
+
};
|
|
196
|
+
case 'left-start':
|
|
197
|
+
return {
|
|
198
|
+
left: anchorRect.left - overlaySize.width - offset,
|
|
199
|
+
top: anchorRect.top,
|
|
200
|
+
};
|
|
201
|
+
case 'left-end':
|
|
202
|
+
return {
|
|
203
|
+
left: anchorRect.left - overlaySize.width - offset,
|
|
204
|
+
top: anchorRect.bottom - overlaySize.height,
|
|
205
|
+
};
|
|
206
|
+
case 'right':
|
|
207
|
+
return {
|
|
208
|
+
left: anchorRect.right + offset,
|
|
209
|
+
top: anchorCenterY - overlaySize.height / 2,
|
|
210
|
+
};
|
|
211
|
+
case 'right-start':
|
|
212
|
+
return {
|
|
213
|
+
left: anchorRect.right + offset,
|
|
214
|
+
top: anchorRect.top,
|
|
215
|
+
};
|
|
216
|
+
case 'right-end':
|
|
217
|
+
return {
|
|
218
|
+
left: anchorRect.right + offset,
|
|
219
|
+
top: anchorRect.bottom - overlaySize.height,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function fitsInBoundary(coords, overlaySize, boundary) {
|
|
224
|
+
return (coords.left >= boundary.left &&
|
|
225
|
+
coords.top >= boundary.top &&
|
|
226
|
+
coords.left + overlaySize.width <= boundary.right &&
|
|
227
|
+
coords.top + overlaySize.height <= boundary.bottom);
|
|
228
|
+
}
|
|
229
|
+
function shiftIntoBoundary(coords, overlaySize, boundary) {
|
|
230
|
+
const maxLeft = Math.max(boundary.left, boundary.right - overlaySize.width);
|
|
231
|
+
const maxTop = Math.max(boundary.top, boundary.bottom - overlaySize.height);
|
|
232
|
+
return {
|
|
233
|
+
left: clamp(coords.left, boundary.left, maxLeft),
|
|
234
|
+
top: clamp(coords.top, boundary.top, maxTop),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function totalOverflowFromBoundary(coords, overlaySize, boundary) {
|
|
238
|
+
const leftOverflow = Math.max(0, boundary.left - coords.left);
|
|
239
|
+
const topOverflow = Math.max(0, boundary.top - coords.top);
|
|
240
|
+
const rightOverflow = Math.max(0, coords.left + overlaySize.width - boundary.right);
|
|
241
|
+
const bottomOverflow = Math.max(0, coords.top + overlaySize.height - boundary.bottom);
|
|
242
|
+
return leftOverflow + topOverflow + rightOverflow + bottomOverflow;
|
|
243
|
+
}
|
|
244
|
+
function clamp(value, min, max) {
|
|
245
|
+
if (value < min)
|
|
246
|
+
return min;
|
|
247
|
+
if (value > max)
|
|
248
|
+
return max;
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
|
|
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.host.setAttribute('popover', 'manual');
|
|
293
|
+
this.panel = document.createElement('div');
|
|
294
|
+
this.panel.className = 'coar-overlay-panel';
|
|
295
|
+
// Apply custom panel class(es) if provided
|
|
296
|
+
if (this.spec.panelClass) {
|
|
297
|
+
const classes = Array.isArray(this.spec.panelClass)
|
|
298
|
+
? this.spec.panelClass
|
|
299
|
+
: [this.spec.panelClass];
|
|
300
|
+
for (const cls of classes) {
|
|
301
|
+
if (cls)
|
|
302
|
+
this.panel.classList.add(cls);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this.host.appendChild(this.panel);
|
|
306
|
+
this.shouldAnimateMenu = this.spec.a11y.role === 'menu';
|
|
307
|
+
this.contentInjector = Injector.create({
|
|
308
|
+
providers: [
|
|
309
|
+
{ provide: COAR_OVERLAY_REF, useValue: this },
|
|
310
|
+
{ provide: COAR_MENU_PARENT, useValue: this },
|
|
311
|
+
],
|
|
312
|
+
parent: this.environmentInjector,
|
|
313
|
+
});
|
|
314
|
+
this.contentEnvironmentInjector = createEnvironmentInjector([
|
|
315
|
+
{ provide: COAR_OVERLAY_REF, useValue: this },
|
|
316
|
+
{ provide: COAR_MENU_PARENT, useValue: this },
|
|
317
|
+
], this.environmentInjector);
|
|
318
|
+
Object.assign(this.host.style, {
|
|
319
|
+
position: 'fixed',
|
|
320
|
+
inset: 'unset',
|
|
321
|
+
top: '0px',
|
|
322
|
+
left: '0px',
|
|
323
|
+
margin: '0',
|
|
324
|
+
border: 'none',
|
|
325
|
+
padding: '0',
|
|
326
|
+
background: 'transparent',
|
|
327
|
+
overflow: 'visible',
|
|
328
|
+
transform: 'translate3d(0px, 0px, 0px)',
|
|
329
|
+
zIndex: `calc(var(--coar-z-overlay, 1000) + ${this.stackIndex * 2})`,
|
|
330
|
+
opacity: '1',
|
|
331
|
+
pointerEvents: 'none',
|
|
332
|
+
});
|
|
333
|
+
if (this.shouldAnimateMenu) {
|
|
334
|
+
Object.assign(this.panel.style, {
|
|
335
|
+
opacity: '0',
|
|
336
|
+
transition: 'opacity var(--coar-duration-slower) var(--coar-ease-out)',
|
|
337
|
+
willChange: 'opacity',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
this.restoreFocusTarget =
|
|
341
|
+
(typeof document !== 'undefined' ? document.activeElement : null) ?? null;
|
|
342
|
+
if (this.parent) {
|
|
343
|
+
this.parent.children.add(this);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
getHoverTreeDismissConfig() {
|
|
347
|
+
return this.spec.dismiss.hoverTree;
|
|
348
|
+
}
|
|
349
|
+
getPanelElement() {
|
|
350
|
+
return this.panel;
|
|
351
|
+
}
|
|
352
|
+
getRoot() {
|
|
353
|
+
return this.parent?.getRoot() ?? this;
|
|
354
|
+
}
|
|
355
|
+
closeChildren(exclude) {
|
|
356
|
+
for (const child of Array.from(this.children)) {
|
|
357
|
+
if (child !== exclude) {
|
|
358
|
+
child.closeChildren();
|
|
359
|
+
child.close();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
open() {
|
|
364
|
+
const backdrop = this.spec.backdrop;
|
|
365
|
+
if (backdrop.kind === 'modal') {
|
|
366
|
+
const backdropEl = document.createElement('div');
|
|
367
|
+
backdropEl.className = 'coar-overlay-backdrop';
|
|
368
|
+
backdropEl.setAttribute('popover', 'manual');
|
|
369
|
+
Object.assign(backdropEl.style, {
|
|
370
|
+
position: 'fixed',
|
|
371
|
+
inset: '0',
|
|
372
|
+
margin: '0',
|
|
373
|
+
border: 'none',
|
|
374
|
+
padding: '0',
|
|
375
|
+
overflow: 'visible',
|
|
376
|
+
background: 'color-mix(in srgb, var(--coar-color-black) 40%, transparent)',
|
|
377
|
+
zIndex: `calc(var(--coar-z-overlay-backdrop, 999) + ${this.stackIndex * 2})`,
|
|
378
|
+
});
|
|
379
|
+
document.body.appendChild(backdropEl);
|
|
380
|
+
if ('showPopover' in backdropEl)
|
|
381
|
+
backdropEl.showPopover();
|
|
382
|
+
this.backdropElement = backdropEl;
|
|
383
|
+
if (backdrop.closeOnBackdropClick !== false) {
|
|
384
|
+
const onClick = (e) => {
|
|
385
|
+
if (e.target === backdropEl) {
|
|
386
|
+
this.close();
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
backdropEl.addEventListener('click', onClick);
|
|
390
|
+
this.cleanupFns.push(() => backdropEl.removeEventListener('click', onClick));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const attachment = this.spec.attachment;
|
|
394
|
+
const attachmentParent = attachment.strategy === 'parent' ? attachment.container : document.body;
|
|
395
|
+
attachmentParent.appendChild(this.host);
|
|
396
|
+
if ('showPopover' in this.host)
|
|
397
|
+
this.host.showPopover();
|
|
398
|
+
this.installHoverTreeDismissIfEnabled();
|
|
399
|
+
this.applyA11y();
|
|
400
|
+
this.destroyContent = this.renderContent(this.spec.content, this.panel, this.inputs);
|
|
401
|
+
this.applySize();
|
|
402
|
+
if (this.spec.focus.trap) {
|
|
403
|
+
this.installFocusTrap();
|
|
404
|
+
}
|
|
405
|
+
this.installRepositionTriggers();
|
|
406
|
+
this.updatePosition();
|
|
407
|
+
}
|
|
408
|
+
installHoverTreeDismissIfEnabled() {
|
|
409
|
+
const hoverTree = this.spec.dismiss.hoverTree;
|
|
410
|
+
if (!hoverTree?.enabled)
|
|
411
|
+
return;
|
|
412
|
+
const delayMs = typeof hoverTree.delayMs === 'number' ? hoverTree.delayMs : 300;
|
|
413
|
+
const onEnter = () => {
|
|
414
|
+
this.cancelHoverCloseUpTree();
|
|
415
|
+
};
|
|
416
|
+
const onHostLeave = () => {
|
|
417
|
+
this.scheduleHoverCloseUpTree(delayMs);
|
|
418
|
+
};
|
|
419
|
+
const onAnchorLeave = () => {
|
|
420
|
+
this.scheduleHoverClose(delayMs);
|
|
421
|
+
};
|
|
422
|
+
this.host.addEventListener('pointerenter', onEnter);
|
|
423
|
+
this.host.addEventListener('pointerleave', onHostLeave);
|
|
424
|
+
this.cleanupFns.push(() => {
|
|
425
|
+
this.host.removeEventListener('pointerenter', onEnter);
|
|
426
|
+
this.host.removeEventListener('pointerleave', onHostLeave);
|
|
427
|
+
this.cancelHoverClose();
|
|
428
|
+
});
|
|
429
|
+
if (this.spec.anchor.kind === 'element') {
|
|
430
|
+
const el = this.spec.anchor.element;
|
|
431
|
+
el.addEventListener('pointerenter', onEnter);
|
|
432
|
+
el.addEventListener('pointerleave', onAnchorLeave);
|
|
433
|
+
this.cleanupFns.push(() => {
|
|
434
|
+
el.removeEventListener('pointerenter', onEnter);
|
|
435
|
+
el.removeEventListener('pointerleave', onAnchorLeave);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
cancelHoverClose() {
|
|
440
|
+
if (!this.hoverCloseTimer)
|
|
441
|
+
return;
|
|
442
|
+
clearTimeout(this.hoverCloseTimer);
|
|
443
|
+
this.hoverCloseTimer = null;
|
|
444
|
+
}
|
|
445
|
+
cancelHoverCloseUpTree() {
|
|
446
|
+
this.cancelHoverClose();
|
|
447
|
+
this.parent?.cancelHoverCloseUpTree();
|
|
448
|
+
}
|
|
449
|
+
scheduleHoverClose(delayMs) {
|
|
450
|
+
const hoverTree = this.spec.dismiss.hoverTree;
|
|
451
|
+
if (!hoverTree?.enabled)
|
|
452
|
+
return;
|
|
453
|
+
this.cancelHoverClose();
|
|
454
|
+
this.hoverCloseTimer = setTimeout(() => {
|
|
455
|
+
this.hoverCloseTimer = null;
|
|
456
|
+
this.close();
|
|
457
|
+
}, delayMs);
|
|
458
|
+
}
|
|
459
|
+
scheduleHoverCloseUpTree(delayMs) {
|
|
460
|
+
this.scheduleHoverClose(delayMs);
|
|
461
|
+
this.parent?.scheduleHoverCloseUpTree(delayMs);
|
|
462
|
+
}
|
|
463
|
+
hasFocusTrap() {
|
|
464
|
+
return this.spec.focus.trap === true;
|
|
465
|
+
}
|
|
466
|
+
isDismissable(kind) {
|
|
467
|
+
if (kind === 'outsideClick') {
|
|
468
|
+
return this.spec.dismiss.outsideClick !== false;
|
|
469
|
+
}
|
|
470
|
+
return this.spec.dismiss.escapeKey !== false;
|
|
471
|
+
}
|
|
472
|
+
containsEventTarget(target) {
|
|
473
|
+
if (!(target instanceof Node))
|
|
474
|
+
return false;
|
|
475
|
+
if (this.host.contains(target))
|
|
476
|
+
return true;
|
|
477
|
+
if (this.backdropElement?.contains(target))
|
|
478
|
+
return true;
|
|
479
|
+
if (this.spec.anchor.kind === 'element') {
|
|
480
|
+
if (this.spec.anchor.element.contains(target))
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
handleTabKey(event) {
|
|
486
|
+
if (!this.spec.focus.trap)
|
|
487
|
+
return false;
|
|
488
|
+
const focusables = this.getFocusableElements();
|
|
489
|
+
if (focusables.length === 0) {
|
|
490
|
+
this.focusElement(this.host);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
const first = focusables[0];
|
|
494
|
+
const last = focusables[focusables.length - 1];
|
|
495
|
+
const active = document.activeElement;
|
|
496
|
+
const isActiveInside = active instanceof Node && (this.host.contains(active) || active === this.host);
|
|
497
|
+
if (!isActiveInside) {
|
|
498
|
+
this.focusElement(event.shiftKey ? last : first);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
if (event.shiftKey) {
|
|
502
|
+
if (active === first || active === this.host) {
|
|
503
|
+
this.focusElement(last);
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (active === last) {
|
|
509
|
+
this.focusElement(first);
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
updatePosition() {
|
|
515
|
+
if (this.closed)
|
|
516
|
+
return;
|
|
517
|
+
if (this.rafPending)
|
|
518
|
+
return;
|
|
519
|
+
this.rafPending = true;
|
|
520
|
+
this.schedule(() => {
|
|
521
|
+
this.rafPending = false;
|
|
522
|
+
if (this.closed)
|
|
523
|
+
return;
|
|
524
|
+
const viewport = getViewportRect();
|
|
525
|
+
const anchorRect = getAnchorRect(this.spec.anchor, viewport);
|
|
526
|
+
const rect = this.host.getBoundingClientRect();
|
|
527
|
+
const overlaySize = {
|
|
528
|
+
width: rect.width,
|
|
529
|
+
height: rect.height,
|
|
530
|
+
};
|
|
531
|
+
const attachment = this.spec.attachment;
|
|
532
|
+
const boundaryRect = attachment.strategy === 'parent' ? getContainerRect(attachment.container) : undefined;
|
|
533
|
+
const coords = computeOverlayCoordinates(anchorRect, overlaySize, this.spec.position, viewport, boundaryRect);
|
|
534
|
+
this.host.style.transform = `translate3d(${Math.round(coords.left)}px, ${Math.round(coords.top)}px, 0px)`;
|
|
535
|
+
this.present();
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
present() {
|
|
539
|
+
if (this.presented)
|
|
540
|
+
return;
|
|
541
|
+
this.presented = true;
|
|
542
|
+
if (this.shouldAnimateMenu) {
|
|
543
|
+
void this.panel.getBoundingClientRect();
|
|
544
|
+
}
|
|
545
|
+
this.host.style.pointerEvents = 'auto';
|
|
546
|
+
if (this.shouldAnimateMenu) {
|
|
547
|
+
this.panel.style.opacity = '1';
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
close(result) {
|
|
551
|
+
if (this.closed)
|
|
552
|
+
return;
|
|
553
|
+
this.closed = true;
|
|
554
|
+
this.lastResult = result;
|
|
555
|
+
this.cancelHoverClose();
|
|
556
|
+
this.closeChildren();
|
|
557
|
+
for (const cleanup of this.cleanupFns) {
|
|
558
|
+
cleanup();
|
|
559
|
+
}
|
|
560
|
+
this.cleanupFns = [];
|
|
561
|
+
this.resizeObserver?.disconnect();
|
|
562
|
+
this.resizeObserver = null;
|
|
563
|
+
if (this.shouldAnimateMenu && this.presented) {
|
|
564
|
+
this.host.style.pointerEvents = 'none';
|
|
565
|
+
this.panel.style.opacity = '0';
|
|
566
|
+
const finalizeOnce = () => {
|
|
567
|
+
this.finalizeClose();
|
|
568
|
+
};
|
|
569
|
+
let fallbackTimer = null;
|
|
570
|
+
const onEnd = (e) => {
|
|
571
|
+
if (e.target === this.panel && e.propertyName === 'opacity') {
|
|
572
|
+
this.host.removeEventListener('transitionend', onEnd);
|
|
573
|
+
if (fallbackTimer) {
|
|
574
|
+
clearTimeout(fallbackTimer);
|
|
575
|
+
fallbackTimer = null;
|
|
576
|
+
}
|
|
577
|
+
finalizeOnce();
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
this.host.addEventListener('transitionend', onEnd);
|
|
581
|
+
const fallbackMs = this.getMaxTransitionTimeMs();
|
|
582
|
+
if (fallbackMs === 0) {
|
|
583
|
+
this.host.removeEventListener('transitionend', onEnd);
|
|
584
|
+
finalizeOnce();
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
fallbackTimer = setTimeout(() => {
|
|
588
|
+
this.host.removeEventListener('transitionend', onEnd);
|
|
589
|
+
finalizeOnce();
|
|
590
|
+
}, fallbackMs);
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
this.finalizeClose();
|
|
595
|
+
}
|
|
596
|
+
finalizeClose() {
|
|
597
|
+
if (this.closeFinalized)
|
|
598
|
+
return;
|
|
599
|
+
this.closeFinalized = true;
|
|
600
|
+
this.destroyContent?.();
|
|
601
|
+
this.destroyContent = null;
|
|
602
|
+
if ('hidePopover' in this.host)
|
|
603
|
+
this.host.hidePopover();
|
|
604
|
+
this.host.remove();
|
|
605
|
+
if (this.backdropElement) {
|
|
606
|
+
if ('hidePopover' in this.backdropElement)
|
|
607
|
+
this.backdropElement.hidePopover();
|
|
608
|
+
this.backdropElement.remove();
|
|
609
|
+
this.backdropElement = null;
|
|
610
|
+
}
|
|
611
|
+
if (this.spec.focus.restore !== false) {
|
|
612
|
+
const el = this.restoreFocusTarget;
|
|
613
|
+
if (el && typeof el.focus === 'function') {
|
|
614
|
+
try {
|
|
615
|
+
el.focus({ preventScroll: true });
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
try {
|
|
619
|
+
el.focus();
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
// noop
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
this.afterClosedSubject.next(this.lastResult);
|
|
628
|
+
this.afterClosedSubject.complete();
|
|
629
|
+
this.parent?.children.delete(this);
|
|
630
|
+
this.onClosed();
|
|
631
|
+
}
|
|
632
|
+
getMaxTransitionTimeMs() {
|
|
633
|
+
if (typeof getComputedStyle === 'undefined')
|
|
634
|
+
return 0;
|
|
635
|
+
const style = getComputedStyle(this.panel);
|
|
636
|
+
const durations = style.transitionDuration.split(',').map((v) => v.trim());
|
|
637
|
+
const delays = style.transitionDelay.split(',').map((v) => v.trim());
|
|
638
|
+
const entries = Math.max(durations.length, delays.length);
|
|
639
|
+
let maxMs = 0;
|
|
640
|
+
for (let i = 0; i < entries; i++) {
|
|
641
|
+
const duration = durations[i] ?? durations[durations.length - 1] ?? '0ms';
|
|
642
|
+
const delay = delays[i] ?? delays[delays.length - 1] ?? '0ms';
|
|
643
|
+
const ms = this.parseCssTimeToMs(duration) + this.parseCssTimeToMs(delay);
|
|
644
|
+
maxMs = Math.max(maxMs, ms);
|
|
645
|
+
}
|
|
646
|
+
return maxMs;
|
|
647
|
+
}
|
|
648
|
+
parseCssTimeToMs(value) {
|
|
649
|
+
const v = value.trim();
|
|
650
|
+
if (!v)
|
|
651
|
+
return 0;
|
|
652
|
+
if (v.endsWith('ms')) {
|
|
653
|
+
const n = Number(v.slice(0, -2));
|
|
654
|
+
return Number.isFinite(n) ? n : 0;
|
|
655
|
+
}
|
|
656
|
+
if (v.endsWith('s')) {
|
|
657
|
+
const n = Number(v.slice(0, -1));
|
|
658
|
+
return Number.isFinite(n) ? n * 1000 : 0;
|
|
659
|
+
}
|
|
660
|
+
const n = Number(v);
|
|
661
|
+
return Number.isFinite(n) ? n : 0;
|
|
662
|
+
}
|
|
663
|
+
installFocusTrap() {
|
|
664
|
+
this.host.tabIndex = -1;
|
|
665
|
+
this.schedule(() => {
|
|
666
|
+
if (this.closed)
|
|
667
|
+
return;
|
|
668
|
+
const focusables = this.getFocusableElements();
|
|
669
|
+
this.focusElement(focusables[0] ?? this.host);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
applySize() {
|
|
673
|
+
const size = this.spec.size;
|
|
674
|
+
if (!size)
|
|
675
|
+
return;
|
|
676
|
+
const viewport = getViewportRect();
|
|
677
|
+
const resolveMin = (value, anchorSize) => {
|
|
678
|
+
if (value === 'anchor')
|
|
679
|
+
return anchorSize;
|
|
680
|
+
if (typeof value === 'number')
|
|
681
|
+
return value;
|
|
682
|
+
return null;
|
|
683
|
+
};
|
|
684
|
+
const resolveMax = (value, viewportSize) => {
|
|
685
|
+
if (value === 'viewport')
|
|
686
|
+
return viewportSize;
|
|
687
|
+
if (typeof value === 'number')
|
|
688
|
+
return value;
|
|
689
|
+
return null;
|
|
690
|
+
};
|
|
691
|
+
const anchorRect = getAnchorRect(this.spec.anchor, viewport);
|
|
692
|
+
const minWidthPx = resolveMin(size.minWidth, anchorRect.width);
|
|
693
|
+
const minHeightPx = resolveMin(size.minHeight, anchorRect.height);
|
|
694
|
+
const maxWidthPx = resolveMax(size.maxWidth, viewport.width);
|
|
695
|
+
const maxHeightPx = resolveMax(size.maxHeight, viewport.height);
|
|
696
|
+
this.host.style.width = '';
|
|
697
|
+
this.host.style.height = '';
|
|
698
|
+
this.host.style.minWidth = '';
|
|
699
|
+
this.host.style.minHeight = '';
|
|
700
|
+
this.host.style.maxWidth = '';
|
|
701
|
+
this.host.style.maxHeight = '';
|
|
702
|
+
this.host.style.overflow = '';
|
|
703
|
+
if (minWidthPx != null && minWidthPx > 0)
|
|
704
|
+
this.host.style.minWidth = `${minWidthPx}px`;
|
|
705
|
+
if (minHeightPx != null && minHeightPx > 0)
|
|
706
|
+
this.host.style.minHeight = `${minHeightPx}px`;
|
|
707
|
+
if (size.mode === 'content') {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (size.mode === 'content-clamped') {
|
|
711
|
+
if (maxWidthPx != null)
|
|
712
|
+
this.host.style.maxWidth = `${maxWidthPx}px`;
|
|
713
|
+
if (maxHeightPx != null)
|
|
714
|
+
this.host.style.maxHeight = `${maxHeightPx}px`;
|
|
715
|
+
this.host.style.overflow = 'auto';
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (maxWidthPx != null)
|
|
719
|
+
this.host.style.width = `${maxWidthPx}px`;
|
|
720
|
+
if (maxHeightPx != null)
|
|
721
|
+
this.host.style.height = `${maxHeightPx}px`;
|
|
722
|
+
this.host.style.overflow = 'auto';
|
|
723
|
+
}
|
|
724
|
+
applyA11y() {
|
|
725
|
+
const a11y = this.spec.a11y;
|
|
726
|
+
if (!a11y)
|
|
727
|
+
return;
|
|
728
|
+
if (a11y.role) {
|
|
729
|
+
this.host.setAttribute('role', a11y.role);
|
|
730
|
+
}
|
|
731
|
+
this.setAttrIfDefined('aria-label', a11y.label);
|
|
732
|
+
this.setAttrIfDefined('aria-labelledby', a11y.labelledBy);
|
|
733
|
+
this.setAttrIfDefined('aria-describedby', a11y.describedBy);
|
|
734
|
+
if (a11y.role === 'dialog' && this.spec.backdrop.kind === 'modal') {
|
|
735
|
+
this.host.setAttribute('aria-modal', 'true');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
setAttrIfDefined(name, value) {
|
|
739
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
740
|
+
this.host.setAttribute(name, value);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
this.host.removeAttribute(name);
|
|
744
|
+
}
|
|
745
|
+
focusElement(el) {
|
|
746
|
+
try {
|
|
747
|
+
el.focus({ preventScroll: true });
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
try {
|
|
751
|
+
el.focus();
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
// noop
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
getFocusableElements() {
|
|
759
|
+
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"]'));
|
|
760
|
+
return candidates.filter((el) => !this.isHidden(el));
|
|
761
|
+
}
|
|
762
|
+
isHidden(el) {
|
|
763
|
+
if (el.hasAttribute('hidden'))
|
|
764
|
+
return true;
|
|
765
|
+
if (el.getAttribute('aria-hidden') === 'true')
|
|
766
|
+
return true;
|
|
767
|
+
const style = el.style;
|
|
768
|
+
if (style.display === 'none' || style.visibility === 'hidden')
|
|
769
|
+
return true;
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
installRepositionTriggers() {
|
|
773
|
+
const strategy = this.spec.scroll.strategy;
|
|
774
|
+
if (strategy === 'noop')
|
|
775
|
+
return;
|
|
776
|
+
const scrollParents = this.spec.anchor.kind === 'element' ? getScrollParents(this.spec.anchor.element) : [window];
|
|
777
|
+
if (strategy === 'reposition') {
|
|
778
|
+
const onScroll = () => this.updatePosition();
|
|
779
|
+
const onResize = () => this.updatePosition();
|
|
780
|
+
for (const parent of scrollParents) {
|
|
781
|
+
if (parent === window) {
|
|
782
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
783
|
+
this.cleanupFns.push(() => window.removeEventListener('scroll', onScroll));
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
parent.addEventListener('scroll', onScroll, { passive: true });
|
|
787
|
+
this.cleanupFns.push(() => parent.removeEventListener('scroll', onScroll));
|
|
788
|
+
}
|
|
789
|
+
window.addEventListener('resize', onResize, { passive: true });
|
|
790
|
+
this.cleanupFns.push(() => window.removeEventListener('resize', onResize));
|
|
791
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
792
|
+
const ro = new ResizeObserver(() => this.updatePosition());
|
|
793
|
+
ro.observe(this.host);
|
|
794
|
+
this.resizeObserver = ro;
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (strategy === 'close') {
|
|
799
|
+
const onScroll = (e) => {
|
|
800
|
+
const target = e.target;
|
|
801
|
+
if (target instanceof Node && this.host.contains(target))
|
|
802
|
+
return;
|
|
803
|
+
this.close();
|
|
804
|
+
};
|
|
805
|
+
if (typeof document !== 'undefined') {
|
|
806
|
+
document.addEventListener('scroll', onScroll, { passive: true, capture: true });
|
|
807
|
+
this.cleanupFns.push(() => document.removeEventListener('scroll', onScroll, { capture: true }));
|
|
808
|
+
}
|
|
809
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
810
|
+
this.cleanupFns.push(() => window.removeEventListener('scroll', onScroll));
|
|
811
|
+
for (const parent of scrollParents) {
|
|
812
|
+
if (parent === window)
|
|
813
|
+
continue;
|
|
814
|
+
parent.addEventListener('scroll', onScroll, { passive: true });
|
|
815
|
+
this.cleanupFns.push(() => parent.removeEventListener('scroll', onScroll));
|
|
816
|
+
}
|
|
817
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
818
|
+
const ro = new ResizeObserver(() => this.updatePosition());
|
|
819
|
+
ro.observe(this.host);
|
|
820
|
+
this.resizeObserver = ro;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
schedule(fn) {
|
|
825
|
+
if (typeof requestAnimationFrame !== 'undefined') {
|
|
826
|
+
requestAnimationFrame(() => fn());
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
setTimeout(() => fn(), 0);
|
|
830
|
+
}
|
|
831
|
+
renderContent(content, host, inputs) {
|
|
832
|
+
switch (content.kind) {
|
|
833
|
+
case 'text': {
|
|
834
|
+
const text = inputs?.text;
|
|
835
|
+
if (typeof text !== 'string') {
|
|
836
|
+
throw new Error('Text overlay requires inputs: { text: string }');
|
|
837
|
+
}
|
|
838
|
+
host.textContent = text;
|
|
839
|
+
return () => {
|
|
840
|
+
host.textContent = '';
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
case 'template': {
|
|
844
|
+
const template = content.template;
|
|
845
|
+
if (!template)
|
|
846
|
+
throw new Error('Template overlay requires a template');
|
|
847
|
+
let templateContext = inputs;
|
|
848
|
+
if (inputs != null && typeof inputs === 'object' && !('$implicit' in inputs)) {
|
|
849
|
+
templateContext = {
|
|
850
|
+
...inputs,
|
|
851
|
+
$implicit: inputs,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const viewRef = template.createEmbeddedView(templateContext, this.contentInjector);
|
|
855
|
+
this.appRef.attachView(viewRef);
|
|
856
|
+
viewRef.detectChanges();
|
|
857
|
+
for (const node of viewRef.rootNodes) {
|
|
858
|
+
host.appendChild(node);
|
|
859
|
+
}
|
|
860
|
+
return () => {
|
|
861
|
+
this.appRef.detachView(viewRef);
|
|
862
|
+
viewRef.destroy();
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
case 'component': {
|
|
866
|
+
const component = content.component;
|
|
867
|
+
if (!component)
|
|
868
|
+
throw new Error('Component overlay requires a component');
|
|
869
|
+
const componentRef = createComponent(component, {
|
|
870
|
+
environmentInjector: this.contentEnvironmentInjector,
|
|
871
|
+
hostElement: host,
|
|
872
|
+
});
|
|
873
|
+
if (inputs && typeof inputs === 'object') {
|
|
874
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
875
|
+
componentRef.setInput(key, value);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
this.appRef.attachView(componentRef.hostView);
|
|
879
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
880
|
+
return () => {
|
|
881
|
+
this.appRef.detachView(componentRef.hostView);
|
|
882
|
+
componentRef.destroy();
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
class CoarOverlayService {
|
|
890
|
+
appRef = inject(ApplicationRef);
|
|
891
|
+
environmentInjector = inject(EnvironmentInjector);
|
|
892
|
+
specResolvers = inject(COAR_OVERLAY_SPEC_RESOLVERS, { optional: true }) ??
|
|
893
|
+
[];
|
|
894
|
+
openOverlays = new Set();
|
|
895
|
+
globalListenersInstalled = false;
|
|
896
|
+
onDocumentPointerDown = (event) => {
|
|
897
|
+
const overlays = this.getOpenOverlaysInOrder();
|
|
898
|
+
if (overlays.length === 0)
|
|
899
|
+
return;
|
|
900
|
+
const target = event.target;
|
|
901
|
+
const topmostContaining = this.getTopmostOverlayContainingTarget(target);
|
|
902
|
+
if (topmostContaining) {
|
|
903
|
+
// When interacting with an overlay, close any child overlays (submenus) that may be open.
|
|
904
|
+
topmostContaining.closeChildren();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const topmost = this.getTopmostDismissableOverlay('outsideClick');
|
|
908
|
+
if (!topmost)
|
|
909
|
+
return;
|
|
910
|
+
// For overlay trees (e.g. menus + submenus), outside-click should close the entire tree.
|
|
911
|
+
const root = topmost.getRoot();
|
|
912
|
+
if (root !== topmost && root.isDismissable('outsideClick')) {
|
|
913
|
+
root.close();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
topmost.close();
|
|
917
|
+
};
|
|
918
|
+
onDocumentKeyDown = (event) => {
|
|
919
|
+
if (event.key === 'Escape') {
|
|
920
|
+
const topmost = this.getTopmostDismissableOverlay('escapeKey');
|
|
921
|
+
if (!topmost)
|
|
922
|
+
return;
|
|
923
|
+
event.preventDefault();
|
|
924
|
+
event.stopPropagation();
|
|
925
|
+
topmost.close();
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (event.key === 'Tab') {
|
|
929
|
+
const topmost = this.getTopmostFocusTrappingOverlay();
|
|
930
|
+
if (!topmost)
|
|
931
|
+
return;
|
|
932
|
+
if (topmost.handleTabKey(event)) {
|
|
933
|
+
event.preventDefault();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
openTemplate(template, settings, inputs) {
|
|
938
|
+
const resolved = this.resolveSpec({
|
|
939
|
+
...settings,
|
|
940
|
+
content: { kind: 'template', template },
|
|
941
|
+
});
|
|
942
|
+
return this.attach(resolved, inputs, undefined);
|
|
943
|
+
}
|
|
944
|
+
openComponent(component, settings, inputs) {
|
|
945
|
+
const resolved = this.resolveSpec({
|
|
946
|
+
...settings,
|
|
947
|
+
content: { kind: 'component', component },
|
|
948
|
+
});
|
|
949
|
+
return this.attach(resolved, inputs, undefined);
|
|
950
|
+
}
|
|
951
|
+
openText(settings, inputs) {
|
|
952
|
+
const resolved = this.resolveSpec({
|
|
953
|
+
...settings,
|
|
954
|
+
content: { kind: 'text' },
|
|
955
|
+
});
|
|
956
|
+
return this.attach(resolved, inputs, undefined);
|
|
957
|
+
}
|
|
958
|
+
openTemplateAsChild(parent, template, settings, inputs, options) {
|
|
959
|
+
const resolved = this.resolveSpec({
|
|
960
|
+
...settings,
|
|
961
|
+
content: { kind: 'template', template },
|
|
962
|
+
});
|
|
963
|
+
return this.attach(resolved, inputs, { parent, closeSiblings: options?.closeSiblings });
|
|
964
|
+
}
|
|
965
|
+
openComponentAsChild(parent, component, settings, inputs, options) {
|
|
966
|
+
const resolved = this.resolveSpec({
|
|
967
|
+
...settings,
|
|
968
|
+
content: { kind: 'component', component },
|
|
969
|
+
});
|
|
970
|
+
return this.attach(resolved, inputs, { parent, closeSiblings: options?.closeSiblings });
|
|
971
|
+
}
|
|
972
|
+
openTextAsChild(parent, settings, inputs, options) {
|
|
973
|
+
const resolved = this.resolveSpec({
|
|
974
|
+
...settings,
|
|
975
|
+
content: { kind: 'text' },
|
|
976
|
+
});
|
|
977
|
+
return this.attach(resolved, inputs, { parent, closeSiblings: options?.closeSiblings });
|
|
978
|
+
}
|
|
979
|
+
closeAll() {
|
|
980
|
+
for (const ref of Array.from(this.openOverlays)) {
|
|
981
|
+
ref.close();
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
resolveSpec(spec) {
|
|
985
|
+
const resolvedSpec = this.applySpecResolvers(spec);
|
|
986
|
+
const content = resolvedSpec.content;
|
|
987
|
+
if (!content) {
|
|
988
|
+
throw new Error('OverlaySpec missing content');
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
content,
|
|
992
|
+
anchor: resolvedSpec.anchor ?? COAR_OVERLAY_DEFAULTS.anchor,
|
|
993
|
+
position: resolvedSpec.position ?? COAR_OVERLAY_DEFAULTS.position,
|
|
994
|
+
size: resolvedSpec.size ?? { mode: 'content' },
|
|
995
|
+
backdrop: resolvedSpec.backdrop ?? COAR_OVERLAY_DEFAULTS.backdrop,
|
|
996
|
+
scroll: resolvedSpec.scroll ?? COAR_OVERLAY_DEFAULTS.scroll,
|
|
997
|
+
dismiss: resolvedSpec.dismiss ?? COAR_OVERLAY_DEFAULTS.dismiss,
|
|
998
|
+
focus: resolvedSpec.focus ?? COAR_OVERLAY_DEFAULTS.focus,
|
|
999
|
+
a11y: resolvedSpec.a11y ?? COAR_OVERLAY_DEFAULTS.a11y,
|
|
1000
|
+
attachment: resolvedSpec.attachment ?? COAR_OVERLAY_DEFAULTS.attachment,
|
|
1001
|
+
panelClass: resolvedSpec.panelClass,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
applySpecResolvers(spec) {
|
|
1005
|
+
if (this.specResolvers.length === 0)
|
|
1006
|
+
return spec;
|
|
1007
|
+
let current = spec;
|
|
1008
|
+
for (const resolver of this.specResolvers) {
|
|
1009
|
+
const next = resolver(current);
|
|
1010
|
+
current = (next ?? current);
|
|
1011
|
+
}
|
|
1012
|
+
return current;
|
|
1013
|
+
}
|
|
1014
|
+
attach(spec, inputs, options) {
|
|
1015
|
+
const parent = this.getInternalRefOrNull(options?.parent);
|
|
1016
|
+
// Close sibling overlays if requested (close all existing children of parent)
|
|
1017
|
+
if (options?.closeSiblings && parent) {
|
|
1018
|
+
parent.closeChildren();
|
|
1019
|
+
}
|
|
1020
|
+
const stackIndex = this.openOverlays.size;
|
|
1021
|
+
const effectiveSpec = this.inheritDismissFromParent(spec, parent);
|
|
1022
|
+
const ref = new CoarOverlayRef(this.appRef, this.environmentInjector, effectiveSpec, this.mergeInputs(spec.content, inputs), stackIndex, parent, () => {
|
|
1023
|
+
this.openOverlays.delete(ref);
|
|
1024
|
+
this.uninstallGlobalListenersIfIdle();
|
|
1025
|
+
});
|
|
1026
|
+
this.openOverlays.add(ref);
|
|
1027
|
+
this.installGlobalListenersIfNeeded();
|
|
1028
|
+
ref.open();
|
|
1029
|
+
return ref;
|
|
1030
|
+
}
|
|
1031
|
+
inheritDismissFromParent(spec, parent) {
|
|
1032
|
+
if (!parent)
|
|
1033
|
+
return spec;
|
|
1034
|
+
const parentHoverTree = parent.getHoverTreeDismissConfig();
|
|
1035
|
+
if (!parentHoverTree?.enabled)
|
|
1036
|
+
return spec;
|
|
1037
|
+
const childHoverTree = spec.dismiss.hoverTree;
|
|
1038
|
+
// Explicit disable in child wins.
|
|
1039
|
+
if (childHoverTree?.enabled === false)
|
|
1040
|
+
return spec;
|
|
1041
|
+
let mergedHoverTree;
|
|
1042
|
+
if (!childHoverTree) {
|
|
1043
|
+
mergedHoverTree = { ...parentHoverTree };
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
mergedHoverTree = {
|
|
1047
|
+
enabled: true,
|
|
1048
|
+
delayMs: childHoverTree.delayMs ?? parentHoverTree.delayMs,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
// No change.
|
|
1052
|
+
if (mergedHoverTree === childHoverTree)
|
|
1053
|
+
return spec;
|
|
1054
|
+
return {
|
|
1055
|
+
...spec,
|
|
1056
|
+
dismiss: {
|
|
1057
|
+
...spec.dismiss,
|
|
1058
|
+
hoverTree: mergedHoverTree,
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
getInternalRefOrNull(ref) {
|
|
1063
|
+
if (!ref)
|
|
1064
|
+
return null;
|
|
1065
|
+
if (ref instanceof CoarOverlayRef)
|
|
1066
|
+
return ref;
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
installGlobalListenersIfNeeded() {
|
|
1070
|
+
if (this.globalListenersInstalled)
|
|
1071
|
+
return;
|
|
1072
|
+
if (typeof document === 'undefined')
|
|
1073
|
+
return;
|
|
1074
|
+
document.addEventListener('pointerdown', this.onDocumentPointerDown, { capture: true });
|
|
1075
|
+
document.addEventListener('keydown', this.onDocumentKeyDown, { capture: true });
|
|
1076
|
+
this.globalListenersInstalled = true;
|
|
1077
|
+
}
|
|
1078
|
+
uninstallGlobalListenersIfIdle() {
|
|
1079
|
+
if (!this.globalListenersInstalled)
|
|
1080
|
+
return;
|
|
1081
|
+
if (this.openOverlays.size > 0)
|
|
1082
|
+
return;
|
|
1083
|
+
if (typeof document === 'undefined')
|
|
1084
|
+
return;
|
|
1085
|
+
document.removeEventListener('pointerdown', this.onDocumentPointerDown, { capture: true });
|
|
1086
|
+
document.removeEventListener('keydown', this.onDocumentKeyDown, { capture: true });
|
|
1087
|
+
this.globalListenersInstalled = false;
|
|
1088
|
+
}
|
|
1089
|
+
getOpenOverlaysInOrder() {
|
|
1090
|
+
return Array.from(this.openOverlays);
|
|
1091
|
+
}
|
|
1092
|
+
getTopmostOverlayContainingTarget(target) {
|
|
1093
|
+
const overlays = this.getOpenOverlaysInOrder();
|
|
1094
|
+
for (let i = overlays.length - 1; i >= 0; i -= 1) {
|
|
1095
|
+
const overlay = overlays[i];
|
|
1096
|
+
if (overlay.containsEventTarget(target))
|
|
1097
|
+
return overlay;
|
|
1098
|
+
}
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
getTopmostDismissableOverlay(kind) {
|
|
1102
|
+
const overlays = this.getOpenOverlaysInOrder();
|
|
1103
|
+
for (let i = overlays.length - 1; i >= 0; i -= 1) {
|
|
1104
|
+
const overlay = overlays[i];
|
|
1105
|
+
if (overlay.isDismissable(kind)) {
|
|
1106
|
+
return overlay;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return null;
|
|
1110
|
+
}
|
|
1111
|
+
getTopmostFocusTrappingOverlay() {
|
|
1112
|
+
const overlays = this.getOpenOverlaysInOrder();
|
|
1113
|
+
for (let i = overlays.length - 1; i >= 0; i -= 1) {
|
|
1114
|
+
const overlay = overlays[i];
|
|
1115
|
+
if (overlay.hasFocusTrap()) {
|
|
1116
|
+
return overlay;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
mergeInputs(content, inputs) {
|
|
1122
|
+
if (!content.defaults)
|
|
1123
|
+
return inputs;
|
|
1124
|
+
if (inputs == null || typeof inputs !== 'object') {
|
|
1125
|
+
return { ...content.defaults };
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
...content.defaults,
|
|
1129
|
+
...inputs,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1133
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, providedIn: 'root' });
|
|
1134
|
+
}
|
|
1135
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarOverlayService, decorators: [{
|
|
1136
|
+
type: Injectable,
|
|
1137
|
+
args: [{ providedIn: 'root' }]
|
|
1138
|
+
}] });
|
|
1139
|
+
|
|
1140
|
+
class CoarOverlayOpenBuilder {
|
|
1141
|
+
service;
|
|
1142
|
+
settings;
|
|
1143
|
+
constructor(service, settings = {}) {
|
|
1144
|
+
this.service = service;
|
|
1145
|
+
this.settings = settings;
|
|
1146
|
+
}
|
|
1147
|
+
fork() {
|
|
1148
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings });
|
|
1149
|
+
}
|
|
1150
|
+
backdrop(value) {
|
|
1151
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, backdrop: value });
|
|
1152
|
+
}
|
|
1153
|
+
anchor(value) {
|
|
1154
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, anchor: value });
|
|
1155
|
+
}
|
|
1156
|
+
position(value) {
|
|
1157
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, position: value });
|
|
1158
|
+
}
|
|
1159
|
+
size(value) {
|
|
1160
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, size: value });
|
|
1161
|
+
}
|
|
1162
|
+
scroll(value) {
|
|
1163
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, scroll: value });
|
|
1164
|
+
}
|
|
1165
|
+
dismiss(value) {
|
|
1166
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, dismiss: value });
|
|
1167
|
+
}
|
|
1168
|
+
focus(value) {
|
|
1169
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, focus: value });
|
|
1170
|
+
}
|
|
1171
|
+
a11y(value) {
|
|
1172
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, a11y: value });
|
|
1173
|
+
}
|
|
1174
|
+
attachment(value) {
|
|
1175
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, attachment: value });
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Add CSS class(es) to the overlay panel element.
|
|
1179
|
+
* Useful for applying custom styles or size variants.
|
|
1180
|
+
*/
|
|
1181
|
+
panelClass(value) {
|
|
1182
|
+
return new CoarOverlayOpenBuilder(this.service, { ...this.settings, panelClass: value });
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Applies baseline settings, without overriding fields already set on this builder.
|
|
1186
|
+
* Useful for applying a preset after you've already configured part of the builder.
|
|
1187
|
+
*/
|
|
1188
|
+
withPreset(value) {
|
|
1189
|
+
const merged = { ...this.settings };
|
|
1190
|
+
merged.anchor ??= value.anchor;
|
|
1191
|
+
merged.position ??= value.position;
|
|
1192
|
+
merged.size ??= value.size;
|
|
1193
|
+
merged.backdrop ??= value.backdrop;
|
|
1194
|
+
merged.scroll ??= value.scroll;
|
|
1195
|
+
merged.dismiss ??= value.dismiss;
|
|
1196
|
+
merged.focus ??= value.focus;
|
|
1197
|
+
merged.a11y ??= value.a11y;
|
|
1198
|
+
merged.attachment ??= value.attachment;
|
|
1199
|
+
merged.panelClass ??= value.panelClass;
|
|
1200
|
+
return new CoarOverlayOpenBuilder(this.service, merged);
|
|
1201
|
+
}
|
|
1202
|
+
fromTemplate(template) {
|
|
1203
|
+
const settingsSnapshot = { ...this.settings };
|
|
1204
|
+
return {
|
|
1205
|
+
open: (inputs) => this.service.openTemplate(template, settingsSnapshot, inputs),
|
|
1206
|
+
openAsChild: (parent, inputs, options) => this.service.openTemplateAsChild(parent, template, settingsSnapshot, inputs, options),
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
fromComponent(component) {
|
|
1210
|
+
const settingsSnapshot = { ...this.settings };
|
|
1211
|
+
return {
|
|
1212
|
+
open: (inputs) => this.service.openComponent(component, settingsSnapshot, inputs),
|
|
1213
|
+
openAsChild: (parent, inputs, options) => this.service.openComponentAsChild(parent, component, settingsSnapshot, inputs, options),
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
fromText() {
|
|
1217
|
+
const settingsSnapshot = { ...this.settings };
|
|
1218
|
+
return {
|
|
1219
|
+
open: (inputs) => this.service.openText(settingsSnapshot, inputs),
|
|
1220
|
+
openAsChild: (parent, inputs, options) => this.service.openTextAsChild(parent, settingsSnapshot, inputs, options),
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function createOverlayBuilder(...initial) {
|
|
1225
|
+
const service = inject(CoarOverlayService);
|
|
1226
|
+
if (initial.length === 0) {
|
|
1227
|
+
return new CoarOverlayOpenBuilder(service);
|
|
1228
|
+
}
|
|
1229
|
+
const merged = Object.assign({}, ...initial);
|
|
1230
|
+
return new CoarOverlayOpenBuilder(service, merged);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const coarTooltipPreset = {
|
|
1234
|
+
backdrop: { kind: 'none' },
|
|
1235
|
+
scroll: { strategy: 'noop' },
|
|
1236
|
+
dismiss: { outsideClick: false, escapeKey: false },
|
|
1237
|
+
a11y: { role: 'tooltip' },
|
|
1238
|
+
position: {
|
|
1239
|
+
placement: ['top', 'bottom', 'left', 'right'],
|
|
1240
|
+
offset: 8,
|
|
1241
|
+
flip: true,
|
|
1242
|
+
shift: true,
|
|
1243
|
+
},
|
|
1244
|
+
attachment: { strategy: 'body' },
|
|
1245
|
+
};
|
|
1246
|
+
const coarModalPreset = {
|
|
1247
|
+
backdrop: { kind: 'modal', closeOnBackdropClick: true },
|
|
1248
|
+
anchor: { kind: 'virtual', placement: 'center' },
|
|
1249
|
+
focus: { trap: true, restore: true },
|
|
1250
|
+
size: { mode: 'content-clamped', maxWidth: 'viewport', maxHeight: 'viewport' },
|
|
1251
|
+
position: { placement: 'center', offset: 0, flip: false, shift: true },
|
|
1252
|
+
a11y: { role: 'dialog' },
|
|
1253
|
+
attachment: { strategy: 'body' },
|
|
1254
|
+
};
|
|
1255
|
+
const coarMenuPreset = {
|
|
1256
|
+
backdrop: { kind: 'none' },
|
|
1257
|
+
scroll: { strategy: 'close' },
|
|
1258
|
+
dismiss: { outsideClick: true, escapeKey: true },
|
|
1259
|
+
focus: { trap: false, restore: true },
|
|
1260
|
+
a11y: { role: 'menu' },
|
|
1261
|
+
position: {
|
|
1262
|
+
placement: ['bottom-start', 'bottom-end', 'top-start', 'top-end'],
|
|
1263
|
+
offset: 4,
|
|
1264
|
+
flip: true,
|
|
1265
|
+
shift: true,
|
|
1266
|
+
},
|
|
1267
|
+
};
|
|
1268
|
+
const coarHoverMenuPreset = {
|
|
1269
|
+
...coarMenuPreset,
|
|
1270
|
+
dismiss: {
|
|
1271
|
+
outsideClick: true,
|
|
1272
|
+
escapeKey: true,
|
|
1273
|
+
hoverTree: { enabled: true, delayMs: 300 },
|
|
1274
|
+
},
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// Public API: one way to create & open overlays.
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Generated bundle index. Do not edit.
|
|
1281
|
+
*/
|
|
1282
|
+
|
|
1283
|
+
export { COAR_MENU_PARENT, COAR_OVERLAY_DEFAULTS, COAR_OVERLAY_REF, COAR_OVERLAY_SPEC_RESOLVERS, CoarOverlayOpenBuilder, coarHoverMenuPreset, coarMenuPreset, coarModalPreset, coarTooltipPreset, createOverlayBuilder };
|
|
1284
|
+
//# sourceMappingURL=cocoar-ui-overlay.mjs.map
|