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