@surdeddd/bottom-sheet 0.1.0 → 0.3.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/README.md +57 -21
- package/dist/BottomSheetEngine-CAymrH-P.d.cts +285 -0
- package/dist/BottomSheetEngine-DcqcuudN.d.ts +285 -0
- package/dist/element.cjs +775 -537
- package/dist/element.d.cts +9 -57
- package/dist/element.d.ts +9 -57
- package/dist/element.iife.global.js +2 -2
- package/dist/element.js +775 -537
- package/dist/index.cjs +760 -600
- package/dist/index.d.cts +18 -85
- package/dist/index.d.ts +18 -85
- package/dist/index.js +758 -601
- package/dist/integrations/formik.d.cts +2 -8
- package/dist/integrations/formik.d.ts +2 -8
- package/dist/integrations/react-hook-form.d.cts +2 -11
- package/dist/integrations/react-hook-form.d.ts +2 -11
- package/dist/overlay.cjs +168 -122
- package/dist/overlay.d.cts +14 -137
- package/dist/overlay.d.ts +14 -137
- package/dist/overlay.js +168 -122
- package/dist/preact.d.cts +2 -2
- package/dist/preact.d.ts +2 -2
- package/dist/preact.js +0 -1
- package/dist/qwik.cjs +667 -519
- package/dist/qwik.d.cts +3 -24
- package/dist/qwik.d.ts +3 -24
- package/dist/qwik.js +668 -520
- package/dist/react.cjs +873 -648
- package/dist/react.d.cts +34 -159
- package/dist/react.d.ts +34 -159
- package/dist/react.js +873 -648
- package/dist/solid.cjs +703 -560
- package/dist/solid.d.cts +264 -32
- package/dist/solid.d.ts +264 -32
- package/dist/solid.js +704 -561
- package/dist/solid.source.jsx +3215 -0
- package/dist/styles.css +37 -112
- package/dist/svelte-core.cjs +3149 -0
- package/dist/svelte-core.js +3147 -0
- package/dist/svelte-src/BottomSheet.svelte +210 -0
- package/dist/svelte-src/index.js +2 -0
- package/dist/svelte.cjs +718 -547
- package/dist/svelte.d.cts +100 -0
- package/dist/svelte.d.ts +15 -4
- package/dist/svelte.js +719 -548
- package/dist/themes/ios.css +0 -3
- package/dist/themes/material.css +0 -3
- package/dist/themes/vercel.css +0 -3
- package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
- package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
- package/dist/vue.cjs +677 -526
- package/dist/vue.d.cts +78 -0
- package/dist/vue.d.ts +12 -2
- package/dist/vue.js +678 -527
- package/package.json +105 -45
- package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
- package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/react.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var react = require('react');
|
|
4
|
+
var reactDom = require('react-dom');
|
|
4
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
6
|
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
@@ -25,6 +26,18 @@ var SheetStack = class {
|
|
|
25
26
|
this.entries.splice(idx, 1);
|
|
26
27
|
this.recompute();
|
|
27
28
|
}
|
|
29
|
+
promote(id) {
|
|
30
|
+
const idx = this.entries.findIndex((e) => e.id === id);
|
|
31
|
+
if (idx === -1) return;
|
|
32
|
+
if (idx !== this.entries.length - 1) {
|
|
33
|
+
const [entry] = this.entries.splice(idx, 1);
|
|
34
|
+
this.entries.push(entry);
|
|
35
|
+
}
|
|
36
|
+
this.recompute();
|
|
37
|
+
}
|
|
38
|
+
update() {
|
|
39
|
+
this.recompute();
|
|
40
|
+
}
|
|
28
41
|
size() {
|
|
29
42
|
return this.entries.length;
|
|
30
43
|
}
|
|
@@ -32,9 +45,24 @@ var SheetStack = class {
|
|
|
32
45
|
this.entries = [];
|
|
33
46
|
}
|
|
34
47
|
recompute() {
|
|
48
|
+
let top = null;
|
|
49
|
+
const open = [];
|
|
50
|
+
for (const entry of this.entries) {
|
|
51
|
+
if (!entry.isOpen || entry.isOpen()) {
|
|
52
|
+
top = entry;
|
|
53
|
+
open.push(entry);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!top && this.entries.length > 0) {
|
|
57
|
+
top = this.entries[this.entries.length - 1];
|
|
58
|
+
}
|
|
35
59
|
this.entries.forEach((entry, i) => {
|
|
36
60
|
entry.setZIndex(BASE_Z + i * STEP);
|
|
37
|
-
entry.setIsTop(
|
|
61
|
+
entry.setIsTop(entry === top);
|
|
62
|
+
if (entry.setDepth) {
|
|
63
|
+
const openIdx = open.indexOf(entry);
|
|
64
|
+
entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
|
|
65
|
+
}
|
|
38
66
|
});
|
|
39
67
|
}
|
|
40
68
|
};
|
|
@@ -239,11 +267,6 @@ var SnapResolver = class {
|
|
|
239
267
|
this.allowedIds = allowed ?? raw.map((p) => p.id);
|
|
240
268
|
this.recompute();
|
|
241
269
|
}
|
|
242
|
-
/**
|
|
243
|
-
* Re-resolve raw → resolved against the current viewport / fit-measure,
|
|
244
|
-
* recompute maxAxisSize, invalidate range cache, notify engine. Called on
|
|
245
|
-
* resize, orientationchange, visualViewport changes, and snap-list swaps.
|
|
246
|
-
*/
|
|
247
270
|
recompute() {
|
|
248
271
|
this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
|
|
249
272
|
this.maxAxisSize = this.resolved.reduce(
|
|
@@ -253,15 +276,10 @@ var SnapResolver = class {
|
|
|
253
276
|
this.rangeCache = null;
|
|
254
277
|
this.onMaxAxisSizeChange?.(this.maxAxisSize);
|
|
255
278
|
}
|
|
256
|
-
/** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
|
|
257
279
|
setRaw(raw) {
|
|
258
280
|
this.raw = raw;
|
|
259
281
|
this.recompute();
|
|
260
282
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Replace the allow-list. Range cache depends on `(resolvedSnaps ×
|
|
263
|
-
* allowedIds)` so flipping ids alone invalidates without a full recompute.
|
|
264
|
-
*/
|
|
265
283
|
setAllowedIds(ids) {
|
|
266
284
|
this.allowedIds = ids;
|
|
267
285
|
this.rangeCache = null;
|
|
@@ -269,11 +287,6 @@ var SnapResolver = class {
|
|
|
269
287
|
findById(id) {
|
|
270
288
|
return findById(id, this.resolved);
|
|
271
289
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Pure drag-settle target resolution. Engine passes the gesture-specific
|
|
274
|
-
* fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
|
|
275
|
-
* `size`/`activeId` since those are still engine-owned.
|
|
276
|
-
*/
|
|
277
290
|
findDragSettleTarget(args) {
|
|
278
291
|
return findDragSettleTarget({
|
|
279
292
|
...args,
|
|
@@ -282,18 +295,9 @@ var SnapResolver = class {
|
|
|
282
295
|
maxAxisSize: this.maxAxisSize
|
|
283
296
|
});
|
|
284
297
|
}
|
|
285
|
-
/**
|
|
286
|
-
* Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
|
|
287
|
-
* applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
|
|
288
|
-
* to one recompute per geometry/allow-list change.
|
|
289
|
-
*/
|
|
290
298
|
getAllowedRange() {
|
|
291
299
|
return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
|
|
292
300
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Read-only view of resolved snaps. Returned as `readonly` to discourage
|
|
295
|
-
* accidental mutation by feature install sites that close over this list.
|
|
296
|
-
*/
|
|
297
301
|
getResolvedSnaps() {
|
|
298
302
|
return this.resolved;
|
|
299
303
|
}
|
|
@@ -303,18 +307,6 @@ var SnapResolver = class {
|
|
|
303
307
|
getMaxAxisSize() {
|
|
304
308
|
return this.maxAxisSize;
|
|
305
309
|
}
|
|
306
|
-
/**
|
|
307
|
-
* External clamp override. The resize-observer feature compares
|
|
308
|
-
* `getMaxAxisSize()` against the live viewport and shrinks if the largest
|
|
309
|
-
* resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
|
|
310
|
-
* window resize). The next `recompute()` overwrites this clamp from the
|
|
311
|
-
* resolved-snap reduction.
|
|
312
|
-
*
|
|
313
|
-
* Does NOT invalidate the range cache — viewport clamping doesn't change
|
|
314
|
-
* `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
|
|
315
|
-
* so the resolver owns the layout-axis style write end-to-end (symmetric
|
|
316
|
-
* with `recompute()` — feature call sites no longer duplicate the write).
|
|
317
|
-
*/
|
|
318
310
|
setMaxAxisSize(n) {
|
|
319
311
|
this.maxAxisSize = n;
|
|
320
312
|
this.onMaxAxisSizeChange?.(n);
|
|
@@ -361,14 +353,17 @@ function installAutoCollapse(deps) {
|
|
|
361
353
|
const fire = () => {
|
|
362
354
|
if (deps.isDestroyed()) return;
|
|
363
355
|
if (deps.isDragging()) return;
|
|
364
|
-
const
|
|
365
|
-
|
|
356
|
+
const active = deps.resolveSnap(deps.getActiveId());
|
|
357
|
+
if (!active || active.size === 0) return;
|
|
358
|
+
let target = null;
|
|
359
|
+
for (const id of deps.getAllowedIds()) {
|
|
366
360
|
const snap = deps.resolveSnap(id);
|
|
367
|
-
|
|
368
|
-
|
|
361
|
+
if (!snap || snap.size <= 0) continue;
|
|
362
|
+
if (!target || snap.size < target.size) target = snap;
|
|
363
|
+
}
|
|
369
364
|
if (!target) return;
|
|
370
|
-
if (target ===
|
|
371
|
-
deps.snapTo(target);
|
|
365
|
+
if (target.id === active.id || target.size >= active.size) return;
|
|
366
|
+
deps.snapTo(target.id);
|
|
372
367
|
};
|
|
373
368
|
const reset = () => {
|
|
374
369
|
clearTimer();
|
|
@@ -379,6 +374,7 @@ function installAutoCollapse(deps) {
|
|
|
379
374
|
}, ms);
|
|
380
375
|
};
|
|
381
376
|
const unsubscribe = deps.on("snap", () => reset());
|
|
377
|
+
if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
|
|
382
378
|
return () => {
|
|
383
379
|
clearTimer();
|
|
384
380
|
unsubscribe();
|
|
@@ -391,6 +387,7 @@ function notifyLinkedSheets(sheets, self) {
|
|
|
391
387
|
for (const linked of sheets) {
|
|
392
388
|
if (linked === self) continue;
|
|
393
389
|
try {
|
|
390
|
+
if (linked.state.size === 0) continue;
|
|
394
391
|
const ids = linked.getAllowedIds();
|
|
395
392
|
const target = ids.find((id) => id !== "closed");
|
|
396
393
|
if (target && target !== linked.state.activeId) {
|
|
@@ -437,7 +434,7 @@ function installRoute(deps) {
|
|
|
437
434
|
const onPop = (event) => {
|
|
438
435
|
if (deps.isDestroyed()) return;
|
|
439
436
|
const state = event.state;
|
|
440
|
-
if (state
|
|
437
|
+
if ((!state || state.__bsRouted !== target) && deps.getSize() > 0) {
|
|
441
438
|
routePushed = false;
|
|
442
439
|
void deps.close();
|
|
443
440
|
}
|
|
@@ -591,13 +588,17 @@ function installResizeObserver(deps) {
|
|
|
591
588
|
if (viewportSize2 > 0 && viewportSize2 < max) {
|
|
592
589
|
deps.setMaxAxisSize(viewportSize2);
|
|
593
590
|
}
|
|
591
|
+
const wasAnimating = deps.isAnimating?.() ?? false;
|
|
594
592
|
deps.cancelInFlight();
|
|
595
593
|
deps.newCycle();
|
|
596
594
|
const current = deps.resolveActiveSnap();
|
|
597
595
|
const newMax = deps.getMaxAxisSize();
|
|
598
596
|
const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
|
|
599
597
|
deps.setSize(targetSize);
|
|
600
|
-
if (current && !deps.isDragging())
|
|
598
|
+
if (current && !deps.isDragging()) {
|
|
599
|
+
deps.applySize(targetSize);
|
|
600
|
+
if (wasAnimating) deps.resyncAfterCancel?.();
|
|
601
|
+
}
|
|
601
602
|
};
|
|
602
603
|
let resizeObserver = null;
|
|
603
604
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -689,73 +690,130 @@ var WriteSentinel = class {
|
|
|
689
690
|
constructor() {
|
|
690
691
|
__publicField(this, "last", -1);
|
|
691
692
|
}
|
|
692
|
-
/**
|
|
693
|
-
* Returns `true` (and updates the cached value) if `next` differs from the
|
|
694
|
-
* cached `last` by more than `epsilon`. Returns `false` (and leaves the
|
|
695
|
-
* cache untouched) when the delta is within epsilon — caller skips the
|
|
696
|
-
* CSSOM write.
|
|
697
|
-
*/
|
|
698
693
|
shouldWrite(next, epsilon) {
|
|
699
694
|
if (Math.abs(next - this.last) <= epsilon) return false;
|
|
700
695
|
this.last = next;
|
|
701
696
|
return true;
|
|
702
697
|
}
|
|
703
|
-
/**
|
|
704
|
-
* Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
|
|
705
|
-
* non-negative `next` exceeds the epsilon comparison. Called from setters
|
|
706
|
-
* that mutate state under the cache (range/mode/enabled flips that change
|
|
707
|
-
* the progress→opacity mapping but don't move `progress` itself).
|
|
708
|
-
*/
|
|
709
698
|
invalidate() {
|
|
710
699
|
this.last = -1;
|
|
711
700
|
}
|
|
712
|
-
/**
|
|
713
|
-
* Record `value` as the last-written without performing a write. Used
|
|
714
|
-
* when a setter writes a literal value through a different path (e.g.
|
|
715
|
-
* `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
|
|
716
|
-
* `shouldWrite` calls to short-circuit against that known cached state.
|
|
717
|
-
*/
|
|
718
701
|
setLastWritten(value) {
|
|
719
702
|
this.last = value;
|
|
720
703
|
}
|
|
721
|
-
/** Read-only accessor for diagnostic / dedup-aware peek. */
|
|
722
704
|
get value() {
|
|
723
705
|
return this.last;
|
|
724
706
|
}
|
|
725
707
|
};
|
|
726
708
|
|
|
709
|
+
// src/core/primitives/devWarn.ts
|
|
710
|
+
function devWarn(...args) {
|
|
711
|
+
if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
|
|
712
|
+
console.warn(...args);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/core/primitives/overlay-position.ts
|
|
717
|
+
function resolveSheetAnchoredStyle(mode, position, inset) {
|
|
718
|
+
const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
|
|
719
|
+
switch (mode) {
|
|
720
|
+
case "bottom":
|
|
721
|
+
if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
|
|
722
|
+
if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
|
|
723
|
+
return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
|
|
724
|
+
case "top":
|
|
725
|
+
if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
|
|
726
|
+
if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
|
|
727
|
+
return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
|
|
728
|
+
case "left":
|
|
729
|
+
if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
|
|
730
|
+
if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
|
|
731
|
+
return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
|
|
732
|
+
case "right":
|
|
733
|
+
if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
|
|
734
|
+
if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
|
|
735
|
+
return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function applyOverlayPosition(ws, mode, position, inset) {
|
|
739
|
+
switch (position) {
|
|
740
|
+
case "top-left":
|
|
741
|
+
ws.top = inset;
|
|
742
|
+
ws.left = inset;
|
|
743
|
+
break;
|
|
744
|
+
case "top-center":
|
|
745
|
+
ws.top = inset;
|
|
746
|
+
ws.left = "50%";
|
|
747
|
+
ws.transform = "translateX(-50%)";
|
|
748
|
+
break;
|
|
749
|
+
case "top-right":
|
|
750
|
+
ws.top = inset;
|
|
751
|
+
ws.right = inset;
|
|
752
|
+
break;
|
|
753
|
+
case "center-left":
|
|
754
|
+
ws.top = "50%";
|
|
755
|
+
ws.left = inset;
|
|
756
|
+
ws.transform = "translateY(-50%)";
|
|
757
|
+
break;
|
|
758
|
+
case "center":
|
|
759
|
+
ws.top = "50%";
|
|
760
|
+
ws.left = "50%";
|
|
761
|
+
ws.transform = "translate(-50%, -50%)";
|
|
762
|
+
break;
|
|
763
|
+
case "center-right":
|
|
764
|
+
ws.top = "50%";
|
|
765
|
+
ws.right = inset;
|
|
766
|
+
ws.transform = "translateY(-50%)";
|
|
767
|
+
break;
|
|
768
|
+
case "bottom-left":
|
|
769
|
+
ws.bottom = inset;
|
|
770
|
+
ws.left = inset;
|
|
771
|
+
break;
|
|
772
|
+
case "bottom-center":
|
|
773
|
+
ws.bottom = inset;
|
|
774
|
+
ws.left = "50%";
|
|
775
|
+
ws.transform = "translateX(-50%)";
|
|
776
|
+
break;
|
|
777
|
+
case "bottom-right":
|
|
778
|
+
ws.bottom = inset;
|
|
779
|
+
ws.right = inset;
|
|
780
|
+
break;
|
|
781
|
+
case "sheet-top-left":
|
|
782
|
+
case "sheet-top-center":
|
|
783
|
+
case "sheet-top-right": {
|
|
784
|
+
const style = resolveSheetAnchoredStyle(mode, position, inset);
|
|
785
|
+
if (style.top !== void 0) ws.top = style.top;
|
|
786
|
+
if (style.bottom !== void 0) ws.bottom = style.bottom;
|
|
787
|
+
if (style.left !== void 0) ws.left = style.left;
|
|
788
|
+
if (style.right !== void 0) ws.right = style.right;
|
|
789
|
+
if (style.transform !== void 0) ws.transform = style.transform;
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
727
795
|
// src/core/controllers/scrim-controller.ts
|
|
796
|
+
var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
|
|
728
797
|
var ScrimController = class {
|
|
729
798
|
constructor(deps, opts) {
|
|
730
|
-
// ── DOM refs (mirror engine's; cached for hot path) ─────────────────
|
|
731
799
|
__publicField(this, "screenComponent");
|
|
732
800
|
__publicField(this, "backdrop");
|
|
733
801
|
__publicField(this, "mode");
|
|
734
|
-
// ── Engine callbacks ────────────────────────────────────────────────
|
|
735
802
|
__publicField(this, "isDestroyed");
|
|
736
803
|
__publicField(this, "isTopSheet");
|
|
737
804
|
__publicField(this, "getAllowedIds");
|
|
738
805
|
__publicField(this, "getResolvedSnaps");
|
|
739
806
|
__publicField(this, "snapToFn");
|
|
740
|
-
|
|
807
|
+
__publicField(this, "closeFn");
|
|
808
|
+
__publicField(this, "scrimInteractive", false);
|
|
741
809
|
__publicField(this, "scrimMode");
|
|
742
810
|
__publicField(this, "backdropRange");
|
|
743
811
|
__publicField(this, "screenRange");
|
|
744
|
-
// setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
|
|
745
|
-
// ranges so re-enable can restore prior values without a remount. Public
|
|
746
|
-
// so tests / introspection can read it without an `as any` cast.
|
|
747
812
|
__publicField(this, "scrimEnabled", true);
|
|
748
813
|
__publicField(this, "savedScrimRanges", null);
|
|
749
|
-
// ── Tap-to-close ────────────────────────────────────────────────────
|
|
750
814
|
__publicField(this, "scrimTapToCloseEnabled", false);
|
|
751
815
|
__publicField(this, "detachScrimTap", null);
|
|
752
|
-
// ── Scrim overlay slot (positioned floating element) ────────────────
|
|
753
816
|
__publicField(this, "scrimOverlayTeardown", null);
|
|
754
|
-
// ── Hot-path opacity dedup caches (per-frame writes) ────────────────
|
|
755
|
-
// Numeric sentinels migrated to WriteSentinel primitive — single
|
|
756
|
-
// invalidation surface, shared with the engine's size/progress sentinels.
|
|
757
|
-
// The two string-typed sentinels (`Pointer`, `Display`) keep their inline
|
|
758
|
-
// pattern: equality on string union, no epsilon, different reset semantics.
|
|
759
817
|
__publicField(this, "backdropOpacitySentinel", new WriteSentinel());
|
|
760
818
|
__publicField(this, "screenOpacitySentinel", new WriteSentinel());
|
|
761
819
|
__publicField(this, "lastBackdropPointer", "");
|
|
@@ -768,6 +826,7 @@ var ScrimController = class {
|
|
|
768
826
|
this.getAllowedIds = deps.getAllowedIds;
|
|
769
827
|
this.getResolvedSnaps = deps.getResolvedSnaps;
|
|
770
828
|
this.snapToFn = deps.snapTo;
|
|
829
|
+
this.closeFn = deps.close;
|
|
771
830
|
this.scrimMode = opts.scrimMode ?? "full";
|
|
772
831
|
const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
|
|
773
832
|
this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
|
|
@@ -790,41 +849,15 @@ var ScrimController = class {
|
|
|
790
849
|
}
|
|
791
850
|
}
|
|
792
851
|
}
|
|
793
|
-
/**
|
|
794
|
-
* Invalidate the opacity dedup caches. Called from any setter that mutates
|
|
795
|
-
* range/mode/enabled — opacity is a pure function of (progress, range,
|
|
796
|
-
* enabled), so a config change makes the cached values stale even if
|
|
797
|
-
* `progress` itself didn't move. Next `applyOpacity(progress, false)` call
|
|
798
|
-
* sees the -1 sentinel and re-runs the math instead of short-circuiting.
|
|
799
|
-
*
|
|
800
|
-
* Public for the engine: `setSnapPoints` mutates resolved geometry which
|
|
801
|
-
* shifts `progress→opacity` mapping under the cache; engine calls this to
|
|
802
|
-
* keep the next render frame correct on a static sheet.
|
|
803
|
-
* @internal
|
|
804
|
-
*/
|
|
805
852
|
invalidateOpacityCache() {
|
|
806
853
|
this.backdropOpacitySentinel.invalidate();
|
|
807
854
|
this.screenOpacitySentinel.invalidate();
|
|
808
855
|
}
|
|
809
|
-
/** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
|
|
810
856
|
attach() {
|
|
811
857
|
if (this.scrimTapToCloseEnabled) {
|
|
812
858
|
this.setScrimTapToClose(true);
|
|
813
859
|
}
|
|
814
860
|
}
|
|
815
|
-
// ── Hot-path: opacity computation called per-frame from engine.applySize ──
|
|
816
|
-
/**
|
|
817
|
-
* Per-frame opacity writer. Computes backdrop + screen opacity from the
|
|
818
|
-
* sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
|
|
819
|
-
* caches so steady-state frames don't trigger style recalc.
|
|
820
|
-
*
|
|
821
|
-
* `progressChanged` is the engine's already-computed dedup gate — when
|
|
822
|
-
* `false`, opacity is by construction a pure function of `progress` so
|
|
823
|
-
* cached values are still valid; we short-circuit the Math.min/max +
|
|
824
|
-
* division work entirely. Setters that mutate ranges/mode/enabled
|
|
825
|
-
* invalidate the cache by writing -1 sentinels so the next call re-runs
|
|
826
|
-
* the math.
|
|
827
|
-
*/
|
|
828
861
|
applyOpacity(progress, progressChanged) {
|
|
829
862
|
if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
|
|
830
863
|
return;
|
|
@@ -870,80 +903,30 @@ var ScrimController = class {
|
|
|
870
903
|
}
|
|
871
904
|
}
|
|
872
905
|
}
|
|
873
|
-
// ── Public setters (engine forwards these 1:1) ─────────────────────
|
|
874
|
-
/**
|
|
875
|
-
* Replace the [start, end] progress window in which the backdrop fades 0→1.
|
|
876
|
-
* @param range - Tuple of two progress values in 0..1 (start <= end).
|
|
877
|
-
* @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
|
|
878
|
-
* @example
|
|
879
|
-
* // Backdrop only starts darkening after the sheet crosses 30% open.
|
|
880
|
-
* engine.setBackdropRange([0.3, 1]);
|
|
881
|
-
*/
|
|
882
906
|
setBackdropRange(range, applySize) {
|
|
883
907
|
if (this.isDestroyed()) return;
|
|
884
908
|
this.backdropRange = range;
|
|
885
909
|
this.invalidateOpacityCache();
|
|
886
910
|
applySize();
|
|
887
911
|
}
|
|
888
|
-
/**
|
|
889
|
-
* Replace the [start, end] progress window in which the screen scrim fades 0→1.
|
|
890
|
-
* @param range - Tuple of two progress values in 0..1 (start <= end).
|
|
891
|
-
* @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
|
|
892
|
-
* @example
|
|
893
|
-
* // Scrim is fully opaque only when the sheet is past 50% — softer feel.
|
|
894
|
-
* engine.setScreenRange([0, 0.5]);
|
|
895
|
-
*/
|
|
896
912
|
setScreenRange(range, applySize) {
|
|
897
913
|
if (this.isDestroyed()) return;
|
|
898
914
|
this.screenRange = range;
|
|
899
915
|
this.invalidateOpacityCache();
|
|
900
916
|
applySize();
|
|
901
917
|
}
|
|
902
|
-
/**
|
|
903
|
-
* Set the scrim background color. Pass `null`/`undefined` to clear the
|
|
904
|
-
* inline write and let consumer CSS take over.
|
|
905
|
-
* @param color - Any valid CSS color string, or `null`/`undefined` to clear.
|
|
906
|
-
* @example
|
|
907
|
-
* engine.setScrimColor("rgba(0, 0, 0, 0.6)");
|
|
908
|
-
* engine.setScrimColor(null); // hand back to consumer CSS
|
|
909
|
-
*/
|
|
910
918
|
setScrimColor(color) {
|
|
911
919
|
if (this.isDestroyed()) return;
|
|
912
920
|
this.applyScrimStyles({ color: color === void 0 ? null : color });
|
|
913
921
|
}
|
|
914
|
-
/**
|
|
915
|
-
* Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
|
|
916
|
-
* @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
|
|
917
|
-
* `null`/`undefined` to remove the filter.
|
|
918
|
-
* @example
|
|
919
|
-
* engine.setScrimBlur("12px"); // frosted-glass effect
|
|
920
|
-
* engine.setScrimBlur(null); // disable blur
|
|
921
|
-
*/
|
|
922
922
|
setScrimBlur(blur) {
|
|
923
923
|
if (this.isDestroyed()) return;
|
|
924
924
|
this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
|
|
925
925
|
}
|
|
926
|
-
/**
|
|
927
|
-
* Toggle whether the scrim layer captures pointer events.
|
|
928
|
-
* @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
|
|
929
|
-
* @example
|
|
930
|
-
* engine.setScrimInteractive(true); // scrim swallows clicks
|
|
931
|
-
* engine.setScrimInteractive(false); // clicks pass through to content below
|
|
932
|
-
*/
|
|
933
926
|
setScrimInteractive(interactive) {
|
|
934
927
|
if (this.isDestroyed()) return;
|
|
935
928
|
this.applyScrimStyles({ interactive });
|
|
936
929
|
}
|
|
937
|
-
/**
|
|
938
|
-
* Switch scrim positioning at runtime.
|
|
939
|
-
* @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
|
|
940
|
-
* so only the area NOT covered by the sheet is dimmed), or
|
|
941
|
-
* `"off"` (display:none + opacity:0 — no remount needed).
|
|
942
|
-
* @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
|
|
943
|
-
* @example
|
|
944
|
-
* engine.setScrimMode("above-sheet"); // dim only the area above the sheet
|
|
945
|
-
* engine.setScrimMode("off"); // disable scrim layer
|
|
946
|
-
*/
|
|
947
930
|
setScrimMode(mode, applySize) {
|
|
948
931
|
if (this.isDestroyed()) return;
|
|
949
932
|
if (mode === this.scrimMode) return;
|
|
@@ -973,15 +956,6 @@ var ScrimController = class {
|
|
|
973
956
|
}
|
|
974
957
|
applySize();
|
|
975
958
|
}
|
|
976
|
-
/**
|
|
977
|
-
* Enable or disable click-on-scrim → snap to first non-zero allowed snap.
|
|
978
|
-
* Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
|
|
979
|
-
* click listener actually fires.
|
|
980
|
-
* @param enabled - `true` installs the click listener; `false` removes it.
|
|
981
|
-
* @example
|
|
982
|
-
* engine.setScrimTapToClose(true); // tap outside sheet collapses it
|
|
983
|
-
* engine.setScrimTapToClose(false); // require explicit close button
|
|
984
|
-
*/
|
|
985
959
|
setScrimTapToClose(enabled) {
|
|
986
960
|
if (this.isDestroyed()) return;
|
|
987
961
|
if (!this.screenComponent) return;
|
|
@@ -994,11 +968,15 @@ var ScrimController = class {
|
|
|
994
968
|
}
|
|
995
969
|
const onClick = () => {
|
|
996
970
|
if (this.isDestroyed()) return;
|
|
971
|
+
if (this.closeFn) {
|
|
972
|
+
this.closeFn();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
997
975
|
const allowed = this.getAllowedIds();
|
|
998
976
|
const snaps = this.getResolvedSnaps();
|
|
999
977
|
const fallback = allowed.find((id) => {
|
|
1000
978
|
const snap = findById(id, snaps);
|
|
1001
|
-
return snap !== null && snap.size
|
|
979
|
+
return snap !== null && snap.size === 0;
|
|
1002
980
|
});
|
|
1003
981
|
if (fallback) this.snapToFn(fallback);
|
|
1004
982
|
};
|
|
@@ -1011,20 +989,11 @@ var ScrimController = class {
|
|
|
1011
989
|
this.scrimTapToCloseEnabled = false;
|
|
1012
990
|
}
|
|
1013
991
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Toggle the scrim layer without remounting. Disabling stashes current
|
|
1016
|
-
* `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
|
|
1017
|
-
* formula → 0 across the whole range); re-enabling restores the saved values.
|
|
1018
|
-
* @param enabled - `false` hides scrim, `true` restores prior ranges.
|
|
1019
|
-
* @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
|
|
1020
|
-
* @example
|
|
1021
|
-
* engine.setScrimEnabled(false); // hide scrim, ranges remembered
|
|
1022
|
-
* engine.setScrimEnabled(true); // restore prior ranges (no remount)
|
|
1023
|
-
*/
|
|
1024
992
|
setScrimEnabled(enabled, applySize) {
|
|
1025
993
|
if (this.isDestroyed()) return;
|
|
1026
994
|
if (enabled === this.scrimEnabled) return;
|
|
1027
995
|
this.scrimEnabled = enabled;
|
|
996
|
+
this.invalidateOpacityCache();
|
|
1028
997
|
if (!enabled) {
|
|
1029
998
|
this.savedScrimRanges = {
|
|
1030
999
|
screen: this.screenRange,
|
|
@@ -1039,20 +1008,6 @@ var ScrimController = class {
|
|
|
1039
1008
|
}
|
|
1040
1009
|
applySize();
|
|
1041
1010
|
}
|
|
1042
|
-
/**
|
|
1043
|
-
* Batch-update multiple scrim fields with a single applySize() at the end.
|
|
1044
|
-
* If `preset` is set, it seeds color/blur/interactive/range first; explicit
|
|
1045
|
-
* fields override.
|
|
1046
|
-
* @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
|
|
1047
|
-
* color, blur, interactive, range.
|
|
1048
|
-
* @param applySize - Engine-supplied recompute hook; runs ONCE per call.
|
|
1049
|
-
* @example
|
|
1050
|
-
* engine.setScrim({
|
|
1051
|
-
* preset: "frosted",
|
|
1052
|
-
* mode: "above-sheet",
|
|
1053
|
-
* tapToClose: true,
|
|
1054
|
-
* });
|
|
1055
|
-
*/
|
|
1056
1011
|
setScrim(opts, applySize) {
|
|
1057
1012
|
if (this.isDestroyed()) return;
|
|
1058
1013
|
if (opts.preset) {
|
|
@@ -1062,7 +1017,12 @@ var ScrimController = class {
|
|
|
1062
1017
|
blur: cfg.blur ?? null,
|
|
1063
1018
|
interactive: cfg.interactive
|
|
1064
1019
|
});
|
|
1065
|
-
this.
|
|
1020
|
+
if (!this.scrimEnabled && this.savedScrimRanges) {
|
|
1021
|
+
this.savedScrimRanges = { screen: cfg.range, backdrop: cfg.range };
|
|
1022
|
+
} else {
|
|
1023
|
+
this.screenRange = cfg.range;
|
|
1024
|
+
this.backdropRange = cfg.range;
|
|
1025
|
+
}
|
|
1066
1026
|
}
|
|
1067
1027
|
const noop = () => {
|
|
1068
1028
|
};
|
|
@@ -1075,27 +1035,18 @@ var ScrimController = class {
|
|
|
1075
1035
|
interactive: opts.interactive
|
|
1076
1036
|
});
|
|
1077
1037
|
if (opts.range !== void 0) {
|
|
1078
|
-
this.
|
|
1038
|
+
if (!this.scrimEnabled && this.savedScrimRanges) {
|
|
1039
|
+
this.savedScrimRanges = {
|
|
1040
|
+
screen: opts.range,
|
|
1041
|
+
backdrop: this.savedScrimRanges.backdrop
|
|
1042
|
+
};
|
|
1043
|
+
} else {
|
|
1044
|
+
this.screenRange = opts.range;
|
|
1045
|
+
}
|
|
1079
1046
|
}
|
|
1080
1047
|
this.invalidateOpacityCache();
|
|
1081
1048
|
applySize();
|
|
1082
1049
|
}
|
|
1083
|
-
/**
|
|
1084
|
-
* Inject a positioned floating element into the scrim layer (e.g. a close
|
|
1085
|
-
* button anchored top-right). Replaces any prior overlay so repeated calls
|
|
1086
|
-
* (reactive effects re-running) don't accumulate wrappers.
|
|
1087
|
-
* @param opts - Children element, optional `inset` (default `"16px"`),
|
|
1088
|
-
* `position` (9-way grid, default `"top-right"`), and
|
|
1089
|
-
* `interactive` flag (default `true`).
|
|
1090
|
-
* @returns Teardown function that removes the wrapper from the DOM.
|
|
1091
|
-
* @example
|
|
1092
|
-
* const teardown = engine.setScrimOverlay({
|
|
1093
|
-
* children: closeBtn,
|
|
1094
|
-
* position: "top-right",
|
|
1095
|
-
* inset: "20px",
|
|
1096
|
-
* });
|
|
1097
|
-
* // later: teardown(); // remove the floating button
|
|
1098
|
-
*/
|
|
1099
1050
|
setScrimOverlay(opts) {
|
|
1100
1051
|
if (this.isDestroyed()) return () => {
|
|
1101
1052
|
};
|
|
@@ -1103,61 +1054,38 @@ var ScrimController = class {
|
|
|
1103
1054
|
};
|
|
1104
1055
|
this.scrimOverlayTeardown?.();
|
|
1105
1056
|
const screen = this.screenComponent;
|
|
1106
|
-
const
|
|
1057
|
+
const insetRaw = opts.inset ?? "16px";
|
|
1058
|
+
const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
|
|
1059
|
+
if (inset !== insetRaw) {
|
|
1060
|
+
devWarn(
|
|
1061
|
+
`[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
|
|
1065
|
+
devWarn(
|
|
1066
|
+
"[bottom-sheet] setScrimOverlay: children belong to a different document than the scrim. Cross-document adoption can drop event listeners; construct children via the same document as the sheet root."
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1107
1069
|
const position = opts.position ?? "top-right";
|
|
1108
1070
|
const interactive = opts.interactive ?? true;
|
|
1109
1071
|
const wrapper = screen.ownerDocument.createElement("div");
|
|
1110
1072
|
wrapper.className = "bs-scrim-overlay";
|
|
1111
1073
|
const ws = wrapper.style;
|
|
1112
1074
|
ws.position = "absolute";
|
|
1113
|
-
|
|
1114
|
-
case "top-left":
|
|
1115
|
-
ws.top = inset;
|
|
1116
|
-
ws.left = inset;
|
|
1117
|
-
break;
|
|
1118
|
-
case "top-center":
|
|
1119
|
-
ws.top = inset;
|
|
1120
|
-
ws.left = "50%";
|
|
1121
|
-
ws.transform = "translateX(-50%)";
|
|
1122
|
-
break;
|
|
1123
|
-
case "top-right":
|
|
1124
|
-
ws.top = inset;
|
|
1125
|
-
ws.right = inset;
|
|
1126
|
-
break;
|
|
1127
|
-
case "center-left":
|
|
1128
|
-
ws.top = "50%";
|
|
1129
|
-
ws.left = inset;
|
|
1130
|
-
ws.transform = "translateY(-50%)";
|
|
1131
|
-
break;
|
|
1132
|
-
case "center":
|
|
1133
|
-
ws.top = "50%";
|
|
1134
|
-
ws.left = "50%";
|
|
1135
|
-
ws.transform = "translate(-50%, -50%)";
|
|
1136
|
-
break;
|
|
1137
|
-
case "center-right":
|
|
1138
|
-
ws.top = "50%";
|
|
1139
|
-
ws.right = inset;
|
|
1140
|
-
ws.transform = "translateY(-50%)";
|
|
1141
|
-
break;
|
|
1142
|
-
case "bottom-left":
|
|
1143
|
-
ws.bottom = inset;
|
|
1144
|
-
ws.left = inset;
|
|
1145
|
-
break;
|
|
1146
|
-
case "bottom-center":
|
|
1147
|
-
ws.bottom = inset;
|
|
1148
|
-
ws.left = "50%";
|
|
1149
|
-
ws.transform = "translateX(-50%)";
|
|
1150
|
-
break;
|
|
1151
|
-
case "bottom-right":
|
|
1152
|
-
ws.bottom = inset;
|
|
1153
|
-
ws.right = inset;
|
|
1154
|
-
break;
|
|
1155
|
-
}
|
|
1075
|
+
applyOverlayPosition(ws, this.mode, position, inset);
|
|
1156
1076
|
if (interactive) {
|
|
1157
1077
|
ws.pointerEvents = "auto";
|
|
1158
1078
|
}
|
|
1159
1079
|
wrapper.appendChild(opts.children);
|
|
1160
|
-
screen.
|
|
1080
|
+
const host = screen.parentElement;
|
|
1081
|
+
if (!host) {
|
|
1082
|
+
devWarn(
|
|
1083
|
+
"[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
|
|
1084
|
+
);
|
|
1085
|
+
return () => {
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
host.appendChild(wrapper);
|
|
1161
1089
|
const teardown = () => {
|
|
1162
1090
|
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
|
|
1163
1091
|
if (this.scrimOverlayTeardown === teardown) {
|
|
@@ -1167,7 +1095,6 @@ var ScrimController = class {
|
|
|
1167
1095
|
this.scrimOverlayTeardown = teardown;
|
|
1168
1096
|
return teardown;
|
|
1169
1097
|
}
|
|
1170
|
-
/** Tear down toggle-state listeners + remove any injected overlay wrapper. */
|
|
1171
1098
|
destroy() {
|
|
1172
1099
|
this.detachScrimTap?.();
|
|
1173
1100
|
this.detachScrimTap = null;
|
|
@@ -1186,8 +1113,6 @@ var ScrimController = class {
|
|
|
1186
1113
|
this.lastBackdropPointer = "";
|
|
1187
1114
|
this.lastScreenDisplay = "";
|
|
1188
1115
|
}
|
|
1189
|
-
// ── Private helpers ────────────────────────────────────────────────
|
|
1190
|
-
/** Centralized scrim style writer — atomic 3-property write across all setters. */
|
|
1191
1116
|
applyScrimStyles(opts) {
|
|
1192
1117
|
if (!this.screenComponent) return;
|
|
1193
1118
|
const s = this.screenComponent.style;
|
|
@@ -1206,10 +1131,10 @@ var ScrimController = class {
|
|
|
1206
1131
|
}
|
|
1207
1132
|
}
|
|
1208
1133
|
if (opts.interactive !== void 0) {
|
|
1134
|
+
this.scrimInteractive = opts.interactive;
|
|
1209
1135
|
s.pointerEvents = opts.interactive ? "auto" : "none";
|
|
1210
1136
|
}
|
|
1211
1137
|
}
|
|
1212
|
-
/** Pin the screen overlay to the area NOT occupied by the sheet. */
|
|
1213
1138
|
applyAboveSheetInset() {
|
|
1214
1139
|
if (!this.screenComponent) return;
|
|
1215
1140
|
const insetByMode = {
|
|
@@ -1221,7 +1146,7 @@ var ScrimController = class {
|
|
|
1221
1146
|
const s = this.screenComponent.style;
|
|
1222
1147
|
s.position = "fixed";
|
|
1223
1148
|
s.inset = insetByMode[this.mode];
|
|
1224
|
-
s.pointerEvents = "none";
|
|
1149
|
+
s.pointerEvents = this.scrimInteractive || this.scrimTapToCloseEnabled ? "auto" : "none";
|
|
1225
1150
|
}
|
|
1226
1151
|
};
|
|
1227
1152
|
|
|
@@ -1275,6 +1200,10 @@ var tween = ({
|
|
|
1275
1200
|
promise
|
|
1276
1201
|
};
|
|
1277
1202
|
};
|
|
1203
|
+
var prefersReducedMotion = () => {
|
|
1204
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
1205
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1206
|
+
};
|
|
1278
1207
|
|
|
1279
1208
|
// src/core/animation/spring.ts
|
|
1280
1209
|
var DEFAULT_SPRING = {
|
|
@@ -1305,14 +1234,20 @@ var runSpring = ({
|
|
|
1305
1234
|
if (cancelled) return;
|
|
1306
1235
|
const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
|
|
1307
1236
|
lastTime = now;
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1237
|
+
const steps = Math.max(1, Math.ceil(dt * 240));
|
|
1238
|
+
const subDt = dt / steps;
|
|
1239
|
+
let stepsLeft = steps;
|
|
1310
1240
|
while (stepsLeft-- > 0) {
|
|
1311
1241
|
const force = -cfg.stiffness * (x - to) - cfg.damping * v;
|
|
1312
1242
|
const accel = force / cfg.mass;
|
|
1313
1243
|
v += accel * subDt;
|
|
1314
1244
|
x += v * subDt;
|
|
1315
1245
|
}
|
|
1246
|
+
if (!Number.isFinite(x) || !Number.isFinite(v)) {
|
|
1247
|
+
onUpdate(to, 0);
|
|
1248
|
+
resolve();
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1316
1251
|
onUpdate(x, v);
|
|
1317
1252
|
const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
|
|
1318
1253
|
if (isAtRest) {
|
|
@@ -1376,13 +1311,9 @@ var AnimationRunner = class {
|
|
|
1376
1311
|
__publicField(this, "duration");
|
|
1377
1312
|
__publicField(this, "easing");
|
|
1378
1313
|
__publicField(this, "respectReducedMotion");
|
|
1379
|
-
/** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
|
|
1380
1314
|
__publicField(this, "reducedMotion", false);
|
|
1381
|
-
/** Disposes the matchMedia change listener — drained from destroy(). */
|
|
1382
1315
|
__publicField(this, "detachReducedMotion", null);
|
|
1383
|
-
/** Computed once — both inputs (option + API presence) stable for life. */
|
|
1384
1316
|
__publicField(this, "viewTransitionsAvailable");
|
|
1385
|
-
/** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
|
|
1386
1317
|
__publicField(this, "currentTween", null);
|
|
1387
1318
|
__publicField(this, "currentSpring", null);
|
|
1388
1319
|
this.element = deps.element;
|
|
@@ -1413,26 +1344,13 @@ var AnimationRunner = class {
|
|
|
1413
1344
|
}
|
|
1414
1345
|
}
|
|
1415
1346
|
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Live derived flag — `true` whenever a spring/tween handle is in flight.
|
|
1418
|
-
* Reading the canonical handles (instead of mirroring into a separate
|
|
1419
|
-
* boolean) eliminates the reduced-motion early-return latent bug where
|
|
1420
|
-
* the flag stayed true forever, and removes the cross-controller
|
|
1421
|
-
* setIsAnimating callback.
|
|
1422
|
-
*/
|
|
1423
1347
|
get isAnimating() {
|
|
1424
1348
|
return this.currentTween !== null || this.currentSpring !== null;
|
|
1425
1349
|
}
|
|
1426
|
-
/** Cancel any in-flight tween/spring without touching engine state. */
|
|
1427
1350
|
cancel() {
|
|
1428
1351
|
this.currentTween?.cancel();
|
|
1429
1352
|
this.currentSpring?.cancel();
|
|
1430
1353
|
}
|
|
1431
|
-
/**
|
|
1432
|
-
* Animate from current size to `target`. Resolves when the animation
|
|
1433
|
-
* completes; resolves early if cancelled mid-flight (caller checks an
|
|
1434
|
-
* abort signal post-await for cycle-invariant correctness).
|
|
1435
|
-
*/
|
|
1436
1354
|
async animateTo(target, velocityPxPerMs) {
|
|
1437
1355
|
if (target === this.getSize()) return;
|
|
1438
1356
|
this.currentTween?.cancel();
|
|
@@ -1473,7 +1391,6 @@ var AnimationRunner = class {
|
|
|
1473
1391
|
if (!this.isDragging()) this.element.style.willChange = "auto";
|
|
1474
1392
|
}
|
|
1475
1393
|
}
|
|
1476
|
-
/** Tear down the matchMedia listener. Engine calls from destroy(). */
|
|
1477
1394
|
destroy() {
|
|
1478
1395
|
this.detachReducedMotion?.();
|
|
1479
1396
|
this.detachReducedMotion = null;
|
|
@@ -1505,9 +1422,13 @@ var isVisible = (el) => {
|
|
|
1505
1422
|
var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
|
|
1506
1423
|
(el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
|
|
1507
1424
|
);
|
|
1425
|
+
var trapStack = [];
|
|
1508
1426
|
var installFocusTrap = (container, options = {}) => {
|
|
1509
1427
|
if (typeof document === "undefined") return () => {
|
|
1510
1428
|
};
|
|
1429
|
+
const token = {};
|
|
1430
|
+
trapStack.push(token);
|
|
1431
|
+
const isActive = () => trapStack[trapStack.length - 1] === token;
|
|
1511
1432
|
const previouslyFocused = document.activeElement;
|
|
1512
1433
|
const focusInitial = () => {
|
|
1513
1434
|
const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
|
|
@@ -1515,6 +1436,7 @@ var installFocusTrap = (container, options = {}) => {
|
|
|
1515
1436
|
};
|
|
1516
1437
|
focusInitial();
|
|
1517
1438
|
const handleKey = (e) => {
|
|
1439
|
+
if (!isActive()) return;
|
|
1518
1440
|
if (e.key === "Escape" && options.onEscape) {
|
|
1519
1441
|
options.onEscape();
|
|
1520
1442
|
return;
|
|
@@ -1537,6 +1459,7 @@ var installFocusTrap = (container, options = {}) => {
|
|
|
1537
1459
|
}
|
|
1538
1460
|
};
|
|
1539
1461
|
const handleFocusIn = (e) => {
|
|
1462
|
+
if (!isActive()) return;
|
|
1540
1463
|
const target = e.target;
|
|
1541
1464
|
if (target && !container.contains(target)) {
|
|
1542
1465
|
const list = focusables(container);
|
|
@@ -1549,6 +1472,8 @@ var installFocusTrap = (container, options = {}) => {
|
|
|
1549
1472
|
return () => {
|
|
1550
1473
|
if (released) return;
|
|
1551
1474
|
released = true;
|
|
1475
|
+
const idx = trapStack.indexOf(token);
|
|
1476
|
+
if (idx !== -1) trapStack.splice(idx, 1);
|
|
1552
1477
|
document.removeEventListener("keydown", handleKey, true);
|
|
1553
1478
|
document.removeEventListener("focusin", handleFocusIn, true);
|
|
1554
1479
|
if (previouslyFocused && document.contains(previouslyFocused)) {
|
|
@@ -1604,11 +1529,17 @@ function createInertSiblings(rootProvider) {
|
|
|
1604
1529
|
return {
|
|
1605
1530
|
apply() {
|
|
1606
1531
|
if (typeof document === "undefined") return;
|
|
1607
|
-
|
|
1608
|
-
|
|
1532
|
+
let top = rootProvider();
|
|
1533
|
+
while (top.parentElement && top.parentElement !== document.body) {
|
|
1534
|
+
top = top.parentElement;
|
|
1535
|
+
}
|
|
1536
|
+
const parent = top.parentElement ?? document.body;
|
|
1609
1537
|
for (const child of Array.from(parent.children)) {
|
|
1610
|
-
if (child ===
|
|
1538
|
+
if (child === top) continue;
|
|
1611
1539
|
if (child.hasAttribute("inert")) continue;
|
|
1540
|
+
if (child.classList.contains("bs-backdrop") || child.classList.contains("bs-screen") || child.classList.contains("bs-root")) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1612
1543
|
child.setAttribute("inert", "");
|
|
1613
1544
|
tracked.push(child);
|
|
1614
1545
|
}
|
|
@@ -1624,7 +1555,6 @@ function createInertSiblings(rootProvider) {
|
|
|
1624
1555
|
var LifecycleController = class {
|
|
1625
1556
|
constructor(deps, opts) {
|
|
1626
1557
|
__publicField(this, "element");
|
|
1627
|
-
__publicField(this, "closeFn");
|
|
1628
1558
|
__publicField(this, "focusTrapEnabled");
|
|
1629
1559
|
__publicField(this, "initialFocus");
|
|
1630
1560
|
__publicField(this, "closeOnEscape");
|
|
@@ -1633,16 +1563,11 @@ var LifecycleController = class {
|
|
|
1633
1563
|
__publicField(this, "inertSiblings");
|
|
1634
1564
|
__publicField(this, "shouldApplyInertSiblings");
|
|
1635
1565
|
__publicField(this, "returnFocus");
|
|
1636
|
-
/** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
|
|
1637
1566
|
__publicField(this, "releaseFocusTrap", null);
|
|
1638
1567
|
__publicField(this, "releaseScrollLock", null);
|
|
1639
|
-
/** True between install() and release(). Guards release()/destroy() from
|
|
1640
|
-
* re-running side effects (notably `returnFocus.focus()`) on already-
|
|
1641
|
-
* released or never-installed instances. */
|
|
1642
1568
|
__publicField(this, "installed", false);
|
|
1643
1569
|
__publicField(this, "destroyed", false);
|
|
1644
1570
|
this.element = deps.element;
|
|
1645
|
-
this.closeFn = deps.close;
|
|
1646
1571
|
this.focusTrapEnabled = opts.focusTrap ?? false;
|
|
1647
1572
|
this.initialFocus = opts.initialFocus;
|
|
1648
1573
|
this.closeOnEscape = opts.closeOnEscape ?? true;
|
|
@@ -1652,14 +1577,9 @@ var LifecycleController = class {
|
|
|
1652
1577
|
this.returnFocus = opts.returnFocus;
|
|
1653
1578
|
this.inertSiblings = createInertSiblings(() => this.element);
|
|
1654
1579
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
* doesn't double-install. Sets `installed=true` BEFORE the first side
|
|
1659
|
-
* effect so a partial throw (detached / shadow-DOM target) leaves the
|
|
1660
|
-
* caller's `release()` rollback path armed to undo whatever did succeed.
|
|
1661
|
-
* No-op when destroyed.
|
|
1662
|
-
*/
|
|
1580
|
+
get isInstalled() {
|
|
1581
|
+
return this.installed;
|
|
1582
|
+
}
|
|
1663
1583
|
install() {
|
|
1664
1584
|
if (this.destroyed) return;
|
|
1665
1585
|
this.installed = true;
|
|
@@ -1668,30 +1588,17 @@ var LifecycleController = class {
|
|
|
1668
1588
|
}
|
|
1669
1589
|
if (this.focusTrapEnabled && !this.releaseFocusTrap) {
|
|
1670
1590
|
this.releaseFocusTrap = installFocusTrap(this.element, {
|
|
1671
|
-
initialFocus: this.initialFocus
|
|
1672
|
-
onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
|
|
1591
|
+
initialFocus: this.initialFocus
|
|
1673
1592
|
});
|
|
1674
1593
|
}
|
|
1675
1594
|
if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
|
|
1676
1595
|
this.inertSiblings.apply();
|
|
1677
1596
|
}
|
|
1678
1597
|
}
|
|
1679
|
-
/**
|
|
1680
|
-
* Update the post-release focus target. Overlay re-binds this when the
|
|
1681
|
-
* consumer changes `returnFocus` after construction; the new value applies
|
|
1682
|
-
* on the next `release()`. No-op when destroyed.
|
|
1683
|
-
*/
|
|
1684
1598
|
setReturnFocus(target) {
|
|
1685
1599
|
if (this.destroyed) return;
|
|
1686
1600
|
this.returnFocus = target;
|
|
1687
1601
|
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Release all install-time teardowns. Short-circuits when not installed,
|
|
1690
|
-
* so `close() → destroy()` (each routing through here) only restores
|
|
1691
|
-
* focus once. The `installed` guard lets `inertSiblings.remove()` and
|
|
1692
|
-
* the focus-trap/scroll-lock teardown nullings stay simple — they were
|
|
1693
|
-
* already idempotent, but `returnFocus.focus()` was firing on every call.
|
|
1694
|
-
*/
|
|
1695
1602
|
release() {
|
|
1696
1603
|
if (!this.installed) return;
|
|
1697
1604
|
this.installed = false;
|
|
@@ -1705,12 +1612,6 @@ var LifecycleController = class {
|
|
|
1705
1612
|
target?.focus?.();
|
|
1706
1613
|
}
|
|
1707
1614
|
}
|
|
1708
|
-
/**
|
|
1709
|
-
* Final teardown — releases any still-installed listeners and seals the
|
|
1710
|
-
* controller. After destroy(), install() is a no-op and setReturnFocus()
|
|
1711
|
-
* is rejected, so post-destroy mutations can't queue up a stale focus
|
|
1712
|
-
* restore.
|
|
1713
|
-
*/
|
|
1714
1615
|
destroy() {
|
|
1715
1616
|
this.release();
|
|
1716
1617
|
this.destroyed = true;
|
|
@@ -1720,12 +1621,10 @@ var LifecycleController = class {
|
|
|
1720
1621
|
// src/core/gestures.ts
|
|
1721
1622
|
var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
|
|
1722
1623
|
var SampleRing = class {
|
|
1723
|
-
// current count, ≤ capacity
|
|
1724
1624
|
constructor(capacity) {
|
|
1725
1625
|
__publicField(this, "slots");
|
|
1726
1626
|
__publicField(this, "capacity");
|
|
1727
1627
|
__publicField(this, "head", 0);
|
|
1728
|
-
// next write index
|
|
1729
1628
|
__publicField(this, "size", 0);
|
|
1730
1629
|
this.capacity = capacity;
|
|
1731
1630
|
this.slots = new Array(capacity);
|
|
@@ -1738,12 +1637,6 @@ var SampleRing = class {
|
|
|
1738
1637
|
this.head = (this.head + 1) % this.capacity;
|
|
1739
1638
|
if (this.size < this.capacity) this.size++;
|
|
1740
1639
|
}
|
|
1741
|
-
/**
|
|
1742
|
-
* Drop oldest entries whose timestamp is below `cutoff`, but never shrink
|
|
1743
|
-
* below `minKeep`. Mirrors the original
|
|
1744
|
-
* `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
|
|
1745
|
-
* but without copying — just advances the logical tail.
|
|
1746
|
-
*/
|
|
1747
1640
|
pruneBefore(cutoff, minKeep) {
|
|
1748
1641
|
while (this.size > minKeep) {
|
|
1749
1642
|
const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
|
|
@@ -1849,10 +1742,11 @@ var installGestures = (handle, mode, callbacks) => {
|
|
|
1849
1742
|
};
|
|
1850
1743
|
|
|
1851
1744
|
// src/core/primitives/rubber-band.ts
|
|
1852
|
-
var RUBBER_BAND_FACTOR = 0.55;
|
|
1853
1745
|
function rubberBand(overshoot, maxAxisSize) {
|
|
1854
1746
|
const cap = Math.min(maxAxisSize * 0.15, 80);
|
|
1855
|
-
|
|
1747
|
+
if (cap <= 0) return 0;
|
|
1748
|
+
const abs = Math.abs(overshoot);
|
|
1749
|
+
return Math.sign(overshoot) * (cap * abs / (abs + cap));
|
|
1856
1750
|
}
|
|
1857
1751
|
|
|
1858
1752
|
// src/core/features/soft-keyboard.ts
|
|
@@ -1879,28 +1773,14 @@ var GestureController = class {
|
|
|
1879
1773
|
__publicField(this, "detach", null);
|
|
1880
1774
|
__publicField(this, "isDragging_", false);
|
|
1881
1775
|
__publicField(this, "dragStartSize", 0);
|
|
1882
|
-
// Only "touch" gestures are eligible for soft-keyboard dismissal —
|
|
1883
|
-
// mouse/trackpad users don't have a keyboard to dismiss.
|
|
1884
1776
|
__publicField(this, "currentPointerKind", "touch");
|
|
1885
|
-
// One blur per drag — flipping focus every move event races the keyboard's
|
|
1886
|
-
// own animations on iOS and makes the page jitter.
|
|
1887
1777
|
__publicField(this, "keyboardDismissed", false);
|
|
1888
|
-
// Reused payload object for the "drag" event — mutated each onMove instead
|
|
1889
|
-
// of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
|
|
1890
|
-
// would otherwise churn tens of KB through the GC. Mirrors browser
|
|
1891
|
-
// PointerEvent semantics: consumers must NOT retain the reference across
|
|
1892
|
-
// frames (clone if needed). See SheetEventMap.drag JSDoc.
|
|
1893
1778
|
__publicField(this, "dragPayload", { size: 0, delta: 0 });
|
|
1894
1779
|
this.deps = deps;
|
|
1895
1780
|
}
|
|
1896
|
-
/** True between `dragstart` and `dragend`/`onCancel`. */
|
|
1897
1781
|
get isDragging() {
|
|
1898
1782
|
return this.isDragging_;
|
|
1899
1783
|
}
|
|
1900
|
-
/**
|
|
1901
|
-
* Bind pointer listeners on the handle and return a teardown. Engine pushes
|
|
1902
|
-
* the teardown into its TeardownStack so destroy() drains it once.
|
|
1903
|
-
*/
|
|
1904
1784
|
install() {
|
|
1905
1785
|
if (this.detach) return this.detach;
|
|
1906
1786
|
this.detach = installGestures(this.deps.handle, this.deps.mode, {
|
|
@@ -1951,17 +1831,16 @@ var GestureController = class {
|
|
|
1951
1831
|
this.isDragging_ = false;
|
|
1952
1832
|
this.keyboardDismissed = false;
|
|
1953
1833
|
this.deps.getRoot()?.removeAttribute("data-dragging");
|
|
1834
|
+
this.deps.emit("dragend", {
|
|
1835
|
+
size: this.deps.getDragContext().size,
|
|
1836
|
+
velocity: 0
|
|
1837
|
+
});
|
|
1954
1838
|
const restoreTo = this.dragStartSize;
|
|
1955
1839
|
void this.deps.animateTo(restoreTo, 0);
|
|
1956
1840
|
}
|
|
1957
1841
|
});
|
|
1958
1842
|
return this.detach;
|
|
1959
1843
|
}
|
|
1960
|
-
/**
|
|
1961
|
-
* Force-clear drag visual state. Engine calls from destroy() when destroyed
|
|
1962
|
-
* mid-drag — gesture detach won't synthesize an onCancel, so we have to
|
|
1963
|
-
* roll back the data-attribute + will-change ourselves.
|
|
1964
|
-
*/
|
|
1965
1844
|
forceClearDragState() {
|
|
1966
1845
|
if (!this.isDragging_) return;
|
|
1967
1846
|
this.isDragging_ = false;
|
|
@@ -1980,11 +1859,6 @@ var AriaSliderWriter = class {
|
|
|
1980
1859
|
mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
|
|
1981
1860
|
);
|
|
1982
1861
|
}
|
|
1983
|
-
/**
|
|
1984
|
-
* Update slider position. `allowedIds` length determines `aria-valuemax`;
|
|
1985
|
-
* `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
|
|
1986
|
-
* Empty list → strip every dynamic attr so AT skips the handle entirely.
|
|
1987
|
-
*/
|
|
1988
1862
|
setValue(allowedIds, activeId) {
|
|
1989
1863
|
if (allowedIds.length === 0) {
|
|
1990
1864
|
this.handle.removeAttribute("aria-valuemin");
|
|
@@ -2034,6 +1908,287 @@ function createScrollCache(deps) {
|
|
|
2034
1908
|
};
|
|
2035
1909
|
}
|
|
2036
1910
|
|
|
1911
|
+
// src/core/primitives/anchor-animations.ts
|
|
1912
|
+
var DEFAULT_DURATION3 = 200;
|
|
1913
|
+
var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
1914
|
+
var PRESET_FRAMES = {
|
|
1915
|
+
fade: {
|
|
1916
|
+
enter: [{ opacity: 0 }, { opacity: 1 }],
|
|
1917
|
+
exit: [{ opacity: 1 }, { opacity: 0 }]
|
|
1918
|
+
},
|
|
1919
|
+
scale: {
|
|
1920
|
+
enter: [
|
|
1921
|
+
{ opacity: 0, transform: "scale(0.85)" },
|
|
1922
|
+
{ opacity: 1, transform: "scale(1)" }
|
|
1923
|
+
],
|
|
1924
|
+
exit: [
|
|
1925
|
+
{ opacity: 1, transform: "scale(1)" },
|
|
1926
|
+
{ opacity: 0, transform: "scale(0.85)" }
|
|
1927
|
+
]
|
|
1928
|
+
},
|
|
1929
|
+
slide: {
|
|
1930
|
+
enter: [
|
|
1931
|
+
{ opacity: 0, transform: "translateY(10px)" },
|
|
1932
|
+
{ opacity: 1, transform: "translateY(0)" }
|
|
1933
|
+
],
|
|
1934
|
+
exit: [
|
|
1935
|
+
{ opacity: 1, transform: "translateY(0)" },
|
|
1936
|
+
{ opacity: 0, transform: "translateY(10px)" }
|
|
1937
|
+
]
|
|
1938
|
+
},
|
|
1939
|
+
pop: {
|
|
1940
|
+
enter: [
|
|
1941
|
+
{ opacity: 0, transform: "scale(0.6)", offset: 0 },
|
|
1942
|
+
{ opacity: 1, transform: "scale(1.06)", offset: 0.7 },
|
|
1943
|
+
{ opacity: 1, transform: "scale(1)", offset: 1 }
|
|
1944
|
+
],
|
|
1945
|
+
exit: [
|
|
1946
|
+
{ opacity: 1, transform: "scale(1)" },
|
|
1947
|
+
{ opacity: 0, transform: "scale(0.6)" }
|
|
1948
|
+
]
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
var INSTANT = {
|
|
1952
|
+
finished: Promise.resolve(),
|
|
1953
|
+
cancel: () => {
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
function resolveTransitionFrames(spec, phase) {
|
|
1957
|
+
const fallback = { frames: null, duration: 0, easing: DEFAULT_EASING };
|
|
1958
|
+
if (spec === "none") return fallback;
|
|
1959
|
+
if (spec === void 0 || typeof spec === "string") {
|
|
1960
|
+
const preset2 = PRESET_FRAMES[spec ?? "fade"];
|
|
1961
|
+
return {
|
|
1962
|
+
frames: preset2[phase],
|
|
1963
|
+
duration: DEFAULT_DURATION3,
|
|
1964
|
+
easing: DEFAULT_EASING
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
const custom = phase === "enter" ? spec.enter : spec.exit;
|
|
1968
|
+
if (custom) {
|
|
1969
|
+
return {
|
|
1970
|
+
frames: custom,
|
|
1971
|
+
duration: spec.duration ?? DEFAULT_DURATION3,
|
|
1972
|
+
easing: spec.easing ?? DEFAULT_EASING
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (spec.preset === "none") return fallback;
|
|
1976
|
+
const preset = PRESET_FRAMES[spec.preset ?? "fade"];
|
|
1977
|
+
return {
|
|
1978
|
+
frames: preset[phase],
|
|
1979
|
+
duration: spec.duration ?? DEFAULT_DURATION3,
|
|
1980
|
+
easing: spec.easing ?? DEFAULT_EASING
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function runAnchorTransition(el, spec, phase) {
|
|
1984
|
+
const respectReduced = typeof spec === "object" ? spec.respectReducedMotion !== false : true;
|
|
1985
|
+
if (respectReduced && prefersReducedMotion()) return INSTANT;
|
|
1986
|
+
const { frames, duration, easing } = resolveTransitionFrames(spec, phase);
|
|
1987
|
+
if (!frames || duration <= 0) return INSTANT;
|
|
1988
|
+
if (typeof el.animate !== "function") return INSTANT;
|
|
1989
|
+
const animation = el.animate(frames, {
|
|
1990
|
+
duration,
|
|
1991
|
+
easing,
|
|
1992
|
+
fill: "both"
|
|
1993
|
+
});
|
|
1994
|
+
const finished = animation.finished.then(() => void 0).catch(() => void 0);
|
|
1995
|
+
return {
|
|
1996
|
+
finished,
|
|
1997
|
+
cancel: () => {
|
|
1998
|
+
try {
|
|
1999
|
+
animation.cancel();
|
|
2000
|
+
} catch {
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// src/core/features/sheet-anchors.ts
|
|
2008
|
+
function attachAnchor(deps, opts) {
|
|
2009
|
+
const doc = deps.host.ownerDocument;
|
|
2010
|
+
const wrapper = doc.createElement("div");
|
|
2011
|
+
wrapper.className = "bs-anchor";
|
|
2012
|
+
const interactive = opts.interactive ?? true;
|
|
2013
|
+
const position = opts.position ?? "sheet-top-right";
|
|
2014
|
+
const isDock = position === "dock-bottom" || position === "dock-top";
|
|
2015
|
+
const ws = wrapper.style;
|
|
2016
|
+
ws.position = "fixed";
|
|
2017
|
+
if (isDock) {
|
|
2018
|
+
ws.left = "0";
|
|
2019
|
+
ws.right = "0";
|
|
2020
|
+
if (position === "dock-bottom") {
|
|
2021
|
+
ws.bottom = "0";
|
|
2022
|
+
} else {
|
|
2023
|
+
ws.top = "0";
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
2026
|
+
applyOverlayPosition(ws, deps.mode, position, opts.inset ?? "16px");
|
|
2027
|
+
}
|
|
2028
|
+
if (opts.fadeRange) {
|
|
2029
|
+
const [r0, r1] = opts.fadeRange;
|
|
2030
|
+
const span = Math.max(r1 - r0, 1e-4);
|
|
2031
|
+
ws.opacity = `clamp(0, calc((var(--bs-progress, 0) - ${r0}) / ${span}), 1)`;
|
|
2032
|
+
}
|
|
2033
|
+
wrapper.appendChild(opts.element);
|
|
2034
|
+
deps.host.appendChild(wrapper);
|
|
2035
|
+
let visible = null;
|
|
2036
|
+
let inFlight = null;
|
|
2037
|
+
const shouldShow = (state) => {
|
|
2038
|
+
if (typeof opts.showOn === "function") return opts.showOn(state);
|
|
2039
|
+
if (Array.isArray(opts.showOn)) return opts.showOn.includes(state.activeId);
|
|
2040
|
+
return isDock || state.size > 0;
|
|
2041
|
+
};
|
|
2042
|
+
const applyVisibility = (next, animate) => {
|
|
2043
|
+
if (next === visible) return;
|
|
2044
|
+
visible = next;
|
|
2045
|
+
inFlight?.cancel();
|
|
2046
|
+
inFlight = null;
|
|
2047
|
+
wrapper.style.pointerEvents = next && interactive ? "auto" : "none";
|
|
2048
|
+
if (next) {
|
|
2049
|
+
wrapper.style.visibility = "";
|
|
2050
|
+
if (animate) {
|
|
2051
|
+
inFlight = runAnchorTransition(opts.element, opts.animation, "enter");
|
|
2052
|
+
}
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
if (!animate) {
|
|
2056
|
+
wrapper.style.visibility = "hidden";
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const handle = runAnchorTransition(opts.element, opts.animation, "exit");
|
|
2060
|
+
inFlight = handle;
|
|
2061
|
+
void handle.finished.then(() => {
|
|
2062
|
+
if (inFlight === handle && visible === false) {
|
|
2063
|
+
wrapper.style.visibility = "hidden";
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
};
|
|
2067
|
+
const evaluate = (animate) => {
|
|
2068
|
+
if (deps.isDestroyed()) return;
|
|
2069
|
+
applyVisibility(shouldShow(deps.getState()), animate);
|
|
2070
|
+
};
|
|
2071
|
+
const offs = [
|
|
2072
|
+
deps.on("snap", () => evaluate(true)),
|
|
2073
|
+
deps.on("open", () => evaluate(true)),
|
|
2074
|
+
deps.on("close", () => evaluate(true))
|
|
2075
|
+
];
|
|
2076
|
+
evaluate(false);
|
|
2077
|
+
return {
|
|
2078
|
+
wrapper,
|
|
2079
|
+
detach: () => {
|
|
2080
|
+
offs.forEach((off) => off());
|
|
2081
|
+
inFlight?.cancel();
|
|
2082
|
+
inFlight = null;
|
|
2083
|
+
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
|
|
2084
|
+
},
|
|
2085
|
+
syncZ: (z) => {
|
|
2086
|
+
wrapper.style.zIndex = String(z);
|
|
2087
|
+
}
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// src/core/features/scrim-stages.ts
|
|
2092
|
+
var matchesId = (def, activeId) => {
|
|
2093
|
+
if (def.for === void 0) return false;
|
|
2094
|
+
return Array.isArray(def.for) ? def.for.includes(activeId) : def.for === activeId;
|
|
2095
|
+
};
|
|
2096
|
+
var matchesRange = (def, progress) => {
|
|
2097
|
+
if (!def.forRange) return false;
|
|
2098
|
+
const [from, to] = def.forRange;
|
|
2099
|
+
return progress >= from && progress <= to;
|
|
2100
|
+
};
|
|
2101
|
+
function installScrimStages(deps, opts) {
|
|
2102
|
+
const doc = deps.host.ownerDocument;
|
|
2103
|
+
const entries = opts.stages.map((def) => {
|
|
2104
|
+
const wrapper = doc.createElement("div");
|
|
2105
|
+
wrapper.className = "bs-scrim-stage";
|
|
2106
|
+
const ws = wrapper.style;
|
|
2107
|
+
ws.position = "absolute";
|
|
2108
|
+
applyOverlayPosition(
|
|
2109
|
+
ws,
|
|
2110
|
+
deps.mode,
|
|
2111
|
+
def.position ?? opts.position ?? "center",
|
|
2112
|
+
def.inset ?? opts.inset ?? "16px"
|
|
2113
|
+
);
|
|
2114
|
+
ws.visibility = "hidden";
|
|
2115
|
+
ws.pointerEvents = "none";
|
|
2116
|
+
wrapper.appendChild(def.element);
|
|
2117
|
+
deps.host.appendChild(wrapper);
|
|
2118
|
+
return { def, wrapper, inFlight: null };
|
|
2119
|
+
});
|
|
2120
|
+
let active = null;
|
|
2121
|
+
const resolveActive = () => {
|
|
2122
|
+
const state = deps.getState();
|
|
2123
|
+
if (state.size === 0) {
|
|
2124
|
+
return entries.find((e) => matchesId(e.def, state.activeId)) ?? null;
|
|
2125
|
+
}
|
|
2126
|
+
return entries.find((e) => matchesId(e.def, state.activeId)) ?? entries.find((e) => matchesRange(e.def, state.progress)) ?? null;
|
|
2127
|
+
};
|
|
2128
|
+
const hide = (entry, animate) => {
|
|
2129
|
+
entry.inFlight?.cancel();
|
|
2130
|
+
entry.wrapper.style.pointerEvents = "none";
|
|
2131
|
+
if (!animate) {
|
|
2132
|
+
entry.inFlight = null;
|
|
2133
|
+
entry.wrapper.style.visibility = "hidden";
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const handle = runAnchorTransition(
|
|
2137
|
+
entry.def.element,
|
|
2138
|
+
entry.def.animation ?? opts.animation,
|
|
2139
|
+
"exit"
|
|
2140
|
+
);
|
|
2141
|
+
entry.inFlight = handle;
|
|
2142
|
+
void handle.finished.then(() => {
|
|
2143
|
+
if (entry.inFlight === handle && active !== entry) {
|
|
2144
|
+
entry.wrapper.style.visibility = "hidden";
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
};
|
|
2148
|
+
const show = (entry, animate) => {
|
|
2149
|
+
entry.inFlight?.cancel();
|
|
2150
|
+
entry.inFlight = null;
|
|
2151
|
+
entry.wrapper.style.visibility = "";
|
|
2152
|
+
entry.wrapper.style.pointerEvents = entry.def.interactive ?? opts.interactive ?? false ? "auto" : "none";
|
|
2153
|
+
if (animate) {
|
|
2154
|
+
entry.inFlight = runAnchorTransition(
|
|
2155
|
+
entry.def.element,
|
|
2156
|
+
entry.def.animation ?? opts.animation,
|
|
2157
|
+
"enter"
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
const update = (animate) => {
|
|
2162
|
+
if (deps.isDestroyed()) return;
|
|
2163
|
+
const next = resolveActive();
|
|
2164
|
+
if (next === active) return;
|
|
2165
|
+
const prev = active;
|
|
2166
|
+
active = next;
|
|
2167
|
+
if (prev) hide(prev, animate);
|
|
2168
|
+
if (next) show(next, animate);
|
|
2169
|
+
};
|
|
2170
|
+
const offs = [
|
|
2171
|
+
deps.on("snap", () => update(true)),
|
|
2172
|
+
deps.on("open", () => update(true)),
|
|
2173
|
+
deps.on("close", () => update(true))
|
|
2174
|
+
];
|
|
2175
|
+
if (entries.some((e) => e.def.forRange)) {
|
|
2176
|
+
offs.push(deps.on("progress", () => update(true)));
|
|
2177
|
+
}
|
|
2178
|
+
update(false);
|
|
2179
|
+
return () => {
|
|
2180
|
+
offs.forEach((off) => off());
|
|
2181
|
+
for (const entry of entries) {
|
|
2182
|
+
entry.inFlight?.cancel();
|
|
2183
|
+
entry.inFlight = null;
|
|
2184
|
+
if (entry.wrapper.parentNode) {
|
|
2185
|
+
entry.wrapper.parentNode.removeChild(entry.wrapper);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
active = null;
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2037
2192
|
// src/core/primitives/engine-options.ts
|
|
2038
2193
|
var DEFAULT_FLICK_VELOCITY = 0.65;
|
|
2039
2194
|
var DEFAULT_DRAG_THRESHOLD = 18;
|
|
@@ -2058,9 +2213,6 @@ function resolveEngineOptions(opts, extras) {
|
|
|
2058
2213
|
scrimMode: opts.scrimMode,
|
|
2059
2214
|
scrimColor: opts.scrimColor,
|
|
2060
2215
|
scrimBlur: opts.scrimBlur,
|
|
2061
|
-
// Auto-promote pointer-events when tap-to-close is on so the click
|
|
2062
|
-
// listener actually receives events. Caller can still force `false`
|
|
2063
|
-
// explicitly to opt out — `??` only kicks in for `undefined`.
|
|
2064
2216
|
scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
|
|
2065
2217
|
scrimTapToClose: opts.scrimTapToClose,
|
|
2066
2218
|
scrimPreset: opts.scrimPreset,
|
|
@@ -2081,10 +2233,6 @@ function resolveEngineOptions(opts, extras) {
|
|
|
2081
2233
|
closeOnEscape: opts.closeOnEscape,
|
|
2082
2234
|
lockBodyScroll: opts.lockBodyScroll,
|
|
2083
2235
|
inertSiblings: opts.inertSiblings,
|
|
2084
|
-
// Folded in from `extras` so the lifecycle bag is fully assembled at
|
|
2085
|
-
// the call site — engine no longer needs a spread+override at
|
|
2086
|
-
// controller construction. When `extras` is omitted, the field stays
|
|
2087
|
-
// undefined and the controller uses its own default predicate.
|
|
2088
2236
|
shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
|
|
2089
2237
|
}
|
|
2090
2238
|
};
|
|
@@ -2119,6 +2267,7 @@ var BottomSheetEngine = class {
|
|
|
2119
2267
|
__publicField(this, "scrollContainer");
|
|
2120
2268
|
__publicField(this, "backdrop");
|
|
2121
2269
|
__publicField(this, "screenComponent");
|
|
2270
|
+
__publicField(this, "scrimParent", null);
|
|
2122
2271
|
__publicField(this, "mode");
|
|
2123
2272
|
__publicField(this, "flickVelocity");
|
|
2124
2273
|
__publicField(this, "dragThreshold");
|
|
@@ -2129,40 +2278,22 @@ var BottomSheetEngine = class {
|
|
|
2129
2278
|
__publicField(this, "lifecycle");
|
|
2130
2279
|
__publicField(this, "closeOnBack");
|
|
2131
2280
|
__publicField(this, "routedTo");
|
|
2132
|
-
// Engine retains `snapPointsRaw` for the "find a closed-id" query in
|
|
2133
|
-
// close() (raw input, not resolved geometry). Resolver owns geometry.
|
|
2134
2281
|
__publicField(this, "snapPointsRaw");
|
|
2135
2282
|
__publicField(this, "snaps");
|
|
2136
2283
|
__publicField(this, "activeId");
|
|
2137
2284
|
__publicField(this, "size", 0);
|
|
2138
|
-
// Constructed in attach(); a plugin's install() could read `state` before
|
|
2139
|
-
// attach completes if the constructor throws partway through. State getter
|
|
2140
|
-
// null-guards via `this.gesture?.isDragging ?? false`.
|
|
2141
2285
|
__publicField(this, "gesture");
|
|
2142
2286
|
__publicField(this, "rootEl", null);
|
|
2143
2287
|
__publicField(this, "destroyed", false);
|
|
2144
|
-
// In-flight ViewTransition handle. A second snapTo while a VT is in flight
|
|
2145
|
-
// would otherwise let Chromium reject the second's promise and flicker —
|
|
2146
|
-
// newCycle() calls skipTransition() to abort cleanly.
|
|
2147
2288
|
__publicField(this, "currentViewTransition", null);
|
|
2148
|
-
// Each cycle captures the signal at entry; when a newer cycle aborts the
|
|
2149
|
-
// controller, the captured signal flips to `aborted=true` and post-await
|
|
2150
|
-
// guards short-circuit (prevents stale snap/open/close emits).
|
|
2151
2289
|
__publicField(this, "currentAbort", new AbortController());
|
|
2152
|
-
// Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
|
|
2153
|
-
// visually distinguishable; gating CSSOM writes drops 30-50% of writes
|
|
2154
|
-
// during the spring's settle tail.
|
|
2155
2290
|
__publicField(this, "sizeWriteSentinel", new WriteSentinel());
|
|
2156
2291
|
__publicField(this, "progressWriteSentinel", new WriteSentinel());
|
|
2157
2292
|
__publicField(this, "isTopSheet", true);
|
|
2158
|
-
// Pooled payload — identity documented in SheetEventMap.progress JSDoc;
|
|
2159
|
-
// consumers must not retain across frames.
|
|
2160
2293
|
__publicField(this, "progressPayload", {
|
|
2161
2294
|
value: 0,
|
|
2162
2295
|
size: 0
|
|
2163
2296
|
});
|
|
2164
|
-
// Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
|
|
2165
|
-
// Inner `range` object identity is also stable (mutated in place).
|
|
2166
2297
|
__publicField(this, "dragContextBuf", {
|
|
2167
2298
|
size: 0,
|
|
2168
2299
|
maxAxisSize: 0,
|
|
@@ -2173,13 +2304,13 @@ var BottomSheetEngine = class {
|
|
|
2173
2304
|
__publicField(this, "persistKey");
|
|
2174
2305
|
__publicField(this, "linkedSheets", []);
|
|
2175
2306
|
__publicField(this, "scrollCache");
|
|
2176
|
-
// Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
|
|
2177
|
-
// tear down first before the infrastructure they depend on. Toggle-semantic
|
|
2178
|
-
// listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
|
|
2179
|
-
// fields on their controllers.
|
|
2180
2307
|
__publicField(this, "teardowns", new TeardownStack());
|
|
2181
|
-
|
|
2182
|
-
|
|
2308
|
+
__publicField(this, "anchors", []);
|
|
2309
|
+
__publicField(this, "anchorHost", null);
|
|
2310
|
+
__publicField(this, "stackZ", 100);
|
|
2311
|
+
__publicField(this, "detachScrimStages", null);
|
|
2312
|
+
__publicField(this, "stackEffectEnabled", false);
|
|
2313
|
+
__publicField(this, "stackEffectPrimed", false);
|
|
2183
2314
|
__publicField(this, "transformTemplate");
|
|
2184
2315
|
const resolved = resolveEngineOptions(opts, {
|
|
2185
2316
|
shouldApplyInertSiblings: () => {
|
|
@@ -2194,7 +2325,8 @@ var BottomSheetEngine = class {
|
|
|
2194
2325
|
this.handle = opts.handle ?? opts.element;
|
|
2195
2326
|
this.scrollContainer = opts.scrollContainer;
|
|
2196
2327
|
this.backdrop = opts.backdrop;
|
|
2197
|
-
this.screenComponent = opts.screenComponent;
|
|
2328
|
+
this.screenComponent = opts.scrim ?? opts.screenComponent;
|
|
2329
|
+
this.scrimParent = this.screenComponent?.parentElement ?? null;
|
|
2198
2330
|
this.mode = resolved.mode;
|
|
2199
2331
|
this.transformTemplate = buildTransformTemplate(this.mode);
|
|
2200
2332
|
this.aria = new AriaSliderWriter(this.handle, this.mode);
|
|
@@ -2212,6 +2344,9 @@ var BottomSheetEngine = class {
|
|
|
2212
2344
|
getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
|
|
2213
2345
|
snapTo: (id) => {
|
|
2214
2346
|
void this.snapTo(id);
|
|
2347
|
+
},
|
|
2348
|
+
close: () => {
|
|
2349
|
+
void this.close();
|
|
2215
2350
|
}
|
|
2216
2351
|
},
|
|
2217
2352
|
resolved.scrim
|
|
@@ -2222,8 +2357,6 @@ var BottomSheetEngine = class {
|
|
|
2222
2357
|
getRootEl: () => this.rootEl,
|
|
2223
2358
|
applySize: (size) => this.applySize(size),
|
|
2224
2359
|
getSize: () => this.size,
|
|
2225
|
-
// Optional chain protects synthetic test paths that drive animateTo
|
|
2226
|
-
// before attach() (gesture is constructed there).
|
|
2227
2360
|
isDragging: () => this.gesture?.isDragging ?? false
|
|
2228
2361
|
},
|
|
2229
2362
|
resolved.animation
|
|
@@ -2237,6 +2370,7 @@ var BottomSheetEngine = class {
|
|
|
2237
2370
|
);
|
|
2238
2371
|
this.closeOnBack = resolved.closeOnBack;
|
|
2239
2372
|
this.routedTo = opts.routedTo;
|
|
2373
|
+
this.stackEffectEnabled = opts.stackEffect ?? false;
|
|
2240
2374
|
this.snapPointsRaw = opts.snapPoints;
|
|
2241
2375
|
this.persistKey = opts.persistKey;
|
|
2242
2376
|
if (opts.linkedSheets) {
|
|
@@ -2249,7 +2383,7 @@ var BottomSheetEngine = class {
|
|
|
2249
2383
|
this.snapPointsRaw,
|
|
2250
2384
|
resolved.initialAllowed,
|
|
2251
2385
|
this.mode,
|
|
2252
|
-
() => this.handle.offsetHeight,
|
|
2386
|
+
() => layoutAxis(this.mode) === "width" ? this.handle.offsetWidth : this.handle.offsetHeight,
|
|
2253
2387
|
(maxAxisSize) => {
|
|
2254
2388
|
this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
|
|
2255
2389
|
}
|
|
@@ -2328,21 +2462,6 @@ var BottomSheetEngine = class {
|
|
|
2328
2462
|
on(event, fn) {
|
|
2329
2463
|
return this.bus.on(event, fn);
|
|
2330
2464
|
}
|
|
2331
|
-
/**
|
|
2332
|
-
* Register a plugin. `install` runs synchronously; if it returns a function,
|
|
2333
|
-
* it's captured for teardown during `destroy()`. No-op if already destroyed
|
|
2334
|
-
* — installing into a torn-down engine would leak listeners.
|
|
2335
|
-
*
|
|
2336
|
-
* Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
|
|
2337
|
-
* so a buggy plugin can't take the engine down with it: the engine stays
|
|
2338
|
-
* usable, sibling plugins still install, and the error surfaces in dev tools
|
|
2339
|
-
* on the next tick instead of being swallowed. Mirrors the per-fn isolation
|
|
2340
|
-
* model used for teardowns.
|
|
2341
|
-
*
|
|
2342
|
-
* Plugins that accept the second `scope` argument get transactional cleanup:
|
|
2343
|
-
* push partial-install cleanups via `scope.add(fn)` and the engine drains
|
|
2344
|
-
* them on throw, OR merges them into destroy-time teardowns on success.
|
|
2345
|
-
*/
|
|
2346
2465
|
use(plugin) {
|
|
2347
2466
|
if (this.destroyed) {
|
|
2348
2467
|
console.warn(
|
|
@@ -2352,9 +2471,6 @@ var BottomSheetEngine = class {
|
|
|
2352
2471
|
}
|
|
2353
2472
|
const scoped = [];
|
|
2354
2473
|
const scope = {
|
|
2355
|
-
// typeof check — not just truthiness — so a JS-only plugin pushing a
|
|
2356
|
-
// bogus value is silently rejected instead of queueing a confusing
|
|
2357
|
-
// TypeError on destroy() drain.
|
|
2358
2474
|
add: (fn) => {
|
|
2359
2475
|
if (typeof fn === "function") scoped.push(fn);
|
|
2360
2476
|
}
|
|
@@ -2383,36 +2499,13 @@ var BottomSheetEngine = class {
|
|
|
2383
2499
|
}
|
|
2384
2500
|
return this;
|
|
2385
2501
|
}
|
|
2386
|
-
/**
|
|
2387
|
-
* Replace the linked-sheets list at runtime. Use when peer engines aren't
|
|
2388
|
-
* available at construction time (mounted later in the tree). The wiring
|
|
2389
|
-
* to the `open` event is independent of this list — it reads `linkedSheets`
|
|
2390
|
-
* lazily on emit, so this setter is a simple field swap.
|
|
2391
|
-
*/
|
|
2392
2502
|
setLinkedSheets(sheets) {
|
|
2393
2503
|
if (this.destroyed) return;
|
|
2394
2504
|
this.linkedSheets = sheets;
|
|
2395
2505
|
}
|
|
2396
|
-
/** Read-only view of the current allow-list, for cross-sheet target negotiation. */
|
|
2397
2506
|
getAllowedIds() {
|
|
2398
2507
|
return this.snaps.getAllowedIds().slice();
|
|
2399
2508
|
}
|
|
2400
|
-
/**
|
|
2401
|
-
* Animate to a snap point by id. Resolves when the animation completes.
|
|
2402
|
-
*
|
|
2403
|
-
* Two call shapes:
|
|
2404
|
-
* ```ts
|
|
2405
|
-
* engine.snapTo("full"); // default velocity
|
|
2406
|
-
* engine.snapTo("full", 0.5); // px/ms initial velocity
|
|
2407
|
-
* engine.snapTo("full", { velocity: 0.5, signal }); // abortable
|
|
2408
|
-
* ```
|
|
2409
|
-
*
|
|
2410
|
-
* When `opts.signal` is provided, calling `controller.abort()` cancels the
|
|
2411
|
-
* in-flight tween/spring and the returned promise resolves without firing
|
|
2412
|
-
* `snap`. The captured `AbortSignal` is checked after each await so external
|
|
2413
|
-
* aborts ride the same cycle invariant as internal ones (resize, destroy,
|
|
2414
|
-
* concurrent snapTo).
|
|
2415
|
-
*/
|
|
2416
2509
|
async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
|
|
2417
2510
|
if (this.destroyed) return;
|
|
2418
2511
|
const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
|
|
@@ -2471,16 +2564,6 @@ var BottomSheetEngine = class {
|
|
|
2471
2564
|
this.handleClose();
|
|
2472
2565
|
}
|
|
2473
2566
|
}
|
|
2474
|
-
/**
|
|
2475
|
-
* Drive the sheet to an arbitrary px size, bypassing snap matching.
|
|
2476
|
-
* Useful for preview scrubs, animation chaining, or programmatic gestures.
|
|
2477
|
-
* Animates with the configured spring/tween. Resolves when the animation
|
|
2478
|
-
* completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
|
|
2479
|
-
*
|
|
2480
|
-
* Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
|
|
2481
|
-
* nor `snap` (the sheet isn't settling to a known snap). `progress` events
|
|
2482
|
-
* still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
|
|
2483
|
-
*/
|
|
2484
2567
|
async dragTo(targetSize, velocityOrOpts = 0) {
|
|
2485
2568
|
if (this.destroyed) return;
|
|
2486
2569
|
const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
|
|
@@ -2510,28 +2593,13 @@ var BottomSheetEngine = class {
|
|
|
2510
2593
|
}
|
|
2511
2594
|
setAllowed(ids, snap) {
|
|
2512
2595
|
if (this.destroyed) return;
|
|
2513
|
-
const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
|
|
2514
|
-
if (!willSnap) {
|
|
2515
|
-
this.animation.cancel();
|
|
2516
|
-
this.newCycle();
|
|
2517
|
-
}
|
|
2518
2596
|
this.snaps.setAllowedIds(ids);
|
|
2519
2597
|
this.updateAriaSlider();
|
|
2520
2598
|
if (snap && ids.includes(snap)) {
|
|
2521
|
-
void this.snapTo(
|
|
2522
|
-
snap,
|
|
2523
|
-
0,
|
|
2524
|
-
/* internal */
|
|
2525
|
-
true
|
|
2526
|
-
);
|
|
2599
|
+
void this.snapTo(snap, 0, true);
|
|
2527
2600
|
} else if (!ids.includes(this.activeId)) {
|
|
2528
2601
|
const fallback = ids[0];
|
|
2529
|
-
if (fallback) void this.snapTo(
|
|
2530
|
-
fallback,
|
|
2531
|
-
0,
|
|
2532
|
-
/* internal */
|
|
2533
|
-
true
|
|
2534
|
-
);
|
|
2602
|
+
if (fallback) void this.snapTo(fallback, 0, true);
|
|
2535
2603
|
}
|
|
2536
2604
|
}
|
|
2537
2605
|
setBackdropRange(range) {
|
|
@@ -2549,21 +2617,6 @@ var BottomSheetEngine = class {
|
|
|
2549
2617
|
setScrimInteractive(interactive) {
|
|
2550
2618
|
this.scrim.setScrimInteractive(interactive);
|
|
2551
2619
|
}
|
|
2552
|
-
/**
|
|
2553
|
-
* Batch-update multiple scrim fields in one call. Apply order is:
|
|
2554
|
-
* 1. `preset` — fills baseline color/blur/range/interactive.
|
|
2555
|
-
* 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
|
|
2556
|
-
* 3. `enabled` — flips the opacity contribution on/off.
|
|
2557
|
-
* 4. `tapToClose` — installs/tears the click listener.
|
|
2558
|
-
* 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
|
|
2559
|
-
* 6. ONE `applySize` so the per-frame opacity stays consistent.
|
|
2560
|
-
*
|
|
2561
|
-
* Performance: collecting changes here (vs calling individual setters)
|
|
2562
|
-
* avoids repeated `applySize` calls during a multi-field swap. Setters
|
|
2563
|
-
* called here suppress their own internal `applySize` either by passing
|
|
2564
|
-
* through fast-paths (mode/enabled no-op when value unchanged) or by
|
|
2565
|
-
* being visual-only (color/blur/interactive don't influence opacity).
|
|
2566
|
-
*/
|
|
2567
2620
|
setScrim(opts) {
|
|
2568
2621
|
this.scrim.setScrim(opts, () => this.applySize(this.size));
|
|
2569
2622
|
}
|
|
@@ -2579,11 +2632,79 @@ var BottomSheetEngine = class {
|
|
|
2579
2632
|
setScrimOverlay(opts) {
|
|
2580
2633
|
return this.scrim.setScrimOverlay(opts);
|
|
2581
2634
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2635
|
+
addAnchor(opts) {
|
|
2636
|
+
if (this.destroyed || typeof document === "undefined") return () => {
|
|
2637
|
+
};
|
|
2638
|
+
if (!this.anchorHost) {
|
|
2639
|
+
this.anchorHost = this.rootEl ?? this.element.parentElement ?? document.body;
|
|
2640
|
+
this.anchorHost.style.setProperty("--bs-size", `${this.size}px`);
|
|
2641
|
+
this.anchorHost.style.setProperty(
|
|
2642
|
+
"--bs-progress",
|
|
2643
|
+
String(this.computeProgress(this.size))
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
const handle = attachAnchor(
|
|
2647
|
+
{
|
|
2648
|
+
mode: this.mode,
|
|
2649
|
+
host: this.anchorHost,
|
|
2650
|
+
getState: () => ({
|
|
2651
|
+
activeId: this.activeId,
|
|
2652
|
+
size: this.size,
|
|
2653
|
+
progress: this.computeProgress(this.size)
|
|
2654
|
+
}),
|
|
2655
|
+
on: (event, fn) => this.on(event, fn),
|
|
2656
|
+
isDestroyed: () => this.destroyed
|
|
2657
|
+
},
|
|
2658
|
+
opts
|
|
2659
|
+
);
|
|
2660
|
+
handle.syncZ(this.stackZ + 1);
|
|
2661
|
+
this.anchors.push(handle);
|
|
2662
|
+
const detach = () => {
|
|
2663
|
+
const idx = this.anchors.indexOf(handle);
|
|
2664
|
+
if (idx === -1) return;
|
|
2665
|
+
this.anchors.splice(idx, 1);
|
|
2666
|
+
handle.detach();
|
|
2667
|
+
};
|
|
2668
|
+
this.teardowns.add(detach);
|
|
2669
|
+
return detach;
|
|
2670
|
+
}
|
|
2671
|
+
setScrimStages(opts) {
|
|
2672
|
+
if (this.destroyed || typeof document === "undefined") return () => {
|
|
2673
|
+
};
|
|
2674
|
+
this.detachScrimStages?.();
|
|
2675
|
+
this.detachScrimStages = null;
|
|
2676
|
+
if (!opts) return () => {
|
|
2677
|
+
};
|
|
2678
|
+
const host = this.screenComponent?.parentElement;
|
|
2679
|
+
if (!host) {
|
|
2680
|
+
console.warn(
|
|
2681
|
+
"[BottomSheet] setScrimStages: no scrim element mounted \u2014 pass `scrim` to the engine first."
|
|
2682
|
+
);
|
|
2683
|
+
return () => {
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
const detachInstalled = installScrimStages(
|
|
2687
|
+
{
|
|
2688
|
+
mode: this.mode,
|
|
2689
|
+
host,
|
|
2690
|
+
getState: () => ({
|
|
2691
|
+
activeId: this.activeId,
|
|
2692
|
+
size: this.size,
|
|
2693
|
+
progress: this.computeProgress(this.size)
|
|
2694
|
+
}),
|
|
2695
|
+
on: (event, fn) => this.on(event, fn),
|
|
2696
|
+
isDestroyed: () => this.destroyed
|
|
2697
|
+
},
|
|
2698
|
+
opts
|
|
2699
|
+
);
|
|
2700
|
+
const detach = () => {
|
|
2701
|
+
if (this.detachScrimStages === detach) this.detachScrimStages = null;
|
|
2702
|
+
detachInstalled();
|
|
2703
|
+
};
|
|
2704
|
+
this.detachScrimStages = detach;
|
|
2705
|
+
this.teardowns.add(() => this.detachScrimStages?.());
|
|
2706
|
+
return detach;
|
|
2707
|
+
}
|
|
2587
2708
|
getScrimState() {
|
|
2588
2709
|
return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
|
|
2589
2710
|
}
|
|
@@ -2629,24 +2750,11 @@ var BottomSheetEngine = class {
|
|
|
2629
2750
|
emit(event, payload) {
|
|
2630
2751
|
this.bus.emit(event, payload);
|
|
2631
2752
|
}
|
|
2632
|
-
/**
|
|
2633
|
-
* Abort the current cycle's signal and replace the controller. Returns the
|
|
2634
|
-
* fresh signal so the caller can capture it locally before any await — when
|
|
2635
|
-
* a future newCycle() / destroy aborts our controller, the captured signal
|
|
2636
|
-
* flips to aborted and post-await guards short-circuit.
|
|
2637
|
-
*/
|
|
2638
2753
|
newCycle() {
|
|
2639
2754
|
this.currentAbort.abort();
|
|
2640
2755
|
this.currentAbort = new AbortController();
|
|
2641
2756
|
return this.currentAbort.signal;
|
|
2642
2757
|
}
|
|
2643
|
-
/**
|
|
2644
|
-
* Fire `before-snap` and report whether a listener cancelled it. The
|
|
2645
|
-
* `cancel()` callback exposed to listeners is frozen after the synchronous
|
|
2646
|
-
* emit phase so async callers can't silently miss — they get a console
|
|
2647
|
-
* warning. Returns `true` when cancelled so callers can early-return /
|
|
2648
|
-
* spring-back without re-implementing the cancel/frozen pattern.
|
|
2649
|
-
*/
|
|
2650
2758
|
emitBeforeSnap(target, previousId) {
|
|
2651
2759
|
let cancelled = false;
|
|
2652
2760
|
let frozen = false;
|
|
@@ -2680,59 +2788,54 @@ var BottomSheetEngine = class {
|
|
|
2680
2788
|
this.snaps.setRaw(this.snapPointsRaw);
|
|
2681
2789
|
this.scrim.invalidateOpacityCache();
|
|
2682
2790
|
}
|
|
2683
|
-
/**
|
|
2684
|
-
* Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
|
|
2685
|
-
* Engine wrapper kept so existing call sites don't all need updating;
|
|
2686
|
-
* resolver-level memoization survives across method invocations.
|
|
2687
|
-
*/
|
|
2688
2791
|
getAllowedRange() {
|
|
2689
2792
|
return this.snaps.getAllowedRange();
|
|
2690
2793
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
* without it screen readers can't announce drag/snap progress to keyboard
|
|
2697
|
-
* + AT users (WCAG 4.1.2 Name, Role, Value).
|
|
2698
|
-
*
|
|
2699
|
-
* Engine does NOT set `role="slider"` itself — that's the adapter's or
|
|
2700
|
-
* consumer markup's responsibility. We only decorate with dynamic values.
|
|
2701
|
-
*
|
|
2702
|
-
* Empty allowed-list is malformed input (settable via `setAllowed([])`);
|
|
2703
|
-
* short-circuit to remove ARIA value attributes so screen readers see a
|
|
2704
|
-
* no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
|
|
2705
|
-
* requires `valuemax > valuemin` for `role="slider"`).
|
|
2706
|
-
*/
|
|
2794
|
+
allowedIdsBySize() {
|
|
2795
|
+
return this.snaps.getAllowedIds().slice().sort(
|
|
2796
|
+
(a, b) => (this.snaps.findById(a)?.size ?? 0) - (this.snaps.findById(b)?.size ?? 0)
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2707
2799
|
updateAriaSlider() {
|
|
2708
|
-
this.aria.setValue(this.
|
|
2800
|
+
this.aria.setValue(this.allowedIdsBySize(), this.activeId);
|
|
2709
2801
|
}
|
|
2710
2802
|
registerInStack() {
|
|
2711
2803
|
this.teardowns.add(sheetStack.push({
|
|
2712
2804
|
id: this.id,
|
|
2713
2805
|
setZIndex: (z) => {
|
|
2806
|
+
this.stackZ = z;
|
|
2714
2807
|
this.element.style.zIndex = String(z);
|
|
2715
2808
|
if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
|
|
2809
|
+
for (const anchor of this.anchors) anchor.syncZ(z + 1);
|
|
2716
2810
|
},
|
|
2717
2811
|
setIsTop: (isTop) => {
|
|
2718
2812
|
this.isTopSheet = isTop;
|
|
2719
2813
|
if (this.backdrop) {
|
|
2720
2814
|
this.backdrop.style.display = isTop ? "" : "none";
|
|
2721
2815
|
}
|
|
2722
|
-
}
|
|
2816
|
+
},
|
|
2817
|
+
isOpen: () => this.size > 0,
|
|
2818
|
+
setDepth: (depth) => this.applyStackDepth(depth)
|
|
2723
2819
|
}));
|
|
2724
2820
|
}
|
|
2821
|
+
applyStackDepth(depth) {
|
|
2822
|
+
if (!this.stackEffectEnabled) return;
|
|
2823
|
+
this.element.setAttribute("data-stack-depth", String(depth));
|
|
2824
|
+
if (!this.stackEffectPrimed) {
|
|
2825
|
+
this.stackEffectPrimed = true;
|
|
2826
|
+
const isVertical = this.mode === "bottom" || this.mode === "top";
|
|
2827
|
+
this.element.style.transformOrigin = isVertical ? "50% 100%" : "0% 50%";
|
|
2828
|
+
this.element.style.transition = "scale 320ms cubic-bezier(0.29, 1.04, 0.84, 0.99)";
|
|
2829
|
+
}
|
|
2830
|
+
const scale = Math.max(1 - depth * 0.04, 0.86);
|
|
2831
|
+
this.element.style.scale = scale === 1 ? "" : String(scale);
|
|
2832
|
+
}
|
|
2725
2833
|
attach() {
|
|
2726
2834
|
this.gesture = new GestureController({
|
|
2727
2835
|
handle: this.handle,
|
|
2728
2836
|
element: this.element,
|
|
2729
2837
|
mode: this.mode,
|
|
2730
2838
|
getRoot: () => this.rootEl,
|
|
2731
|
-
// Single coalesced reader — narrows the DI surface and removes 3 getter
|
|
2732
|
-
// dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
|
|
2733
|
-
// (this.dragContextBuf) — see field JSDoc for the retention contract.
|
|
2734
|
-
// Inner `range` object is mutated in place too so callers caching its
|
|
2735
|
-
// identity between frames stay valid.
|
|
2736
2839
|
getDragContext: () => {
|
|
2737
2840
|
const buf = this.dragContextBuf;
|
|
2738
2841
|
const r = this.getAllowedRange();
|
|
@@ -2786,7 +2889,7 @@ var BottomSheetEngine = class {
|
|
|
2786
2889
|
container: this.scrollContainer,
|
|
2787
2890
|
isDragging: () => this.gesture?.isDragging ?? false,
|
|
2788
2891
|
isAnimating: () => this.animation.isAnimating,
|
|
2789
|
-
getAllowedIds: () => this.
|
|
2892
|
+
getAllowedIds: () => this.allowedIdsBySize(),
|
|
2790
2893
|
getActiveId: () => this.activeId,
|
|
2791
2894
|
snapTo: (id) => {
|
|
2792
2895
|
void this.snapTo(id);
|
|
@@ -2815,11 +2918,13 @@ var BottomSheetEngine = class {
|
|
|
2815
2918
|
},
|
|
2816
2919
|
newCycle: () => {
|
|
2817
2920
|
this.newCycle();
|
|
2818
|
-
}
|
|
2921
|
+
},
|
|
2922
|
+
isAnimating: () => this.animation.isAnimating,
|
|
2923
|
+
resyncAfterCancel: () => this.resyncAfterResize()
|
|
2819
2924
|
}));
|
|
2820
2925
|
if (this.lifecycle.closeOnEscape) {
|
|
2821
2926
|
const onKey = (e) => {
|
|
2822
|
-
if (this.destroyed) return;
|
|
2927
|
+
if (this.destroyed || e.defaultPrevented) return;
|
|
2823
2928
|
if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
|
|
2824
2929
|
void this.close();
|
|
2825
2930
|
}
|
|
@@ -2851,7 +2956,7 @@ var BottomSheetEngine = class {
|
|
|
2851
2956
|
handle: this.handle,
|
|
2852
2957
|
mode: this.mode,
|
|
2853
2958
|
isDestroyed: () => this.destroyed,
|
|
2854
|
-
getAllowedIds: () => this.
|
|
2959
|
+
getAllowedIds: () => this.allowedIdsBySize(),
|
|
2855
2960
|
getActiveId: () => this.activeId,
|
|
2856
2961
|
snapTo: (id) => {
|
|
2857
2962
|
void this.snapTo(id);
|
|
@@ -2872,6 +2977,7 @@ var BottomSheetEngine = class {
|
|
|
2872
2977
|
});
|
|
2873
2978
|
if (!target) return;
|
|
2874
2979
|
const previousId = this.activeId;
|
|
2980
|
+
if (target.id === previousId && target.size === this.size) return;
|
|
2875
2981
|
if (this.emitBeforeSnap(target, previousId)) {
|
|
2876
2982
|
const restore = this.snaps.findById(previousId);
|
|
2877
2983
|
if (restore) {
|
|
@@ -2880,6 +2986,7 @@ var BottomSheetEngine = class {
|
|
|
2880
2986
|
return;
|
|
2881
2987
|
}
|
|
2882
2988
|
const previousSize = this.size;
|
|
2989
|
+
const previousSnapSize = this.snaps.findById(previousId)?.size;
|
|
2883
2990
|
this.scrollCache.cache(previousId, previousSize, target.size);
|
|
2884
2991
|
this.activeId = target.id;
|
|
2885
2992
|
void this.animation.animateTo(target.size, velocity).then(() => {
|
|
@@ -2888,9 +2995,10 @@ var BottomSheetEngine = class {
|
|
|
2888
2995
|
this.updateAriaSlider();
|
|
2889
2996
|
this.emit("snap", { id: target.id, size: target.size });
|
|
2890
2997
|
this.haptic();
|
|
2891
|
-
if (
|
|
2998
|
+
if (previousSnapSize === 0 && target.size > 0) {
|
|
2892
2999
|
this.emit("open", { id: target.id });
|
|
2893
3000
|
this.handleOpen();
|
|
3001
|
+
notifyLinkedSheets(this.linkedSheets, this);
|
|
2894
3002
|
}
|
|
2895
3003
|
if (target.size === 0) {
|
|
2896
3004
|
this.emit("close", void 0);
|
|
@@ -2898,7 +3006,6 @@ var BottomSheetEngine = class {
|
|
|
2898
3006
|
}
|
|
2899
3007
|
});
|
|
2900
3008
|
}
|
|
2901
|
-
/** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
|
|
2902
3009
|
haptic() {
|
|
2903
3010
|
if (typeof navigator !== "undefined" && "vibrate" in navigator) {
|
|
2904
3011
|
try {
|
|
@@ -2914,6 +3021,12 @@ var BottomSheetEngine = class {
|
|
|
2914
3021
|
style.transform = this.transformTemplate(offset);
|
|
2915
3022
|
if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
|
|
2916
3023
|
style.setProperty("--bs-size", `${size}px`);
|
|
3024
|
+
if (this.scrimParent) {
|
|
3025
|
+
this.scrimParent.style.setProperty("--bs-size", `${size}px`);
|
|
3026
|
+
}
|
|
3027
|
+
if (this.anchorHost && this.anchorHost !== this.scrimParent) {
|
|
3028
|
+
this.anchorHost.style.setProperty("--bs-size", `${size}px`);
|
|
3029
|
+
}
|
|
2917
3030
|
}
|
|
2918
3031
|
const progress = this.computeProgress(size);
|
|
2919
3032
|
const progressChanged = this.progressWriteSentinel.shouldWrite(
|
|
@@ -2922,6 +3035,9 @@ var BottomSheetEngine = class {
|
|
|
2922
3035
|
);
|
|
2923
3036
|
if (progressChanged) {
|
|
2924
3037
|
style.setProperty("--bs-progress", String(progress));
|
|
3038
|
+
if (this.anchorHost) {
|
|
3039
|
+
this.anchorHost.style.setProperty("--bs-progress", String(progress));
|
|
3040
|
+
}
|
|
2925
3041
|
}
|
|
2926
3042
|
this.scrim.applyOpacity(progress, progressChanged);
|
|
2927
3043
|
if (progressChanged && this.bus.listenerCount("progress") > 0) {
|
|
@@ -2936,6 +3052,7 @@ var BottomSheetEngine = class {
|
|
|
2936
3052
|
return Math.min(Math.max((size - min) / (max - min), 0), 1);
|
|
2937
3053
|
}
|
|
2938
3054
|
handleOpen() {
|
|
3055
|
+
sheetStack.promote(this.id);
|
|
2939
3056
|
try {
|
|
2940
3057
|
this.lifecycle.install();
|
|
2941
3058
|
} catch (err) {
|
|
@@ -2949,8 +3066,24 @@ var BottomSheetEngine = class {
|
|
|
2949
3066
|
});
|
|
2950
3067
|
}
|
|
2951
3068
|
}
|
|
3069
|
+
resyncAfterResize() {
|
|
3070
|
+
if (this.destroyed) return;
|
|
3071
|
+
const target = this.snaps.findById(this.activeId);
|
|
3072
|
+
if (!target) return;
|
|
3073
|
+
this.updateAriaSlider();
|
|
3074
|
+
this.emit("snap", { id: target.id, size: target.size });
|
|
3075
|
+
if (target.size > 0 && !this.lifecycle.isInstalled) {
|
|
3076
|
+
this.emit("open", { id: target.id });
|
|
3077
|
+
this.handleOpen();
|
|
3078
|
+
notifyLinkedSheets(this.linkedSheets, this);
|
|
3079
|
+
} else if (target.size === 0 && this.lifecycle.isInstalled) {
|
|
3080
|
+
this.emit("close", void 0);
|
|
3081
|
+
this.handleClose();
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
2952
3084
|
handleClose() {
|
|
2953
3085
|
this.lifecycle.release();
|
|
3086
|
+
sheetStack.update();
|
|
2954
3087
|
}
|
|
2955
3088
|
};
|
|
2956
3089
|
|
|
@@ -2984,7 +3117,7 @@ function useBottomSheet(opts) {
|
|
|
2984
3117
|
handle: handleRef.current ?? void 0,
|
|
2985
3118
|
scrollContainer: contentRef.current ?? void 0,
|
|
2986
3119
|
backdrop: backdropRef.current ?? void 0,
|
|
2987
|
-
|
|
3120
|
+
scrim: screenRef.current ?? void 0
|
|
2988
3121
|
});
|
|
2989
3122
|
engineRef.current = engine;
|
|
2990
3123
|
cachedSnapshotRef.current = { ...engine.state };
|
|
@@ -3044,6 +3177,16 @@ function useBottomSheet(opts) {
|
|
|
3044
3177
|
engineRef.current?.setAllowed(ids, snap);
|
|
3045
3178
|
}, []);
|
|
3046
3179
|
const getEngine = react.useCallback(() => engineRef.current, []);
|
|
3180
|
+
const addAnchor = react.useCallback(
|
|
3181
|
+
(anchorOpts) => engineRef.current?.addAnchor(anchorOpts) ?? (() => {
|
|
3182
|
+
}),
|
|
3183
|
+
[]
|
|
3184
|
+
);
|
|
3185
|
+
const setScrimStages = react.useCallback(
|
|
3186
|
+
(stagesOpts) => engineRef.current?.setScrimStages(stagesOpts) ?? (() => {
|
|
3187
|
+
}),
|
|
3188
|
+
[]
|
|
3189
|
+
);
|
|
3047
3190
|
return react.useMemo(
|
|
3048
3191
|
() => ({
|
|
3049
3192
|
sheetRef,
|
|
@@ -3056,10 +3199,12 @@ function useBottomSheet(opts) {
|
|
|
3056
3199
|
open,
|
|
3057
3200
|
close,
|
|
3058
3201
|
setAllowed,
|
|
3202
|
+
addAnchor,
|
|
3203
|
+
setScrimStages,
|
|
3059
3204
|
engine: engineRef.current,
|
|
3060
3205
|
getEngine
|
|
3061
3206
|
}),
|
|
3062
|
-
[state, snapTo, open, close, setAllowed, getEngine]
|
|
3207
|
+
[state, snapTo, open, close, setAllowed, addAnchor, setScrimStages, getEngine]
|
|
3063
3208
|
);
|
|
3064
3209
|
}
|
|
3065
3210
|
var useHasMounted = () => {
|
|
@@ -3079,6 +3224,8 @@ var BottomSheet = react.forwardRef(
|
|
|
3079
3224
|
leftButton,
|
|
3080
3225
|
rightButton,
|
|
3081
3226
|
screen,
|
|
3227
|
+
anchors,
|
|
3228
|
+
scrimStages,
|
|
3082
3229
|
onChange,
|
|
3083
3230
|
ariaLabel = "Bottom sheet",
|
|
3084
3231
|
ariaLabelledBy,
|
|
@@ -3099,10 +3246,7 @@ var BottomSheet = react.forwardRef(
|
|
|
3099
3246
|
setAllowed,
|
|
3100
3247
|
getEngine
|
|
3101
3248
|
} = useBottomSheet(engineOpts);
|
|
3102
|
-
const isVerticalAxis = (engineOpts.mode ?? "bottom") === "bottom" || (engineOpts.mode ?? "bottom") === "top";
|
|
3103
3249
|
const ariaModal = engineOpts.focusTrap === true;
|
|
3104
|
-
const allowedIds = engineOpts.allowed ?? engineOpts.snapPoints.map((p) => p.id);
|
|
3105
|
-
const activeIdx = allowedIds.indexOf(state.activeId);
|
|
3106
3250
|
const lastAnnouncedRef = react.useRef("");
|
|
3107
3251
|
const [announce, setAnnounce] = react.useState("");
|
|
3108
3252
|
react.useEffect(() => {
|
|
@@ -3118,6 +3262,15 @@ var BottomSheet = react.forwardRef(
|
|
|
3118
3262
|
open,
|
|
3119
3263
|
close,
|
|
3120
3264
|
setAllowed,
|
|
3265
|
+
setSnapPoints: (points, allowed) => getEngine()?.setSnapPoints(points, allowed),
|
|
3266
|
+
setScrim: (opts) => getEngine()?.setScrim(opts),
|
|
3267
|
+
setScrimOverlay: (opts) => getEngine()?.setScrimOverlay(opts) ?? (() => {
|
|
3268
|
+
}),
|
|
3269
|
+
addAnchor: (opts) => getEngine()?.addAnchor(opts) ?? (() => {
|
|
3270
|
+
}),
|
|
3271
|
+
setScrimStages: (opts) => getEngine()?.setScrimStages(opts) ?? (() => {
|
|
3272
|
+
}),
|
|
3273
|
+
getEngine,
|
|
3121
3274
|
get state() {
|
|
3122
3275
|
return getEngine()?.state ?? state;
|
|
3123
3276
|
}
|
|
@@ -3127,6 +3280,61 @@ var BottomSheet = react.forwardRef(
|
|
|
3127
3280
|
react.useEffect(() => {
|
|
3128
3281
|
onChange?.(state);
|
|
3129
3282
|
}, [state, onChange]);
|
|
3283
|
+
const [anchorHosts, setAnchorHosts] = react.useState([]);
|
|
3284
|
+
const anchorsKey = anchors ? JSON.stringify(
|
|
3285
|
+
anchors.map((a) => [
|
|
3286
|
+
a.position,
|
|
3287
|
+
a.inset,
|
|
3288
|
+
Array.isArray(a.showOn) ? a.showOn : a.showOn ? "fn" : null,
|
|
3289
|
+
a.fadeRange,
|
|
3290
|
+
a.interactive,
|
|
3291
|
+
typeof a.animation === "string" ? a.animation : a.animation ? "custom" : null
|
|
3292
|
+
])
|
|
3293
|
+
) : "";
|
|
3294
|
+
react.useEffect(() => {
|
|
3295
|
+
const engine = getEngine();
|
|
3296
|
+
if (!engine || !anchors || anchors.length === 0) return;
|
|
3297
|
+
const hosts = [];
|
|
3298
|
+
const detachers = anchors.map((a) => {
|
|
3299
|
+
const host = document.createElement("div");
|
|
3300
|
+
hosts.push(host);
|
|
3301
|
+
const { node: _node, ...rest } = a;
|
|
3302
|
+
return engine.addAnchor({ ...rest, element: host });
|
|
3303
|
+
});
|
|
3304
|
+
setAnchorHosts(hosts);
|
|
3305
|
+
return () => {
|
|
3306
|
+
detachers.forEach((d) => d());
|
|
3307
|
+
setAnchorHosts([]);
|
|
3308
|
+
};
|
|
3309
|
+
}, [getEngine, anchorsKey]);
|
|
3310
|
+
const [stageHosts, setStageHosts] = react.useState([]);
|
|
3311
|
+
const stagesKey = scrimStages ? JSON.stringify(
|
|
3312
|
+
scrimStages.stages.map((s) => [
|
|
3313
|
+
s.for,
|
|
3314
|
+
s.forRange,
|
|
3315
|
+
s.position,
|
|
3316
|
+
s.inset,
|
|
3317
|
+
s.interactive,
|
|
3318
|
+
typeof s.animation === "string" ? s.animation : s.animation ? "custom" : null
|
|
3319
|
+
])
|
|
3320
|
+
) : "";
|
|
3321
|
+
react.useEffect(() => {
|
|
3322
|
+
const engine = getEngine();
|
|
3323
|
+
if (!engine || !scrimStages || scrimStages.stages.length === 0) return;
|
|
3324
|
+
const hosts = [];
|
|
3325
|
+
const defs = scrimStages.stages.map((s) => {
|
|
3326
|
+
const host = document.createElement("div");
|
|
3327
|
+
hosts.push(host);
|
|
3328
|
+
const { node: _node, ...rest } = s;
|
|
3329
|
+
return { ...rest, element: host };
|
|
3330
|
+
});
|
|
3331
|
+
const detach = engine.setScrimStages({ ...scrimStages, stages: defs });
|
|
3332
|
+
setStageHosts(hosts);
|
|
3333
|
+
return () => {
|
|
3334
|
+
detach();
|
|
3335
|
+
setStageHosts([]);
|
|
3336
|
+
};
|
|
3337
|
+
}, [getEngine, stagesKey]);
|
|
3130
3338
|
if (noSSR && !mounted) return null;
|
|
3131
3339
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bs-root", children: [
|
|
3132
3340
|
backdrop && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3137,7 +3345,7 @@ var BottomSheet = react.forwardRef(
|
|
|
3137
3345
|
onClick: closeOnBackdrop ? () => void close() : void 0
|
|
3138
3346
|
}
|
|
3139
3347
|
),
|
|
3140
|
-
|
|
3348
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3141
3349
|
"div",
|
|
3142
3350
|
{
|
|
3143
3351
|
ref: screenRef,
|
|
@@ -3167,11 +3375,6 @@ var BottomSheet = react.forwardRef(
|
|
|
3167
3375
|
className: "bs-handle",
|
|
3168
3376
|
role: "slider",
|
|
3169
3377
|
tabIndex: 0,
|
|
3170
|
-
"aria-orientation": isVerticalAxis ? "vertical" : "horizontal",
|
|
3171
|
-
"aria-valuemin": 0,
|
|
3172
|
-
"aria-valuemax": Math.max(0, allowedIds.length - 1),
|
|
3173
|
-
"aria-valuenow": Math.max(0, activeIdx),
|
|
3174
|
-
"aria-valuetext": state.activeId,
|
|
3175
3378
|
"aria-label": "Resize sheet",
|
|
3176
3379
|
children: typeof header === "function" ? header(state) : header
|
|
3177
3380
|
}
|
|
@@ -3190,6 +3393,12 @@ var BottomSheet = react.forwardRef(
|
|
|
3190
3393
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "bs-sr-only", role: "status", "aria-live": "polite", children: announce })
|
|
3191
3394
|
]
|
|
3192
3395
|
}
|
|
3396
|
+
),
|
|
3397
|
+
anchorHosts.map(
|
|
3398
|
+
(host, i) => anchors?.[i] ? reactDom.createPortal(anchors[i].node, host) : null
|
|
3399
|
+
),
|
|
3400
|
+
stageHosts.map(
|
|
3401
|
+
(host, i) => scrimStages?.stages[i] ? reactDom.createPortal(scrimStages.stages[i].node, host) : null
|
|
3193
3402
|
)
|
|
3194
3403
|
] });
|
|
3195
3404
|
}
|
|
@@ -3260,8 +3469,6 @@ var OverlayEngine = class {
|
|
|
3260
3469
|
__publicField(this, "exitEasing");
|
|
3261
3470
|
__publicField(this, "backdropOpacity");
|
|
3262
3471
|
__publicField(this, "backdropFilter");
|
|
3263
|
-
// Definite-assignment: written via assignSwipeFromConfig in the
|
|
3264
|
-
// constructor — TS can't follow the helper, mark the assertion explicit.
|
|
3265
3472
|
__publicField(this, "swipeToCloseEnabled");
|
|
3266
3473
|
__publicField(this, "swipeThreshold");
|
|
3267
3474
|
__publicField(this, "swipeVelocityThreshold");
|
|
@@ -3276,18 +3483,16 @@ var OverlayEngine = class {
|
|
|
3276
3483
|
__publicField(this, "closeOnOutsidePointerEnabled");
|
|
3277
3484
|
__publicField(this, "detachOutsidePointer", null);
|
|
3278
3485
|
__publicField(this, "detachSwipe", null);
|
|
3486
|
+
__publicField(this, "respectReducedMotionEnabled");
|
|
3487
|
+
__publicField(this, "isTop", true);
|
|
3488
|
+
__publicField(this, "backPushed", false);
|
|
3489
|
+
__publicField(this, "movedToTarget", false);
|
|
3279
3490
|
__publicField(this, "onOpenCb");
|
|
3280
3491
|
__publicField(this, "onCloseCb");
|
|
3281
3492
|
__publicField(this, "isOpen_", false);
|
|
3282
3493
|
__publicField(this, "isAnimating_", false);
|
|
3283
3494
|
__publicField(this, "destroyed", false);
|
|
3284
|
-
// Tracks whether setOverlayChildren ever populated the panel — lets
|
|
3285
|
-
// clearOverlayChildren skip the DOM mutation when nothing was injected
|
|
3286
|
-
// (so the field also acts as a zero-allocation feature flag).
|
|
3287
3495
|
__publicField(this, "hasInjectedChildren", false);
|
|
3288
|
-
// Bumped on every open/close. awaitTransition captures it and ignores
|
|
3289
|
-
// transitionend/timeout fired by older cycles — prevents cross-cycle
|
|
3290
|
-
// listener bleed during rapid toggling.
|
|
3291
3496
|
__publicField(this, "cycleNonce", 0);
|
|
3292
3497
|
__publicField(this, "bus", createEventBus());
|
|
3293
3498
|
__publicField(this, "detachEscape", null);
|
|
@@ -3318,6 +3523,7 @@ var OverlayEngine = class {
|
|
|
3318
3523
|
this.mountTarget = opts.mountTo;
|
|
3319
3524
|
this.peekRaw = opts.peek;
|
|
3320
3525
|
this.closeOnOutsidePointerEnabled = opts.closeOnOutsidePointer ?? false;
|
|
3526
|
+
this.respectReducedMotionEnabled = opts.respectReducedMotion ?? true;
|
|
3321
3527
|
this.onOpenCb = opts.onOpen;
|
|
3322
3528
|
this.onCloseCb = opts.onClose;
|
|
3323
3529
|
if (this.peekRaw !== void 0 && this.edge !== "bottom") {
|
|
@@ -3340,9 +3546,6 @@ var OverlayEngine = class {
|
|
|
3340
3546
|
closeOnEscape: this.closeOnEscape,
|
|
3341
3547
|
lockBodyScroll: this.bodyScrollLockEnabled,
|
|
3342
3548
|
inertSiblings: this.inertSiblingsEnabled,
|
|
3343
|
-
// Overlay-specific guard: only paint body siblings inert when the
|
|
3344
|
-
// overlay element is actually a body descendant. Detached / shadow-
|
|
3345
|
-
// DOM / portal overlays would otherwise lock unrelated trees.
|
|
3346
3549
|
shouldApplyInertSiblings: () => {
|
|
3347
3550
|
const bodyChildren = Array.from(document.body.children);
|
|
3348
3551
|
return bodyChildren.some(
|
|
@@ -3368,15 +3571,17 @@ var OverlayEngine = class {
|
|
|
3368
3571
|
this.emit("before-open", void 0);
|
|
3369
3572
|
this.isOpen_ = true;
|
|
3370
3573
|
this.isAnimating_ = true;
|
|
3574
|
+
sheetStack.promote(this.id);
|
|
3371
3575
|
this.element.removeAttribute("hidden");
|
|
3372
3576
|
this.element.setAttribute("data-state", "entering");
|
|
3373
3577
|
if (this.focusTrapEnabled) {
|
|
3374
3578
|
this.element.setAttribute("aria-modal", "true");
|
|
3375
3579
|
}
|
|
3376
|
-
const
|
|
3580
|
+
const dur = this.effectiveDuration();
|
|
3581
|
+
const transitionStr = `transform ${dur}ms ${this.enterEasing}, opacity ${dur}ms ease-out`;
|
|
3377
3582
|
this.element.style.transition = transitionStr;
|
|
3378
3583
|
if (this.backdrop) {
|
|
3379
|
-
this.backdrop.style.transition = `opacity ${
|
|
3584
|
+
this.backdrop.style.transition = `opacity ${dur}ms ease-out`;
|
|
3380
3585
|
}
|
|
3381
3586
|
const cycle = ++this.cycleNonce;
|
|
3382
3587
|
return new Promise((resolve) => {
|
|
@@ -3388,7 +3593,7 @@ var OverlayEngine = class {
|
|
|
3388
3593
|
this.element.style.opacity = "1";
|
|
3389
3594
|
if (this.backdrop) {
|
|
3390
3595
|
this.backdrop.style.opacity = String(this.backdropOpacity);
|
|
3391
|
-
this.backdrop.style.pointerEvents = "auto";
|
|
3596
|
+
this.backdrop.style.pointerEvents = this.backdropOpacity > 0 ? "auto" : "none";
|
|
3392
3597
|
if (this.backdropFilter) {
|
|
3393
3598
|
this.backdrop.style.backdropFilter = this.backdropFilter;
|
|
3394
3599
|
this.backdrop.style.webkitBackdropFilter = this.backdropFilter;
|
|
@@ -3424,7 +3629,10 @@ var OverlayEngine = class {
|
|
|
3424
3629
|
});
|
|
3425
3630
|
return resolve();
|
|
3426
3631
|
}
|
|
3427
|
-
this.awaitTransition(
|
|
3632
|
+
this.awaitTransition(
|
|
3633
|
+
cycle,
|
|
3634
|
+
this.enterAnimation === "fade" ? "opacity" : "transform"
|
|
3635
|
+
).then(() => {
|
|
3428
3636
|
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
3429
3637
|
this.isAnimating_ = false;
|
|
3430
3638
|
this.element.setAttribute("data-state", "open");
|
|
@@ -3441,11 +3649,14 @@ var OverlayEngine = class {
|
|
|
3441
3649
|
this.emit("before-close", void 0);
|
|
3442
3650
|
this.isOpen_ = false;
|
|
3443
3651
|
this.isAnimating_ = true;
|
|
3652
|
+
sheetStack.update();
|
|
3653
|
+
this.popBackMarker(reason);
|
|
3444
3654
|
this.element.setAttribute("data-state", "exiting");
|
|
3445
|
-
const
|
|
3655
|
+
const dur = this.effectiveDuration();
|
|
3656
|
+
const transitionStr = `transform ${dur}ms ${this.exitEasing}, opacity ${dur}ms ease-in`;
|
|
3446
3657
|
this.element.style.transition = transitionStr;
|
|
3447
3658
|
if (this.backdrop) {
|
|
3448
|
-
this.backdrop.style.transition = `opacity ${
|
|
3659
|
+
this.backdrop.style.transition = `opacity ${dur}ms ease-in`;
|
|
3449
3660
|
}
|
|
3450
3661
|
const cycle = ++this.cycleNonce;
|
|
3451
3662
|
return new Promise((resolve) => {
|
|
@@ -3456,7 +3667,7 @@ var OverlayEngine = class {
|
|
|
3456
3667
|
this.exitAnimation,
|
|
3457
3668
|
this.peekPx
|
|
3458
3669
|
);
|
|
3459
|
-
this.element.style.opacity = "0";
|
|
3670
|
+
this.element.style.opacity = this.peekPx !== void 0 ? "1" : "0";
|
|
3460
3671
|
if (this.backdrop) {
|
|
3461
3672
|
this.backdrop.style.opacity = "0";
|
|
3462
3673
|
this.backdrop.style.pointerEvents = "none";
|
|
@@ -3466,7 +3677,10 @@ var OverlayEngine = class {
|
|
|
3466
3677
|
}
|
|
3467
3678
|
}
|
|
3468
3679
|
this.releaseInteractiveListeners();
|
|
3469
|
-
this.awaitTransition(
|
|
3680
|
+
this.awaitTransition(
|
|
3681
|
+
cycle,
|
|
3682
|
+
this.exitAnimation === "fade" ? "opacity" : "transform"
|
|
3683
|
+
).then(() => {
|
|
3470
3684
|
if (this.destroyed || cycle !== this.cycleNonce) return resolve();
|
|
3471
3685
|
this.isAnimating_ = false;
|
|
3472
3686
|
if (this.peekPx === void 0) {
|
|
@@ -3494,14 +3708,12 @@ var OverlayEngine = class {
|
|
|
3494
3708
|
this.releaseInteractiveListeners();
|
|
3495
3709
|
this.releaseStackEntry?.();
|
|
3496
3710
|
this.releaseStackEntry = null;
|
|
3711
|
+
const hadOriginalParent = this.originalParent !== null;
|
|
3497
3712
|
this.restoreMount();
|
|
3713
|
+
if (this.movedToTarget && !hadOriginalParent && this.element.parentNode) {
|
|
3714
|
+
this.element.parentNode.removeChild(this.element);
|
|
3715
|
+
}
|
|
3498
3716
|
}
|
|
3499
|
-
/**
|
|
3500
|
-
* Runtime visual setter — opacity is applied immediately when open so a
|
|
3501
|
-
* live-controls panel can preview the change without re-opening. When
|
|
3502
|
-
* closed we only stash the field; next open() picks it up so we don't
|
|
3503
|
-
* mutate hidden DOM (avoids paint thrash on detached panels).
|
|
3504
|
-
*/
|
|
3505
3717
|
setBackdropOpacity(opacity) {
|
|
3506
3718
|
if (this.destroyed) return;
|
|
3507
3719
|
this.backdropOpacity = opacity;
|
|
@@ -3509,11 +3721,6 @@ var OverlayEngine = class {
|
|
|
3509
3721
|
this.backdrop.style.opacity = String(opacity);
|
|
3510
3722
|
}
|
|
3511
3723
|
}
|
|
3512
|
-
/**
|
|
3513
|
-
* Runtime visual setter for `backdrop-filter`. `undefined` clears the
|
|
3514
|
-
* filter (mirrors the close() behaviour). Defers to next open() when the
|
|
3515
|
-
* overlay is currently closed.
|
|
3516
|
-
*/
|
|
3517
3724
|
setBackdropFilter(filter) {
|
|
3518
3725
|
if (this.destroyed) return;
|
|
3519
3726
|
this.backdropFilter = filter;
|
|
@@ -3527,12 +3734,6 @@ var OverlayEngine = class {
|
|
|
3527
3734
|
style.webkitBackdropFilter = "";
|
|
3528
3735
|
}
|
|
3529
3736
|
}
|
|
3530
|
-
/**
|
|
3531
|
-
* Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
|
|
3532
|
-
* threshold fields, then installs or tears down the pointer listener so
|
|
3533
|
-
* the change applies to the CURRENT open cycle. Idempotent: safe to call
|
|
3534
|
-
* with the same config repeatedly.
|
|
3535
|
-
*/
|
|
3536
3737
|
setSwipeToClose(config) {
|
|
3537
3738
|
if (this.destroyed) return;
|
|
3538
3739
|
const wasEnabled = this.swipeToCloseEnabled;
|
|
@@ -3545,7 +3746,6 @@ var OverlayEngine = class {
|
|
|
3545
3746
|
this.detachSwipe = null;
|
|
3546
3747
|
}
|
|
3547
3748
|
}
|
|
3548
|
-
/** Animation setters only stage the field — applied on next open/close. */
|
|
3549
3749
|
setEnterAnimation(animation) {
|
|
3550
3750
|
if (this.destroyed) return;
|
|
3551
3751
|
this.enterAnimation = animation;
|
|
@@ -3554,23 +3754,15 @@ var OverlayEngine = class {
|
|
|
3554
3754
|
if (this.destroyed) return;
|
|
3555
3755
|
this.exitAnimation = animation;
|
|
3556
3756
|
}
|
|
3557
|
-
/** Updates the field used at next close() — no immediate side effect. */
|
|
3558
3757
|
setReturnFocus(target) {
|
|
3559
3758
|
if (this.destroyed) return;
|
|
3560
3759
|
this.lifecycle.setReturnFocus(target);
|
|
3561
3760
|
}
|
|
3562
|
-
/**
|
|
3563
|
-
* Batch update — preset first, then individual overrides. Applied as a
|
|
3564
|
-
* single DOM-write phase when open so live-controls don't trigger
|
|
3565
|
-
* cascading reflows. Safe to call when closed (defers to next open).
|
|
3566
|
-
*/
|
|
3567
3761
|
setOverlay(opts) {
|
|
3568
3762
|
if (this.destroyed) return;
|
|
3569
3763
|
const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
|
|
3570
3764
|
const merged = preset ? {
|
|
3571
3765
|
backdropOpacity: preset.backdropOpacity,
|
|
3572
|
-
// Preset's `undefined` filter means "no filter" — surface as null
|
|
3573
|
-
// so the merged step clears the field instead of inheriting old.
|
|
3574
3766
|
backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
|
|
3575
3767
|
swipeToClose: preset.swipeToClose,
|
|
3576
3768
|
enterAnimation: preset.enterAnimation,
|
|
@@ -3621,6 +3813,36 @@ var OverlayEngine = class {
|
|
|
3621
3813
|
}
|
|
3622
3814
|
}
|
|
3623
3815
|
}
|
|
3816
|
+
if (opts.duration !== void 0) this.duration = opts.duration;
|
|
3817
|
+
if (opts.enterEasing !== void 0) this.enterEasing = opts.enterEasing;
|
|
3818
|
+
if (opts.exitEasing !== void 0) this.exitEasing = opts.exitEasing;
|
|
3819
|
+
if (opts.closeOnEscape !== void 0) {
|
|
3820
|
+
this.closeOnEscape = opts.closeOnEscape;
|
|
3821
|
+
if (!opts.closeOnEscape) {
|
|
3822
|
+
this.detachEscape?.();
|
|
3823
|
+
this.detachEscape = null;
|
|
3824
|
+
} else if (this.isOpen_) {
|
|
3825
|
+
this.installInteractiveListeners();
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (opts.closeOnBackdrop !== void 0) {
|
|
3829
|
+
this.closeOnBackdropEnabled = opts.closeOnBackdrop;
|
|
3830
|
+
if (!opts.closeOnBackdrop) {
|
|
3831
|
+
this.detachBackdrop?.();
|
|
3832
|
+
this.detachBackdrop = null;
|
|
3833
|
+
} else if (this.isOpen_) {
|
|
3834
|
+
this.installInteractiveListeners();
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
if (opts.closeOnOutsidePointer !== void 0) {
|
|
3838
|
+
this.closeOnOutsidePointerEnabled = opts.closeOnOutsidePointer;
|
|
3839
|
+
if (!opts.closeOnOutsidePointer) {
|
|
3840
|
+
this.detachOutsidePointer?.();
|
|
3841
|
+
this.detachOutsidePointer = null;
|
|
3842
|
+
} else if (this.isOpen_) {
|
|
3843
|
+
this.installInteractiveListeners();
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3624
3846
|
if (opts.children !== void 0) {
|
|
3625
3847
|
if (opts.children === null) {
|
|
3626
3848
|
this.clearOverlayChildren();
|
|
@@ -3629,30 +3851,17 @@ var OverlayEngine = class {
|
|
|
3629
3851
|
}
|
|
3630
3852
|
}
|
|
3631
3853
|
}
|
|
3632
|
-
/**
|
|
3633
|
-
* Runtime children injection — replace the panel's contents without
|
|
3634
|
-
* unmounting the engine. `replaceChildren()` is a single atomic DOM op,
|
|
3635
|
-
* so callers can swap content rapidly without intermediate flicker.
|
|
3636
|
-
* Function form is resolved exactly ONCE per call (cheap lazy content).
|
|
3637
|
-
*/
|
|
3638
3854
|
setOverlayChildren(children) {
|
|
3639
3855
|
if (this.destroyed) return;
|
|
3640
3856
|
const node = typeof children === "function" ? children() : children;
|
|
3641
3857
|
this.element.replaceChildren(node);
|
|
3642
3858
|
this.hasInjectedChildren = true;
|
|
3643
3859
|
}
|
|
3644
|
-
/**
|
|
3645
|
-
* Clear injected children. No-op when nothing was injected so consumers
|
|
3646
|
-
* who never used setOverlayChildren can call this defensively without
|
|
3647
|
-
* paying for a DOM mutation.
|
|
3648
|
-
*/
|
|
3649
3860
|
clearOverlayChildren() {
|
|
3650
3861
|
if (this.destroyed || !this.hasInjectedChildren) return;
|
|
3651
3862
|
this.element.replaceChildren();
|
|
3652
3863
|
this.hasInjectedChildren = false;
|
|
3653
3864
|
}
|
|
3654
|
-
// Shared swipe-config normalizer — used by constructor, setSwipeToClose,
|
|
3655
|
-
// and setOverlay so the union/object shape is decoded in exactly one place.
|
|
3656
3865
|
assignSwipeFromConfig(swipe) {
|
|
3657
3866
|
if (typeof swipe === "object" && swipe !== null) {
|
|
3658
3867
|
this.swipeToCloseEnabled = swipe.enabled ?? true;
|
|
@@ -3667,6 +3876,22 @@ var OverlayEngine = class {
|
|
|
3667
3876
|
emit(event, payload) {
|
|
3668
3877
|
this.bus.emit(event, payload);
|
|
3669
3878
|
}
|
|
3879
|
+
effectiveDuration() {
|
|
3880
|
+
return this.respectReducedMotionEnabled && prefersReducedMotion() ? 0 : this.duration;
|
|
3881
|
+
}
|
|
3882
|
+
popBackMarker(reason) {
|
|
3883
|
+
if (!this.backPushed) return;
|
|
3884
|
+
this.backPushed = false;
|
|
3885
|
+
if (reason === "back") return;
|
|
3886
|
+
const state = history.state;
|
|
3887
|
+
if (state && state.__bsOverlay === this.id) {
|
|
3888
|
+
try {
|
|
3889
|
+
history.back();
|
|
3890
|
+
} catch {
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3670
3895
|
resolvePeekPx(raw) {
|
|
3671
3896
|
if (typeof raw === "number") return Math.max(0, raw);
|
|
3672
3897
|
try {
|
|
@@ -3688,6 +3913,7 @@ var OverlayEngine = class {
|
|
|
3688
3913
|
this.originalParent = this.element.parentNode;
|
|
3689
3914
|
this.originalNextSibling = this.element.nextSibling;
|
|
3690
3915
|
target.appendChild(this.element);
|
|
3916
|
+
this.movedToTarget = true;
|
|
3691
3917
|
this.mounted = true;
|
|
3692
3918
|
}
|
|
3693
3919
|
restoreMount() {
|
|
@@ -3721,7 +3947,7 @@ var OverlayEngine = class {
|
|
|
3721
3947
|
this.backdrop.style.willChange = "opacity";
|
|
3722
3948
|
}
|
|
3723
3949
|
}
|
|
3724
|
-
awaitTransition(cycle) {
|
|
3950
|
+
awaitTransition(cycle, property = "transform") {
|
|
3725
3951
|
return new Promise((resolve) => {
|
|
3726
3952
|
let done = false;
|
|
3727
3953
|
const finish = () => {
|
|
@@ -3736,10 +3962,10 @@ var OverlayEngine = class {
|
|
|
3736
3962
|
finish();
|
|
3737
3963
|
return;
|
|
3738
3964
|
}
|
|
3739
|
-
if (e.propertyName ===
|
|
3965
|
+
if (e.target === this.element && e.propertyName === property) finish();
|
|
3740
3966
|
};
|
|
3741
3967
|
this.element.addEventListener("transitionend", onEnd);
|
|
3742
|
-
const timer = window.setTimeout(finish, this.
|
|
3968
|
+
const timer = window.setTimeout(finish, this.effectiveDuration() + 50);
|
|
3743
3969
|
});
|
|
3744
3970
|
}
|
|
3745
3971
|
installInteractiveListeners() {
|
|
@@ -3749,7 +3975,10 @@ var OverlayEngine = class {
|
|
|
3749
3975
|
}
|
|
3750
3976
|
if (this.closeOnEscape && !this.detachEscape) {
|
|
3751
3977
|
const handler = (e) => {
|
|
3752
|
-
if (e.
|
|
3978
|
+
if (e.defaultPrevented) return;
|
|
3979
|
+
if (e.key === "Escape" && this.isOpen_ && this.isTop) {
|
|
3980
|
+
void this.close("escape");
|
|
3981
|
+
}
|
|
3753
3982
|
};
|
|
3754
3983
|
document.addEventListener("keydown", handler);
|
|
3755
3984
|
this.detachEscape = () => document.removeEventListener("keydown", handler);
|
|
@@ -3762,10 +3991,14 @@ var OverlayEngine = class {
|
|
|
3762
3991
|
if (this.closeOnBackEnabled && typeof window !== "undefined" && !this.detachHardwareBack) {
|
|
3763
3992
|
try {
|
|
3764
3993
|
history.pushState({ __bsOverlay: this.id }, "");
|
|
3994
|
+
this.backPushed = true;
|
|
3765
3995
|
} catch {
|
|
3766
3996
|
}
|
|
3767
3997
|
const onPop = () => {
|
|
3768
|
-
if (this.isOpen_
|
|
3998
|
+
if (this.isOpen_ && this.isTop) {
|
|
3999
|
+
this.backPushed = false;
|
|
4000
|
+
void this.close("back");
|
|
4001
|
+
}
|
|
3769
4002
|
};
|
|
3770
4003
|
window.addEventListener("popstate", onPop);
|
|
3771
4004
|
this.detachHardwareBack = () => window.removeEventListener("popstate", onPop);
|
|
@@ -3796,55 +4029,57 @@ var OverlayEngine = class {
|
|
|
3796
4029
|
this.detachOutsidePointer = null;
|
|
3797
4030
|
this.lifecycle.release();
|
|
3798
4031
|
}
|
|
3799
|
-
/**
|
|
3800
|
-
* Light pointer-driven dismiss. Tracks finger along the edge axis, follows
|
|
3801
|
-
* with translate, dismisses if past `swipeThreshold` (fraction of panel
|
|
3802
|
-
* size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
|
|
3803
|
-
* Pure CSS-transform during drag — no rAF or physics required since the
|
|
3804
|
-
* panel only moves one direction.
|
|
3805
|
-
*/
|
|
3806
4032
|
installSwipeToClose() {
|
|
3807
4033
|
const el = this.element;
|
|
3808
4034
|
const edge = this.edge;
|
|
3809
4035
|
const isVertical = edge === "bottom" || edge === "top";
|
|
3810
4036
|
const sign = edge === "bottom" || edge === "right" ? 1 : -1;
|
|
3811
|
-
const
|
|
3812
|
-
const velocityThreshold = this.swipeVelocityThreshold;
|
|
4037
|
+
const SLOP = 8;
|
|
3813
4038
|
let startX = 0;
|
|
3814
4039
|
let startY = 0;
|
|
3815
4040
|
let startT = 0;
|
|
4041
|
+
let tracking = false;
|
|
3816
4042
|
let dragging = false;
|
|
3817
4043
|
let pointerId = null;
|
|
4044
|
+
const previousTouchAction = el.style.touchAction;
|
|
4045
|
+
el.style.touchAction = isVertical ? "pan-x" : "pan-y";
|
|
3818
4046
|
const onDown = (e) => {
|
|
3819
4047
|
if (e.button !== void 0 && e.button !== 0) return;
|
|
3820
4048
|
pointerId = e.pointerId;
|
|
3821
4049
|
startX = e.clientX;
|
|
3822
4050
|
startY = e.clientY;
|
|
3823
4051
|
startT = performance.now();
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
try {
|
|
3827
|
-
el.setPointerCapture(e.pointerId);
|
|
3828
|
-
} catch {
|
|
3829
|
-
}
|
|
4052
|
+
tracking = true;
|
|
4053
|
+
dragging = false;
|
|
3830
4054
|
};
|
|
3831
4055
|
const onMove = (e) => {
|
|
3832
|
-
if (!
|
|
4056
|
+
if (!tracking || e.pointerId !== pointerId) return;
|
|
3833
4057
|
const delta = isVertical ? e.clientY - startY : e.clientX - startX;
|
|
4058
|
+
if (!dragging) {
|
|
4059
|
+
if (Math.abs(delta) < SLOP) return;
|
|
4060
|
+
dragging = true;
|
|
4061
|
+
el.style.transition = "none";
|
|
4062
|
+
try {
|
|
4063
|
+
el.setPointerCapture(e.pointerId);
|
|
4064
|
+
} catch {
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
3834
4067
|
const offset = Math.max(0, sign * delta);
|
|
3835
4068
|
el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
|
|
3836
4069
|
};
|
|
3837
4070
|
const finish = (e) => {
|
|
3838
|
-
if (!
|
|
3839
|
-
|
|
4071
|
+
if (!tracking || e.pointerId !== pointerId) return;
|
|
4072
|
+
tracking = false;
|
|
3840
4073
|
pointerId = null;
|
|
4074
|
+
if (!dragging) return;
|
|
4075
|
+
dragging = false;
|
|
3841
4076
|
const dt = performance.now() - startT || 1;
|
|
3842
4077
|
const delta = isVertical ? e.clientY - startY : e.clientX - startX;
|
|
3843
4078
|
const offset = Math.max(0, sign * delta);
|
|
3844
4079
|
const velocity = sign * delta / dt;
|
|
3845
4080
|
const size = isVertical ? el.offsetHeight : el.offsetWidth;
|
|
3846
|
-
const past = offset > size *
|
|
3847
|
-
el.style.transition = `transform ${this.
|
|
4081
|
+
const past = offset > size * this.swipeThreshold || velocity > this.swipeVelocityThreshold;
|
|
4082
|
+
el.style.transition = `transform ${this.effectiveDuration()}ms ${this.exitEasing}`;
|
|
3848
4083
|
if (past) {
|
|
3849
4084
|
void this.close("swipe");
|
|
3850
4085
|
} else {
|
|
@@ -3856,6 +4091,7 @@ var OverlayEngine = class {
|
|
|
3856
4091
|
el.addEventListener("pointerup", finish);
|
|
3857
4092
|
el.addEventListener("pointercancel", finish);
|
|
3858
4093
|
return () => {
|
|
4094
|
+
el.style.touchAction = previousTouchAction;
|
|
3859
4095
|
el.removeEventListener("pointerdown", onDown);
|
|
3860
4096
|
el.removeEventListener("pointermove", onMove);
|
|
3861
4097
|
el.removeEventListener("pointerup", finish);
|
|
@@ -3870,10 +4106,12 @@ var OverlayEngine = class {
|
|
|
3870
4106
|
if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
|
|
3871
4107
|
},
|
|
3872
4108
|
setIsTop: (isTop) => {
|
|
4109
|
+
this.isTop = isTop;
|
|
3873
4110
|
if (this.backdrop) {
|
|
3874
4111
|
this.backdrop.style.display = isTop ? "" : "none";
|
|
3875
4112
|
}
|
|
3876
|
-
}
|
|
4113
|
+
},
|
|
4114
|
+
isOpen: () => this.isOpen_
|
|
3877
4115
|
});
|
|
3878
4116
|
}
|
|
3879
4117
|
};
|
|
@@ -3959,7 +4197,7 @@ var Overlay = react.forwardRef(function Overlay2(props, ref) {
|
|
|
3959
4197
|
if (openProp === void 0) return;
|
|
3960
4198
|
if (openProp && !state.isOpen) void open();
|
|
3961
4199
|
else if (!openProp && state.isOpen) void close();
|
|
3962
|
-
}, [openProp]);
|
|
4200
|
+
}, [openProp, state.isOpen]);
|
|
3963
4201
|
react.useEffect(() => {
|
|
3964
4202
|
onChange?.(state);
|
|
3965
4203
|
}, [state, onChange]);
|
|
@@ -4091,46 +4329,34 @@ var BottomSheetFromConfig = react.forwardRef(function BottomSheetFromConfig2(pro
|
|
|
4091
4329
|
() => sheetRef.current,
|
|
4092
4330
|
[]
|
|
4093
4331
|
);
|
|
4094
|
-
const
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
const
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
const hs = handlersRef.current;
|
|
4101
|
-
if (evs.onSnap) {
|
|
4102
|
-
const fn = hs[evs.onSnap];
|
|
4103
|
-
if (typeof fn === "function") {
|
|
4104
|
-
fn(next);
|
|
4105
|
-
} else {
|
|
4106
|
-
console.warn(
|
|
4107
|
-
`[BottomSheetFromConfig] eventHandlers["${evs.onSnap}"] is not a function; onSnap is a no-op.`
|
|
4108
|
-
);
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
|
-
const isClosedNext = next === "" || next === "closed";
|
|
4112
|
-
const isClosedPrev = prev === null || prev === "" || prev === "closed";
|
|
4113
|
-
if (isClosedPrev && !isClosedNext && evs.onOpen) {
|
|
4114
|
-
const fn = hs[evs.onOpen];
|
|
4115
|
-
if (typeof fn === "function") {
|
|
4116
|
-
fn(next);
|
|
4117
|
-
} else {
|
|
4118
|
-
console.warn(
|
|
4119
|
-
`[BottomSheetFromConfig] eventHandlers["${evs.onOpen}"] is not a function; onOpen is a no-op.`
|
|
4120
|
-
);
|
|
4121
|
-
}
|
|
4122
|
-
}
|
|
4123
|
-
if (!isClosedPrev && isClosedNext && evs.onClose) {
|
|
4124
|
-
const fn = hs[evs.onClose];
|
|
4125
|
-
if (typeof fn === "function") {
|
|
4332
|
+
const invoke = (key, arg) => {
|
|
4333
|
+
const evName = eventsRef.current[key];
|
|
4334
|
+
if (!evName) return;
|
|
4335
|
+
const fn = handlersRef.current[evName];
|
|
4336
|
+
if (typeof fn === "function") {
|
|
4337
|
+
if (arg === void 0) {
|
|
4126
4338
|
fn();
|
|
4127
4339
|
} else {
|
|
4128
|
-
|
|
4129
|
-
`[BottomSheetFromConfig] eventHandlers["${evs.onClose}"] is not a function; onClose is a no-op.`
|
|
4130
|
-
);
|
|
4340
|
+
fn(arg);
|
|
4131
4341
|
}
|
|
4342
|
+
} else {
|
|
4343
|
+
console.warn(
|
|
4344
|
+
`[BottomSheetFromConfig] eventHandlers["${evName}"] is not a function; ${key} is a no-op.`
|
|
4345
|
+
);
|
|
4132
4346
|
}
|
|
4133
4347
|
};
|
|
4348
|
+
const invokeRef = react.useRef(invoke);
|
|
4349
|
+
invokeRef.current = invoke;
|
|
4350
|
+
react.useEffect(() => {
|
|
4351
|
+
const engine = sheetRef.current?.getEngine();
|
|
4352
|
+
if (!engine) return;
|
|
4353
|
+
const offs = [
|
|
4354
|
+
engine.on("snap", (p) => invokeRef.current("onSnap", p.id)),
|
|
4355
|
+
engine.on("open", (p) => invokeRef.current("onOpen", p.id)),
|
|
4356
|
+
engine.on("close", () => invokeRef.current("onClose"))
|
|
4357
|
+
];
|
|
4358
|
+
return () => offs.forEach((off) => off());
|
|
4359
|
+
}, []);
|
|
4134
4360
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4135
4361
|
BottomSheet,
|
|
4136
4362
|
{
|
|
@@ -4146,7 +4372,6 @@ var BottomSheetFromConfig = react.forwardRef(function BottomSheetFromConfig2(pro
|
|
|
4146
4372
|
closeOnBackdrop: derived.behavior.closeOnBackdrop,
|
|
4147
4373
|
lockBodyScroll: derived.behavior.lockBodyScroll,
|
|
4148
4374
|
rubberBand: derived.behavior.rubberBand,
|
|
4149
|
-
onChange: handleChange,
|
|
4150
4375
|
header: slotContent?.header,
|
|
4151
4376
|
children: slotContent?.body
|
|
4152
4377
|
}
|