@surdeddd/bottom-sheet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +377 -0
- package/dist/BottomSheetEngine-CvSVbo6c.d.cts +704 -0
- package/dist/BottomSheetEngine-DbO8rW_3.d.ts +704 -0
- package/dist/element.cjs +3442 -0
- package/dist/element.d.cts +88 -0
- package/dist/element.d.ts +88 -0
- package/dist/element.iife.global.js +3 -0
- package/dist/element.js +3442 -0
- package/dist/index.cjs +3693 -0
- package/dist/index.d.cts +184 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +3669 -0
- package/dist/integrations/formik.cjs +27 -0
- package/dist/integrations/formik.d.cts +27 -0
- package/dist/integrations/formik.d.ts +27 -0
- package/dist/integrations/formik.js +25 -0
- package/dist/integrations/react-hook-form.cjs +28 -0
- package/dist/integrations/react-hook-form.d.cts +30 -0
- package/dist/integrations/react-hook-form.d.ts +30 -0
- package/dist/integrations/react-hook-form.js +26 -0
- package/dist/overlay.cjs +1058 -0
- package/dist/overlay.d.cts +282 -0
- package/dist/overlay.d.ts +282 -0
- package/dist/overlay.js +1053 -0
- package/dist/preact.cjs +2 -0
- package/dist/preact.d.cts +5 -0
- package/dist/preact.d.ts +5 -0
- package/dist/preact.js +2 -0
- package/dist/qwik.cjs +3046 -0
- package/dist/qwik.d.cts +55 -0
- package/dist/qwik.d.ts +55 -0
- package/dist/qwik.js +3044 -0
- package/dist/react.cjs +4162 -0
- package/dist/react.d.cts +301 -0
- package/dist/react.d.ts +301 -0
- package/dist/react.js +4156 -0
- package/dist/solid.cjs +3063 -0
- package/dist/solid.d.cts +66 -0
- package/dist/solid.d.ts +66 -0
- package/dist/solid.js +3061 -0
- package/dist/styles.css +508 -0
- package/dist/svelte.cjs +3200 -0
- package/dist/svelte.d.ts +89 -0
- package/dist/svelte.js +3177 -0
- package/dist/themes/ios.css +24 -0
- package/dist/themes/material.css +24 -0
- package/dist/themes/vercel.css +24 -0
- package/dist/transform-Brkf-wzo.d.cts +4 -0
- package/dist/transform-Brkf-wzo.d.ts +4 -0
- package/dist/vue.cjs +3361 -0
- package/dist/vue.d.ts +68 -0
- package/dist/vue.js +3358 -0
- package/package.json +288 -0
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/core/lifecycle/sheetStack.ts
|
|
6
|
+
var BASE_Z = 100;
|
|
7
|
+
var STEP = 10;
|
|
8
|
+
var SheetStack = class {
|
|
9
|
+
constructor() {
|
|
10
|
+
__publicField(this, "entries", []);
|
|
11
|
+
}
|
|
12
|
+
push(entry) {
|
|
13
|
+
this.entries.push(entry);
|
|
14
|
+
this.recompute();
|
|
15
|
+
return () => this.remove(entry.id);
|
|
16
|
+
}
|
|
17
|
+
remove(id) {
|
|
18
|
+
const idx = this.entries.findIndex((e) => e.id === id);
|
|
19
|
+
if (idx === -1) return;
|
|
20
|
+
this.entries.splice(idx, 1);
|
|
21
|
+
this.recompute();
|
|
22
|
+
}
|
|
23
|
+
size() {
|
|
24
|
+
return this.entries.length;
|
|
25
|
+
}
|
|
26
|
+
clear() {
|
|
27
|
+
this.entries = [];
|
|
28
|
+
}
|
|
29
|
+
recompute() {
|
|
30
|
+
this.entries.forEach((entry, i) => {
|
|
31
|
+
entry.setZIndex(BASE_Z + i * STEP);
|
|
32
|
+
entry.setIsTop(i === this.entries.length - 1);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var sheetStack = new SheetStack();
|
|
37
|
+
|
|
38
|
+
// src/core/primitives/transform.ts
|
|
39
|
+
var CLOSED_TRANSFORM = {
|
|
40
|
+
bottom: "translate3d(0, 100%, 0)",
|
|
41
|
+
top: "translate3d(0, -100%, 0)",
|
|
42
|
+
left: "translate3d(-100%, 0, 0)",
|
|
43
|
+
right: "translate3d(100%, 0, 0)"
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/core/primitives/cssLength.ts
|
|
47
|
+
var probe = null;
|
|
48
|
+
var PROBE_ATTR = "data-bs-probe";
|
|
49
|
+
var ensureProbe = (axis) => {
|
|
50
|
+
if (typeof document === "undefined") return null;
|
|
51
|
+
if (!probe || !probe.isConnected) {
|
|
52
|
+
const existing = document.querySelector(`[${PROBE_ATTR}]`);
|
|
53
|
+
if (existing) {
|
|
54
|
+
probe = existing;
|
|
55
|
+
} else {
|
|
56
|
+
probe = document.createElement("div");
|
|
57
|
+
probe.setAttribute("aria-hidden", "true");
|
|
58
|
+
probe.setAttribute(PROBE_ATTR, "");
|
|
59
|
+
probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none;contain:strict;left:0;top:0;width:0;height:0;";
|
|
60
|
+
document.body.appendChild(probe);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
{
|
|
64
|
+
probe.style.width = "0";
|
|
65
|
+
probe.style.height = "";
|
|
66
|
+
}
|
|
67
|
+
return probe;
|
|
68
|
+
};
|
|
69
|
+
var viewportSize = (mode) => {
|
|
70
|
+
if (typeof window === "undefined") return 0;
|
|
71
|
+
return window.innerHeight ;
|
|
72
|
+
};
|
|
73
|
+
var PERCENT = /^(-?\d+(?:\.\d+)?)%$/;
|
|
74
|
+
var resolveSnap = (point, mode, measureFit) => {
|
|
75
|
+
if (typeof point === "number") return Math.max(0, point);
|
|
76
|
+
if (point === "full") return viewportSize();
|
|
77
|
+
if (point === "fit") return Math.max(0, 0);
|
|
78
|
+
const pctMatch = point.match(PERCENT);
|
|
79
|
+
if (pctMatch) {
|
|
80
|
+
const pct = parseFloat(pctMatch[1]) / 100;
|
|
81
|
+
return Math.max(0, viewportSize() * pct);
|
|
82
|
+
}
|
|
83
|
+
const el = ensureProbe();
|
|
84
|
+
if (!el) return 0;
|
|
85
|
+
{
|
|
86
|
+
el.style.height = point;
|
|
87
|
+
return Math.max(0, el.getBoundingClientRect().height);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/core/primitives/instance-id.ts
|
|
92
|
+
var counters = {};
|
|
93
|
+
function nextInstanceId(prefix) {
|
|
94
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
95
|
+
return `${prefix}-${crypto.randomUUID()}`;
|
|
96
|
+
}
|
|
97
|
+
counters[prefix] = (counters[prefix] ?? 0) + 1;
|
|
98
|
+
return `${prefix}-${counters[prefix]}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/core/primitives/event-bus.ts
|
|
102
|
+
function createEventBus() {
|
|
103
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
104
|
+
return {
|
|
105
|
+
on(event, fn) {
|
|
106
|
+
let set = listeners.get(event);
|
|
107
|
+
if (!set) {
|
|
108
|
+
set = /* @__PURE__ */ new Set();
|
|
109
|
+
listeners.set(event, set);
|
|
110
|
+
}
|
|
111
|
+
set.add(fn);
|
|
112
|
+
return () => {
|
|
113
|
+
set.delete(fn);
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
emit(event, payload) {
|
|
117
|
+
const set = listeners.get(event);
|
|
118
|
+
if (!set) return;
|
|
119
|
+
for (const fn of [...set]) {
|
|
120
|
+
fn(payload);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
listenerCount(event) {
|
|
124
|
+
return listeners.get(event)?.size ?? 0;
|
|
125
|
+
},
|
|
126
|
+
clear() {
|
|
127
|
+
listeners.clear();
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/core/lifecycle/focusTrap.ts
|
|
133
|
+
var FOCUSABLE = [
|
|
134
|
+
"a[href]",
|
|
135
|
+
"area[href]",
|
|
136
|
+
"button:not([disabled])",
|
|
137
|
+
"input:not([disabled]):not([type='hidden'])",
|
|
138
|
+
"select:not([disabled])",
|
|
139
|
+
"textarea:not([disabled])",
|
|
140
|
+
"iframe",
|
|
141
|
+
"object",
|
|
142
|
+
"embed",
|
|
143
|
+
"[contenteditable='true']",
|
|
144
|
+
"[tabindex]:not([tabindex='-1'])"
|
|
145
|
+
].join(",");
|
|
146
|
+
var isVisible = (el) => {
|
|
147
|
+
if (typeof el.checkVisibility === "function") {
|
|
148
|
+
return el.checkVisibility({ checkOpacity: false, checkVisibilityCSS: true });
|
|
149
|
+
}
|
|
150
|
+
return el.offsetParent !== null;
|
|
151
|
+
};
|
|
152
|
+
var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
|
|
153
|
+
(el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
|
|
154
|
+
);
|
|
155
|
+
var installFocusTrap = (container, options = {}) => {
|
|
156
|
+
if (typeof document === "undefined") return () => {
|
|
157
|
+
};
|
|
158
|
+
const previouslyFocused = document.activeElement;
|
|
159
|
+
const focusInitial = () => {
|
|
160
|
+
const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
|
|
161
|
+
initial?.focus({ preventScroll: true });
|
|
162
|
+
};
|
|
163
|
+
focusInitial();
|
|
164
|
+
const handleKey = (e) => {
|
|
165
|
+
if (e.key === "Escape" && options.onEscape) {
|
|
166
|
+
options.onEscape();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (e.key !== "Tab") return;
|
|
170
|
+
const list = focusables(container);
|
|
171
|
+
if (list.length === 0) {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const first = list[0];
|
|
176
|
+
const last = list[list.length - 1];
|
|
177
|
+
const active = document.activeElement;
|
|
178
|
+
if (e.shiftKey && active === first) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
last.focus();
|
|
181
|
+
} else if (!e.shiftKey && active === last) {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
first.focus();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const handleFocusIn = (e) => {
|
|
187
|
+
const target = e.target;
|
|
188
|
+
if (target && !container.contains(target)) {
|
|
189
|
+
const list = focusables(container);
|
|
190
|
+
list[0]?.focus({ preventScroll: true });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
document.addEventListener("keydown", handleKey, true);
|
|
194
|
+
document.addEventListener("focusin", handleFocusIn, true);
|
|
195
|
+
let released = false;
|
|
196
|
+
return () => {
|
|
197
|
+
if (released) return;
|
|
198
|
+
released = true;
|
|
199
|
+
document.removeEventListener("keydown", handleKey, true);
|
|
200
|
+
document.removeEventListener("focusin", handleFocusIn, true);
|
|
201
|
+
if (previouslyFocused && document.contains(previouslyFocused)) {
|
|
202
|
+
previouslyFocused.focus({ preventScroll: true });
|
|
203
|
+
} else {
|
|
204
|
+
const fallback = document.querySelector("[data-bs-restore-fallback]");
|
|
205
|
+
fallback?.focus({ preventScroll: true });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/core/lifecycle/scrollLock.ts
|
|
211
|
+
var lockCount = 0;
|
|
212
|
+
var savedStyles = null;
|
|
213
|
+
var lockBodyScroll = () => {
|
|
214
|
+
if (typeof document === "undefined") return () => {
|
|
215
|
+
};
|
|
216
|
+
if (lockCount === 0) {
|
|
217
|
+
const body = document.body;
|
|
218
|
+
savedStyles = {
|
|
219
|
+
overflow: body.style.overflow,
|
|
220
|
+
position: body.style.position,
|
|
221
|
+
top: body.style.top,
|
|
222
|
+
width: body.style.width,
|
|
223
|
+
scrollY: window.scrollY
|
|
224
|
+
};
|
|
225
|
+
body.style.overflow = "hidden";
|
|
226
|
+
body.style.position = "fixed";
|
|
227
|
+
body.style.top = `-${savedStyles.scrollY}px`;
|
|
228
|
+
body.style.width = "100%";
|
|
229
|
+
}
|
|
230
|
+
lockCount++;
|
|
231
|
+
let released = false;
|
|
232
|
+
return () => {
|
|
233
|
+
if (released) return;
|
|
234
|
+
released = true;
|
|
235
|
+
lockCount = Math.max(0, lockCount - 1);
|
|
236
|
+
if (lockCount === 0 && savedStyles) {
|
|
237
|
+
const body = document.body;
|
|
238
|
+
body.style.overflow = savedStyles.overflow;
|
|
239
|
+
body.style.position = savedStyles.position;
|
|
240
|
+
body.style.top = savedStyles.top;
|
|
241
|
+
body.style.width = savedStyles.width;
|
|
242
|
+
window.scrollTo(0, savedStyles.scrollY);
|
|
243
|
+
savedStyles = null;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/core/features/inert-siblings.ts
|
|
249
|
+
function createInertSiblings(rootProvider) {
|
|
250
|
+
const tracked = [];
|
|
251
|
+
return {
|
|
252
|
+
apply() {
|
|
253
|
+
if (typeof document === "undefined") return;
|
|
254
|
+
const root = rootProvider();
|
|
255
|
+
const parent = root.parentElement ?? document.body;
|
|
256
|
+
for (const child of Array.from(parent.children)) {
|
|
257
|
+
if (child === root) continue;
|
|
258
|
+
if (child.hasAttribute("inert")) continue;
|
|
259
|
+
child.setAttribute("inert", "");
|
|
260
|
+
tracked.push(child);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
remove() {
|
|
264
|
+
for (const el of tracked) el.removeAttribute("inert");
|
|
265
|
+
tracked.length = 0;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/core/controllers/lifecycle-controller.ts
|
|
271
|
+
var LifecycleController = class {
|
|
272
|
+
constructor(deps, opts) {
|
|
273
|
+
__publicField(this, "element");
|
|
274
|
+
__publicField(this, "closeFn");
|
|
275
|
+
__publicField(this, "focusTrapEnabled");
|
|
276
|
+
__publicField(this, "initialFocus");
|
|
277
|
+
__publicField(this, "closeOnEscape");
|
|
278
|
+
__publicField(this, "bodyScrollLockEnabled");
|
|
279
|
+
__publicField(this, "inertSiblingsEnabled");
|
|
280
|
+
__publicField(this, "inertSiblings");
|
|
281
|
+
__publicField(this, "shouldApplyInertSiblings");
|
|
282
|
+
__publicField(this, "returnFocus");
|
|
283
|
+
/** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
|
|
284
|
+
__publicField(this, "releaseFocusTrap", null);
|
|
285
|
+
__publicField(this, "releaseScrollLock", null);
|
|
286
|
+
/** True between install() and release(). Guards release()/destroy() from
|
|
287
|
+
* re-running side effects (notably `returnFocus.focus()`) on already-
|
|
288
|
+
* released or never-installed instances. */
|
|
289
|
+
__publicField(this, "installed", false);
|
|
290
|
+
__publicField(this, "destroyed", false);
|
|
291
|
+
this.element = deps.element;
|
|
292
|
+
this.closeFn = deps.close;
|
|
293
|
+
this.focusTrapEnabled = opts.focusTrap ?? false;
|
|
294
|
+
this.initialFocus = opts.initialFocus;
|
|
295
|
+
this.closeOnEscape = opts.closeOnEscape ?? true;
|
|
296
|
+
this.bodyScrollLockEnabled = opts.lockBodyScroll ?? true;
|
|
297
|
+
this.inertSiblingsEnabled = opts.inertSiblings ?? false;
|
|
298
|
+
this.shouldApplyInertSiblings = opts.shouldApplyInertSiblings ?? (() => true);
|
|
299
|
+
this.returnFocus = opts.returnFocus;
|
|
300
|
+
this.inertSiblings = createInertSiblings(() => this.element);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Run the open-time install steps. Idempotent: each step checks its
|
|
304
|
+
* existing teardown ref so a re-open during a half-cancelled close
|
|
305
|
+
* doesn't double-install. Sets `installed=true` BEFORE the first side
|
|
306
|
+
* effect so a partial throw (detached / shadow-DOM target) leaves the
|
|
307
|
+
* caller's `release()` rollback path armed to undo whatever did succeed.
|
|
308
|
+
* No-op when destroyed.
|
|
309
|
+
*/
|
|
310
|
+
install() {
|
|
311
|
+
if (this.destroyed) return;
|
|
312
|
+
this.installed = true;
|
|
313
|
+
if (this.bodyScrollLockEnabled && !this.releaseScrollLock) {
|
|
314
|
+
this.releaseScrollLock = lockBodyScroll();
|
|
315
|
+
}
|
|
316
|
+
if (this.focusTrapEnabled && !this.releaseFocusTrap) {
|
|
317
|
+
this.releaseFocusTrap = installFocusTrap(this.element, {
|
|
318
|
+
initialFocus: this.initialFocus,
|
|
319
|
+
onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
|
|
323
|
+
this.inertSiblings.apply();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Update the post-release focus target. Overlay re-binds this when the
|
|
328
|
+
* consumer changes `returnFocus` after construction; the new value applies
|
|
329
|
+
* on the next `release()`. No-op when destroyed.
|
|
330
|
+
*/
|
|
331
|
+
setReturnFocus(target) {
|
|
332
|
+
if (this.destroyed) return;
|
|
333
|
+
this.returnFocus = target;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Release all install-time teardowns. Short-circuits when not installed,
|
|
337
|
+
* so `close() → destroy()` (each routing through here) only restores
|
|
338
|
+
* focus once. The `installed` guard lets `inertSiblings.remove()` and
|
|
339
|
+
* the focus-trap/scroll-lock teardown nullings stay simple — they were
|
|
340
|
+
* already idempotent, but `returnFocus.focus()` was firing on every call.
|
|
341
|
+
*/
|
|
342
|
+
release() {
|
|
343
|
+
if (!this.installed) return;
|
|
344
|
+
this.installed = false;
|
|
345
|
+
this.releaseFocusTrap?.();
|
|
346
|
+
this.releaseFocusTrap = null;
|
|
347
|
+
this.releaseScrollLock?.();
|
|
348
|
+
this.releaseScrollLock = null;
|
|
349
|
+
this.inertSiblings.remove();
|
|
350
|
+
if (this.returnFocus) {
|
|
351
|
+
const target = typeof this.returnFocus === "function" ? this.returnFocus() : this.returnFocus;
|
|
352
|
+
target?.focus?.();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Final teardown — releases any still-installed listeners and seals the
|
|
357
|
+
* controller. After destroy(), install() is a no-op and setReturnFocus()
|
|
358
|
+
* is rejected, so post-destroy mutations can't queue up a stale focus
|
|
359
|
+
* restore.
|
|
360
|
+
*/
|
|
361
|
+
destroy() {
|
|
362
|
+
this.release();
|
|
363
|
+
this.destroyed = true;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/core/overlay.ts
|
|
368
|
+
var OVERLAY_PRESETS = Object.freeze({
|
|
369
|
+
sheet: Object.freeze({
|
|
370
|
+
enterAnimation: "slide",
|
|
371
|
+
exitAnimation: "slide",
|
|
372
|
+
backdropOpacity: 0.5,
|
|
373
|
+
backdropFilter: void 0,
|
|
374
|
+
swipeToClose: true
|
|
375
|
+
}),
|
|
376
|
+
dialog: Object.freeze({
|
|
377
|
+
enterAnimation: "scale",
|
|
378
|
+
exitAnimation: "scale",
|
|
379
|
+
backdropOpacity: 0.6,
|
|
380
|
+
backdropFilter: "blur(8px)",
|
|
381
|
+
swipeToClose: false
|
|
382
|
+
}),
|
|
383
|
+
sidebar: Object.freeze({
|
|
384
|
+
enterAnimation: "slide",
|
|
385
|
+
exitAnimation: "slide",
|
|
386
|
+
backdropOpacity: 0.4,
|
|
387
|
+
backdropFilter: void 0,
|
|
388
|
+
swipeToClose: true
|
|
389
|
+
}),
|
|
390
|
+
toast: Object.freeze({
|
|
391
|
+
enterAnimation: "fade",
|
|
392
|
+
exitAnimation: "fade",
|
|
393
|
+
backdropOpacity: 0,
|
|
394
|
+
backdropFilter: void 0,
|
|
395
|
+
swipeToClose: false
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
var closedTransform = (edge) => CLOSED_TRANSFORM[edge];
|
|
399
|
+
var closedTransformFor = (edge, animation, peekPx) => {
|
|
400
|
+
switch (animation) {
|
|
401
|
+
case "fade":
|
|
402
|
+
return "translate3d(0, 0, 0)";
|
|
403
|
+
case "scale":
|
|
404
|
+
return "translate3d(0, 0, 0) scale(0.94)";
|
|
405
|
+
case "slide":
|
|
406
|
+
default:
|
|
407
|
+
if (peekPx !== void 0 && edge === "bottom") {
|
|
408
|
+
return `translate3d(0, calc(100% - ${peekPx}px), 0)`;
|
|
409
|
+
}
|
|
410
|
+
return closedTransform(edge);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
var openTransformFor = (animation) => animation === "scale" ? "translate3d(0, 0, 0) scale(1)" : "translate3d(0, 0, 0)";
|
|
414
|
+
var OverlayEngine = class {
|
|
415
|
+
constructor(opts) {
|
|
416
|
+
__publicField(this, "id", nextInstanceId("ovl"));
|
|
417
|
+
__publicField(this, "element");
|
|
418
|
+
__publicField(this, "backdrop");
|
|
419
|
+
__publicField(this, "edge");
|
|
420
|
+
__publicField(this, "closeOnEscape");
|
|
421
|
+
__publicField(this, "closeOnBackdropEnabled");
|
|
422
|
+
__publicField(this, "focusTrapEnabled");
|
|
423
|
+
__publicField(this, "initialFocus");
|
|
424
|
+
__publicField(this, "bodyScrollLockEnabled");
|
|
425
|
+
__publicField(this, "inertSiblingsEnabled");
|
|
426
|
+
__publicField(this, "closeOnBackEnabled");
|
|
427
|
+
__publicField(this, "duration");
|
|
428
|
+
__publicField(this, "enterEasing");
|
|
429
|
+
__publicField(this, "exitEasing");
|
|
430
|
+
__publicField(this, "backdropOpacity");
|
|
431
|
+
__publicField(this, "backdropFilter");
|
|
432
|
+
// Definite-assignment: written via assignSwipeFromConfig in the
|
|
433
|
+
// constructor — TS can't follow the helper, mark the assertion explicit.
|
|
434
|
+
__publicField(this, "swipeToCloseEnabled");
|
|
435
|
+
__publicField(this, "swipeThreshold");
|
|
436
|
+
__publicField(this, "swipeVelocityThreshold");
|
|
437
|
+
__publicField(this, "enterAnimation");
|
|
438
|
+
__publicField(this, "exitAnimation");
|
|
439
|
+
__publicField(this, "mountTarget");
|
|
440
|
+
__publicField(this, "originalParent", null);
|
|
441
|
+
__publicField(this, "originalNextSibling", null);
|
|
442
|
+
__publicField(this, "mounted", false);
|
|
443
|
+
__publicField(this, "peekRaw");
|
|
444
|
+
__publicField(this, "peekPx");
|
|
445
|
+
__publicField(this, "closeOnOutsidePointerEnabled");
|
|
446
|
+
__publicField(this, "detachOutsidePointer", null);
|
|
447
|
+
__publicField(this, "detachSwipe", null);
|
|
448
|
+
__publicField(this, "onOpenCb");
|
|
449
|
+
__publicField(this, "onCloseCb");
|
|
450
|
+
__publicField(this, "isOpen_", false);
|
|
451
|
+
__publicField(this, "isAnimating_", false);
|
|
452
|
+
__publicField(this, "destroyed", false);
|
|
453
|
+
// Tracks whether setOverlayChildren ever populated the panel — lets
|
|
454
|
+
// clearOverlayChildren skip the DOM mutation when nothing was injected
|
|
455
|
+
// (so the field also acts as a zero-allocation feature flag).
|
|
456
|
+
__publicField(this, "hasInjectedChildren", false);
|
|
457
|
+
// Bumped on every open/close. awaitTransition captures it and ignores
|
|
458
|
+
// transitionend/timeout fired by older cycles — prevents cross-cycle
|
|
459
|
+
// listener bleed during rapid toggling.
|
|
460
|
+
__publicField(this, "cycleNonce", 0);
|
|
461
|
+
__publicField(this, "bus", createEventBus());
|
|
462
|
+
__publicField(this, "detachEscape", null);
|
|
463
|
+
__publicField(this, "detachBackdrop", null);
|
|
464
|
+
__publicField(this, "detachHardwareBack", null);
|
|
465
|
+
__publicField(this, "releaseStackEntry", null);
|
|
466
|
+
__publicField(this, "lifecycle");
|
|
467
|
+
this.element = opts.element;
|
|
468
|
+
this.backdrop = opts.backdrop;
|
|
469
|
+
this.edge = opts.edge ?? "bottom";
|
|
470
|
+
this.closeOnEscape = opts.closeOnEscape ?? true;
|
|
471
|
+
this.closeOnBackdropEnabled = opts.closeOnBackdrop ?? true;
|
|
472
|
+
this.focusTrapEnabled = opts.focusTrap ?? true;
|
|
473
|
+
this.initialFocus = opts.initialFocus;
|
|
474
|
+
this.bodyScrollLockEnabled = opts.lockBodyScroll ?? true;
|
|
475
|
+
this.inertSiblingsEnabled = opts.inertSiblings ?? false;
|
|
476
|
+
this.closeOnBackEnabled = opts.closeOnBack ?? false;
|
|
477
|
+
this.duration = opts.duration ?? 320;
|
|
478
|
+
this.enterEasing = opts.enterEasing ?? "cubic-bezier(0.29, 1.24, 0.84, 0.99)";
|
|
479
|
+
this.exitEasing = opts.exitEasing ?? "cubic-bezier(0.4, 0, 1, 1)";
|
|
480
|
+
const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
|
|
481
|
+
this.backdropOpacity = opts.backdropOpacity ?? preset?.backdropOpacity ?? 1;
|
|
482
|
+
this.backdropFilter = opts.backdropFilter ?? preset?.backdropFilter ?? void 0;
|
|
483
|
+
const swipeRaw = opts.swipeToClose !== void 0 ? opts.swipeToClose : preset?.swipeToClose;
|
|
484
|
+
this.assignSwipeFromConfig(swipeRaw);
|
|
485
|
+
this.enterAnimation = opts.enterAnimation ?? preset?.enterAnimation ?? "slide";
|
|
486
|
+
this.exitAnimation = opts.exitAnimation ?? preset?.exitAnimation ?? this.enterAnimation;
|
|
487
|
+
this.mountTarget = opts.mountTo;
|
|
488
|
+
this.peekRaw = opts.peek;
|
|
489
|
+
this.closeOnOutsidePointerEnabled = opts.closeOnOutsidePointer ?? false;
|
|
490
|
+
this.onOpenCb = opts.onOpen;
|
|
491
|
+
this.onCloseCb = opts.onClose;
|
|
492
|
+
if (this.peekRaw !== void 0 && this.edge !== "bottom") {
|
|
493
|
+
console.warn(
|
|
494
|
+
"[OverlayEngine] `peek` is only supported on edge: 'bottom' \u2014 ignoring."
|
|
495
|
+
);
|
|
496
|
+
this.peekRaw = void 0;
|
|
497
|
+
}
|
|
498
|
+
if (this.peekRaw !== void 0) {
|
|
499
|
+
this.peekPx = this.resolvePeekPx(this.peekRaw);
|
|
500
|
+
}
|
|
501
|
+
this.lifecycle = new LifecycleController(
|
|
502
|
+
{
|
|
503
|
+
element: this.element,
|
|
504
|
+
close: () => this.close("escape")
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
focusTrap: this.focusTrapEnabled,
|
|
508
|
+
initialFocus: this.initialFocus,
|
|
509
|
+
closeOnEscape: this.closeOnEscape,
|
|
510
|
+
lockBodyScroll: this.bodyScrollLockEnabled,
|
|
511
|
+
inertSiblings: this.inertSiblingsEnabled,
|
|
512
|
+
// Overlay-specific guard: only paint body siblings inert when the
|
|
513
|
+
// overlay element is actually a body descendant. Detached / shadow-
|
|
514
|
+
// DOM / portal overlays would otherwise lock unrelated trees.
|
|
515
|
+
shouldApplyInertSiblings: () => {
|
|
516
|
+
const bodyChildren = Array.from(document.body.children);
|
|
517
|
+
return bodyChildren.some(
|
|
518
|
+
(c) => c === this.element || c.contains(this.element)
|
|
519
|
+
);
|
|
520
|
+
},
|
|
521
|
+
returnFocus: opts.returnFocus
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
this.applyClosedStyles();
|
|
525
|
+
this.registerInStack();
|
|
526
|
+
if (opts.initialOpen) void this.open();
|
|
527
|
+
}
|
|
528
|
+
get state() {
|
|
529
|
+
return { isOpen: this.isOpen_, isAnimating: this.isAnimating_ };
|
|
530
|
+
}
|
|
531
|
+
on(event, fn) {
|
|
532
|
+
return this.bus.on(event, fn);
|
|
533
|
+
}
|
|
534
|
+
open() {
|
|
535
|
+
if (this.destroyed || this.isOpen_) return Promise.resolve();
|
|
536
|
+
this.maybeMount();
|
|
537
|
+
this.emit("before-open", void 0);
|
|
538
|
+
this.isOpen_ = true;
|
|
539
|
+
this.isAnimating_ = true;
|
|
540
|
+
this.element.removeAttribute("hidden");
|
|
541
|
+
this.element.setAttribute("data-state", "entering");
|
|
542
|
+
if (this.focusTrapEnabled) {
|
|
543
|
+
this.element.setAttribute("aria-modal", "true");
|
|
544
|
+
}
|
|
545
|
+
const transitionStr = `transform ${this.duration}ms ${this.enterEasing}, opacity ${this.duration}ms ease-out`;
|
|
546
|
+
this.element.style.transition = transitionStr;
|
|
547
|
+
if (this.backdrop) {
|
|
548
|
+
this.backdrop.style.transition = `opacity ${this.duration}ms ease-out`;
|
|
549
|
+
}
|
|
550
|
+
const cycle = ++this.cycleNonce;
|
|
551
|
+
return new Promise((resolve) => {
|
|
552
|
+
requestAnimationFrame(() => {
|
|
553
|
+
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
554
|
+
requestAnimationFrame(() => {
|
|
555
|
+
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
556
|
+
this.element.style.transform = openTransformFor(this.enterAnimation);
|
|
557
|
+
this.element.style.opacity = "1";
|
|
558
|
+
if (this.backdrop) {
|
|
559
|
+
this.backdrop.style.opacity = String(this.backdropOpacity);
|
|
560
|
+
this.backdrop.style.pointerEvents = "auto";
|
|
561
|
+
if (this.backdropFilter) {
|
|
562
|
+
this.backdrop.style.backdropFilter = this.backdropFilter;
|
|
563
|
+
this.backdrop.style.webkitBackdropFilter = this.backdropFilter;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
this.installInteractiveListeners();
|
|
568
|
+
} catch (err) {
|
|
569
|
+
this.releaseInteractiveListeners();
|
|
570
|
+
this.isOpen_ = false;
|
|
571
|
+
this.isAnimating_ = false;
|
|
572
|
+
this.element.style.transform = closedTransformFor(
|
|
573
|
+
this.edge,
|
|
574
|
+
this.enterAnimation,
|
|
575
|
+
this.peekPx
|
|
576
|
+
);
|
|
577
|
+
this.element.style.opacity = "0";
|
|
578
|
+
if (this.backdrop) {
|
|
579
|
+
this.backdrop.style.opacity = "0";
|
|
580
|
+
this.backdrop.style.pointerEvents = "none";
|
|
581
|
+
if (this.backdropFilter) {
|
|
582
|
+
this.backdrop.style.backdropFilter = "";
|
|
583
|
+
this.backdrop.style.webkitBackdropFilter = "";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
this.element.setAttribute("data-state", "closed");
|
|
587
|
+
this.element.setAttribute("hidden", "");
|
|
588
|
+
if (this.focusTrapEnabled) {
|
|
589
|
+
this.element.removeAttribute("aria-modal");
|
|
590
|
+
}
|
|
591
|
+
queueMicrotask(() => {
|
|
592
|
+
throw err;
|
|
593
|
+
});
|
|
594
|
+
return resolve();
|
|
595
|
+
}
|
|
596
|
+
this.awaitTransition(cycle).then(() => {
|
|
597
|
+
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
598
|
+
this.isAnimating_ = false;
|
|
599
|
+
this.element.setAttribute("data-state", "open");
|
|
600
|
+
this.emit("open", void 0);
|
|
601
|
+
this.onOpenCb?.();
|
|
602
|
+
resolve();
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
close(reason = "programmatic") {
|
|
609
|
+
if (this.destroyed || !this.isOpen_) return Promise.resolve();
|
|
610
|
+
this.emit("before-close", void 0);
|
|
611
|
+
this.isOpen_ = false;
|
|
612
|
+
this.isAnimating_ = true;
|
|
613
|
+
this.element.setAttribute("data-state", "exiting");
|
|
614
|
+
const transitionStr = `transform ${this.duration}ms ${this.exitEasing}, opacity ${this.duration}ms ease-in`;
|
|
615
|
+
this.element.style.transition = transitionStr;
|
|
616
|
+
if (this.backdrop) {
|
|
617
|
+
this.backdrop.style.transition = `opacity ${this.duration}ms ease-in`;
|
|
618
|
+
}
|
|
619
|
+
const cycle = ++this.cycleNonce;
|
|
620
|
+
return new Promise((resolve) => {
|
|
621
|
+
requestAnimationFrame(() => {
|
|
622
|
+
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
623
|
+
this.element.style.transform = closedTransformFor(
|
|
624
|
+
this.edge,
|
|
625
|
+
this.exitAnimation,
|
|
626
|
+
this.peekPx
|
|
627
|
+
);
|
|
628
|
+
this.element.style.opacity = "0";
|
|
629
|
+
if (this.backdrop) {
|
|
630
|
+
this.backdrop.style.opacity = "0";
|
|
631
|
+
this.backdrop.style.pointerEvents = "none";
|
|
632
|
+
if (this.backdropFilter) {
|
|
633
|
+
this.backdrop.style.backdropFilter = "";
|
|
634
|
+
this.backdrop.style.webkitBackdropFilter = "";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
this.releaseInteractiveListeners();
|
|
638
|
+
this.awaitTransition(cycle).then(() => {
|
|
639
|
+
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
640
|
+
this.isAnimating_ = false;
|
|
641
|
+
if (this.peekPx === void 0) {
|
|
642
|
+
this.element.setAttribute("hidden", "");
|
|
643
|
+
}
|
|
644
|
+
this.element.setAttribute("data-state", "closed");
|
|
645
|
+
if (this.focusTrapEnabled) {
|
|
646
|
+
this.element.removeAttribute("aria-modal");
|
|
647
|
+
}
|
|
648
|
+
this.emit("close", { reason });
|
|
649
|
+
this.onCloseCb?.();
|
|
650
|
+
resolve();
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
toggle() {
|
|
656
|
+
if (this.destroyed) return Promise.resolve();
|
|
657
|
+
return this.isOpen_ ? this.close() : this.open();
|
|
658
|
+
}
|
|
659
|
+
destroy() {
|
|
660
|
+
this.destroyed = true;
|
|
661
|
+
this.cycleNonce++;
|
|
662
|
+
this.bus.clear();
|
|
663
|
+
this.releaseInteractiveListeners();
|
|
664
|
+
this.releaseStackEntry?.();
|
|
665
|
+
this.releaseStackEntry = null;
|
|
666
|
+
this.restoreMount();
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Runtime visual setter — opacity is applied immediately when open so a
|
|
670
|
+
* live-controls panel can preview the change without re-opening. When
|
|
671
|
+
* closed we only stash the field; next open() picks it up so we don't
|
|
672
|
+
* mutate hidden DOM (avoids paint thrash on detached panels).
|
|
673
|
+
*/
|
|
674
|
+
setBackdropOpacity(opacity) {
|
|
675
|
+
if (this.destroyed) return;
|
|
676
|
+
this.backdropOpacity = opacity;
|
|
677
|
+
if (this.isOpen_ && this.backdrop) {
|
|
678
|
+
this.backdrop.style.opacity = String(opacity);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Runtime visual setter for `backdrop-filter`. `undefined` clears the
|
|
683
|
+
* filter (mirrors the close() behaviour). Defers to next open() when the
|
|
684
|
+
* overlay is currently closed.
|
|
685
|
+
*/
|
|
686
|
+
setBackdropFilter(filter) {
|
|
687
|
+
if (this.destroyed) return;
|
|
688
|
+
this.backdropFilter = filter;
|
|
689
|
+
if (!this.isOpen_ || !this.backdrop) return;
|
|
690
|
+
const style = this.backdrop.style;
|
|
691
|
+
if (filter) {
|
|
692
|
+
this.backdrop.style.backdropFilter = filter;
|
|
693
|
+
style.webkitBackdropFilter = filter;
|
|
694
|
+
} else {
|
|
695
|
+
this.backdrop.style.backdropFilter = "";
|
|
696
|
+
style.webkitBackdropFilter = "";
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
|
|
701
|
+
* threshold fields, then installs or tears down the pointer listener so
|
|
702
|
+
* the change applies to the CURRENT open cycle. Idempotent: safe to call
|
|
703
|
+
* with the same config repeatedly.
|
|
704
|
+
*/
|
|
705
|
+
setSwipeToClose(config) {
|
|
706
|
+
if (this.destroyed) return;
|
|
707
|
+
const wasEnabled = this.swipeToCloseEnabled;
|
|
708
|
+
this.assignSwipeFromConfig(config);
|
|
709
|
+
if (!this.isOpen_) return;
|
|
710
|
+
if (this.swipeToCloseEnabled && !this.detachSwipe) {
|
|
711
|
+
this.detachSwipe = this.installSwipeToClose();
|
|
712
|
+
} else if (!this.swipeToCloseEnabled && wasEnabled && this.detachSwipe) {
|
|
713
|
+
this.detachSwipe();
|
|
714
|
+
this.detachSwipe = null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/** Animation setters only stage the field — applied on next open/close. */
|
|
718
|
+
setEnterAnimation(animation) {
|
|
719
|
+
if (this.destroyed) return;
|
|
720
|
+
this.enterAnimation = animation;
|
|
721
|
+
}
|
|
722
|
+
setExitAnimation(animation) {
|
|
723
|
+
if (this.destroyed) return;
|
|
724
|
+
this.exitAnimation = animation;
|
|
725
|
+
}
|
|
726
|
+
/** Updates the field used at next close() — no immediate side effect. */
|
|
727
|
+
setReturnFocus(target) {
|
|
728
|
+
if (this.destroyed) return;
|
|
729
|
+
this.lifecycle.setReturnFocus(target);
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Batch update — preset first, then individual overrides. Applied as a
|
|
733
|
+
* single DOM-write phase when open so live-controls don't trigger
|
|
734
|
+
* cascading reflows. Safe to call when closed (defers to next open).
|
|
735
|
+
*/
|
|
736
|
+
setOverlay(opts) {
|
|
737
|
+
if (this.destroyed) return;
|
|
738
|
+
const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
|
|
739
|
+
const merged = preset ? {
|
|
740
|
+
backdropOpacity: preset.backdropOpacity,
|
|
741
|
+
// Preset's `undefined` filter means "no filter" — surface as null
|
|
742
|
+
// so the merged step clears the field instead of inheriting old.
|
|
743
|
+
backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
|
|
744
|
+
swipeToClose: preset.swipeToClose,
|
|
745
|
+
enterAnimation: preset.enterAnimation,
|
|
746
|
+
exitAnimation: preset.exitAnimation
|
|
747
|
+
} : {};
|
|
748
|
+
if (opts.backdropOpacity !== void 0) {
|
|
749
|
+
merged.backdropOpacity = opts.backdropOpacity;
|
|
750
|
+
}
|
|
751
|
+
if (opts.backdropFilter !== void 0) {
|
|
752
|
+
merged.backdropFilter = opts.backdropFilter;
|
|
753
|
+
}
|
|
754
|
+
if (opts.swipeToClose !== void 0) {
|
|
755
|
+
merged.swipeToClose = opts.swipeToClose;
|
|
756
|
+
}
|
|
757
|
+
if (opts.enterAnimation !== void 0) {
|
|
758
|
+
merged.enterAnimation = opts.enterAnimation;
|
|
759
|
+
}
|
|
760
|
+
if (opts.exitAnimation !== void 0) {
|
|
761
|
+
merged.exitAnimation = opts.exitAnimation;
|
|
762
|
+
}
|
|
763
|
+
if (merged.enterAnimation !== void 0) {
|
|
764
|
+
this.enterAnimation = merged.enterAnimation;
|
|
765
|
+
}
|
|
766
|
+
if (merged.exitAnimation !== void 0) {
|
|
767
|
+
this.exitAnimation = merged.exitAnimation;
|
|
768
|
+
}
|
|
769
|
+
if (merged.swipeToClose !== void 0) {
|
|
770
|
+
this.setSwipeToClose(merged.swipeToClose);
|
|
771
|
+
}
|
|
772
|
+
if (merged.backdropOpacity !== void 0) {
|
|
773
|
+
this.backdropOpacity = merged.backdropOpacity;
|
|
774
|
+
}
|
|
775
|
+
if (merged.backdropFilter !== void 0) {
|
|
776
|
+
this.backdropFilter = merged.backdropFilter === null ? void 0 : merged.backdropFilter;
|
|
777
|
+
}
|
|
778
|
+
if (this.isOpen_ && this.backdrop) {
|
|
779
|
+
if (merged.backdropOpacity !== void 0) {
|
|
780
|
+
this.backdrop.style.opacity = String(this.backdropOpacity);
|
|
781
|
+
}
|
|
782
|
+
if (merged.backdropFilter !== void 0) {
|
|
783
|
+
const style = this.backdrop.style;
|
|
784
|
+
if (this.backdropFilter) {
|
|
785
|
+
this.backdrop.style.backdropFilter = this.backdropFilter;
|
|
786
|
+
style.webkitBackdropFilter = this.backdropFilter;
|
|
787
|
+
} else {
|
|
788
|
+
this.backdrop.style.backdropFilter = "";
|
|
789
|
+
style.webkitBackdropFilter = "";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (opts.children !== void 0) {
|
|
794
|
+
if (opts.children === null) {
|
|
795
|
+
this.clearOverlayChildren();
|
|
796
|
+
} else {
|
|
797
|
+
this.setOverlayChildren(opts.children);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Runtime children injection — replace the panel's contents without
|
|
803
|
+
* unmounting the engine. `replaceChildren()` is a single atomic DOM op,
|
|
804
|
+
* so callers can swap content rapidly without intermediate flicker.
|
|
805
|
+
* Function form is resolved exactly ONCE per call (cheap lazy content).
|
|
806
|
+
*/
|
|
807
|
+
setOverlayChildren(children) {
|
|
808
|
+
if (this.destroyed) return;
|
|
809
|
+
const node = typeof children === "function" ? children() : children;
|
|
810
|
+
this.element.replaceChildren(node);
|
|
811
|
+
this.hasInjectedChildren = true;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Clear injected children. No-op when nothing was injected so consumers
|
|
815
|
+
* who never used setOverlayChildren can call this defensively without
|
|
816
|
+
* paying for a DOM mutation.
|
|
817
|
+
*/
|
|
818
|
+
clearOverlayChildren() {
|
|
819
|
+
if (this.destroyed || !this.hasInjectedChildren) return;
|
|
820
|
+
this.element.replaceChildren();
|
|
821
|
+
this.hasInjectedChildren = false;
|
|
822
|
+
}
|
|
823
|
+
// Shared swipe-config normalizer — used by constructor, setSwipeToClose,
|
|
824
|
+
// and setOverlay so the union/object shape is decoded in exactly one place.
|
|
825
|
+
assignSwipeFromConfig(swipe) {
|
|
826
|
+
if (typeof swipe === "object" && swipe !== null) {
|
|
827
|
+
this.swipeToCloseEnabled = swipe.enabled ?? true;
|
|
828
|
+
this.swipeThreshold = swipe.threshold ?? 0.3;
|
|
829
|
+
this.swipeVelocityThreshold = swipe.velocityThreshold ?? 0.5;
|
|
830
|
+
} else {
|
|
831
|
+
this.swipeToCloseEnabled = swipe ?? false;
|
|
832
|
+
this.swipeThreshold = 0.3;
|
|
833
|
+
this.swipeVelocityThreshold = 0.5;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
emit(event, payload) {
|
|
837
|
+
this.bus.emit(event, payload);
|
|
838
|
+
}
|
|
839
|
+
resolvePeekPx(raw) {
|
|
840
|
+
if (typeof raw === "number") return Math.max(0, raw);
|
|
841
|
+
try {
|
|
842
|
+
return Math.max(0, resolveSnap(raw, "bottom"));
|
|
843
|
+
} catch {
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
maybeMount() {
|
|
848
|
+
if (this.mounted || !this.mountTarget || this.mountTarget === "parent") {
|
|
849
|
+
this.mounted = true;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const target = this.mountTarget === "body" ? document.body : this.mountTarget;
|
|
853
|
+
if (!target) {
|
|
854
|
+
this.mounted = true;
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
this.originalParent = this.element.parentNode;
|
|
858
|
+
this.originalNextSibling = this.element.nextSibling;
|
|
859
|
+
target.appendChild(this.element);
|
|
860
|
+
this.mounted = true;
|
|
861
|
+
}
|
|
862
|
+
restoreMount() {
|
|
863
|
+
if (!this.originalParent) return;
|
|
864
|
+
try {
|
|
865
|
+
if (this.originalNextSibling && this.originalNextSibling.parentNode === this.originalParent) {
|
|
866
|
+
this.originalParent.insertBefore(this.element, this.originalNextSibling);
|
|
867
|
+
} else {
|
|
868
|
+
this.originalParent.appendChild(this.element);
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
this.originalParent = null;
|
|
873
|
+
this.originalNextSibling = null;
|
|
874
|
+
}
|
|
875
|
+
applyClosedStyles() {
|
|
876
|
+
this.element.style.willChange = "transform, opacity";
|
|
877
|
+
this.element.style.transform = closedTransformFor(
|
|
878
|
+
this.edge,
|
|
879
|
+
this.enterAnimation,
|
|
880
|
+
this.peekPx
|
|
881
|
+
);
|
|
882
|
+
this.element.style.opacity = this.peekPx !== void 0 ? "1" : "0";
|
|
883
|
+
if (this.peekPx === void 0) {
|
|
884
|
+
this.element.setAttribute("hidden", "");
|
|
885
|
+
}
|
|
886
|
+
this.element.setAttribute("data-state", "closed");
|
|
887
|
+
if (this.backdrop) {
|
|
888
|
+
this.backdrop.style.opacity = "0";
|
|
889
|
+
this.backdrop.style.pointerEvents = "none";
|
|
890
|
+
this.backdrop.style.willChange = "opacity";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
awaitTransition(cycle) {
|
|
894
|
+
return new Promise((resolve) => {
|
|
895
|
+
let done = false;
|
|
896
|
+
const finish = () => {
|
|
897
|
+
if (done) return;
|
|
898
|
+
done = true;
|
|
899
|
+
this.element.removeEventListener("transitionend", onEnd);
|
|
900
|
+
clearTimeout(timer);
|
|
901
|
+
resolve();
|
|
902
|
+
};
|
|
903
|
+
const onEnd = (e) => {
|
|
904
|
+
if (cycle !== this.cycleNonce) {
|
|
905
|
+
finish();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (e.propertyName === "transform") finish();
|
|
909
|
+
};
|
|
910
|
+
this.element.addEventListener("transitionend", onEnd);
|
|
911
|
+
const timer = window.setTimeout(finish, this.duration + 50);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
installInteractiveListeners() {
|
|
915
|
+
this.lifecycle.install();
|
|
916
|
+
if (this.swipeToCloseEnabled && !this.detachSwipe) {
|
|
917
|
+
this.detachSwipe = this.installSwipeToClose();
|
|
918
|
+
}
|
|
919
|
+
if (this.closeOnEscape && !this.detachEscape) {
|
|
920
|
+
const handler = (e) => {
|
|
921
|
+
if (e.key === "Escape" && this.isOpen_) void this.close("escape");
|
|
922
|
+
};
|
|
923
|
+
document.addEventListener("keydown", handler);
|
|
924
|
+
this.detachEscape = () => document.removeEventListener("keydown", handler);
|
|
925
|
+
}
|
|
926
|
+
if (this.closeOnBackdropEnabled && this.backdrop && !this.detachBackdrop) {
|
|
927
|
+
const handler = () => void this.close("backdrop");
|
|
928
|
+
this.backdrop.addEventListener("click", handler);
|
|
929
|
+
this.detachBackdrop = () => this.backdrop?.removeEventListener("click", handler);
|
|
930
|
+
}
|
|
931
|
+
if (this.closeOnBackEnabled && typeof window !== "undefined" && !this.detachHardwareBack) {
|
|
932
|
+
try {
|
|
933
|
+
history.pushState({ __bsOverlay: this.id }, "");
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
const onPop = () => {
|
|
937
|
+
if (this.isOpen_) void this.close("back");
|
|
938
|
+
};
|
|
939
|
+
window.addEventListener("popstate", onPop);
|
|
940
|
+
this.detachHardwareBack = () => window.removeEventListener("popstate", onPop);
|
|
941
|
+
}
|
|
942
|
+
if (this.closeOnOutsidePointerEnabled && !this.detachOutsidePointer) {
|
|
943
|
+
const handler = (e) => {
|
|
944
|
+
if (!this.isOpen_) return;
|
|
945
|
+
const target = e.target;
|
|
946
|
+
if (!target) return;
|
|
947
|
+
if (this.element.contains(target)) return;
|
|
948
|
+
if (this.backdrop && this.backdrop.contains(target)) return;
|
|
949
|
+
void this.close("outside-pointer");
|
|
950
|
+
};
|
|
951
|
+
document.addEventListener("pointerdown", handler, true);
|
|
952
|
+
this.detachOutsidePointer = () => document.removeEventListener("pointerdown", handler, true);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
releaseInteractiveListeners() {
|
|
956
|
+
this.detachEscape?.();
|
|
957
|
+
this.detachEscape = null;
|
|
958
|
+
this.detachBackdrop?.();
|
|
959
|
+
this.detachBackdrop = null;
|
|
960
|
+
this.detachHardwareBack?.();
|
|
961
|
+
this.detachHardwareBack = null;
|
|
962
|
+
this.detachSwipe?.();
|
|
963
|
+
this.detachSwipe = null;
|
|
964
|
+
this.detachOutsidePointer?.();
|
|
965
|
+
this.detachOutsidePointer = null;
|
|
966
|
+
this.lifecycle.release();
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Light pointer-driven dismiss. Tracks finger along the edge axis, follows
|
|
970
|
+
* with translate, dismisses if past `swipeThreshold` (fraction of panel
|
|
971
|
+
* size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
|
|
972
|
+
* Pure CSS-transform during drag — no rAF or physics required since the
|
|
973
|
+
* panel only moves one direction.
|
|
974
|
+
*/
|
|
975
|
+
installSwipeToClose() {
|
|
976
|
+
const el = this.element;
|
|
977
|
+
const edge = this.edge;
|
|
978
|
+
const isVertical = edge === "bottom" || edge === "top";
|
|
979
|
+
const sign = edge === "bottom" || edge === "right" ? 1 : -1;
|
|
980
|
+
const threshold = this.swipeThreshold;
|
|
981
|
+
const velocityThreshold = this.swipeVelocityThreshold;
|
|
982
|
+
let startX = 0;
|
|
983
|
+
let startY = 0;
|
|
984
|
+
let startT = 0;
|
|
985
|
+
let dragging = false;
|
|
986
|
+
let pointerId = null;
|
|
987
|
+
const onDown = (e) => {
|
|
988
|
+
if (e.button !== void 0 && e.button !== 0) return;
|
|
989
|
+
pointerId = e.pointerId;
|
|
990
|
+
startX = e.clientX;
|
|
991
|
+
startY = e.clientY;
|
|
992
|
+
startT = performance.now();
|
|
993
|
+
dragging = true;
|
|
994
|
+
el.style.transition = "none";
|
|
995
|
+
try {
|
|
996
|
+
el.setPointerCapture(e.pointerId);
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
const onMove = (e) => {
|
|
1001
|
+
if (!dragging || e.pointerId !== pointerId) return;
|
|
1002
|
+
const delta = isVertical ? e.clientY - startY : e.clientX - startX;
|
|
1003
|
+
const offset = Math.max(0, sign * delta);
|
|
1004
|
+
el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
|
|
1005
|
+
};
|
|
1006
|
+
const finish = (e) => {
|
|
1007
|
+
if (!dragging || e.pointerId !== pointerId) return;
|
|
1008
|
+
dragging = false;
|
|
1009
|
+
pointerId = null;
|
|
1010
|
+
const dt = performance.now() - startT || 1;
|
|
1011
|
+
const delta = isVertical ? e.clientY - startY : e.clientX - startX;
|
|
1012
|
+
const offset = Math.max(0, sign * delta);
|
|
1013
|
+
const velocity = sign * delta / dt;
|
|
1014
|
+
const size = isVertical ? el.offsetHeight : el.offsetWidth;
|
|
1015
|
+
const past = offset > size * threshold || velocity > velocityThreshold;
|
|
1016
|
+
el.style.transition = `transform ${this.duration}ms ${this.exitEasing}`;
|
|
1017
|
+
if (past) {
|
|
1018
|
+
void this.close("swipe");
|
|
1019
|
+
} else {
|
|
1020
|
+
el.style.transform = openTransformFor(this.enterAnimation);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
el.addEventListener("pointerdown", onDown);
|
|
1024
|
+
el.addEventListener("pointermove", onMove);
|
|
1025
|
+
el.addEventListener("pointerup", finish);
|
|
1026
|
+
el.addEventListener("pointercancel", finish);
|
|
1027
|
+
return () => {
|
|
1028
|
+
el.removeEventListener("pointerdown", onDown);
|
|
1029
|
+
el.removeEventListener("pointermove", onMove);
|
|
1030
|
+
el.removeEventListener("pointerup", finish);
|
|
1031
|
+
el.removeEventListener("pointercancel", finish);
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
registerInStack() {
|
|
1035
|
+
this.releaseStackEntry = sheetStack.push({
|
|
1036
|
+
id: this.id,
|
|
1037
|
+
setZIndex: (z) => {
|
|
1038
|
+
this.element.style.zIndex = String(z);
|
|
1039
|
+
if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
|
|
1040
|
+
},
|
|
1041
|
+
setIsTop: (isTop) => {
|
|
1042
|
+
if (this.backdrop) {
|
|
1043
|
+
this.backdrop.style.display = isTop ? "" : "none";
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var createOverlay = (opts) => new OverlayEngine(opts);
|
|
1050
|
+
|
|
1051
|
+
export { OVERLAY_PRESETS, OverlayEngine as Overlay, OverlayEngine, createOverlay };
|
|
1052
|
+
//# sourceMappingURL=overlay.js.map
|
|
1053
|
+
//# sourceMappingURL=overlay.js.map
|