@surdeddd/bottom-sheet 0.8.1 → 0.9.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.
Files changed (46) hide show
  1. package/dist/{BottomSheetEngine-7BDK2OWX.d.cts → BottomSheetEngine-BcyruNP1.d.cts} +12 -6
  2. package/dist/{BottomSheetEngine-BFTb0-4Z.d.ts → BottomSheetEngine-CBRcBwV-.d.ts} +12 -6
  3. package/dist/element.cjs +530 -349
  4. package/dist/element.d.cts +2 -1
  5. package/dist/element.d.ts +2 -1
  6. package/dist/element.iife.global.js +2 -2
  7. package/dist/element.js +530 -349
  8. package/dist/index.cjs +438 -294
  9. package/dist/index.d.cts +3 -3
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +438 -294
  12. package/dist/integrations/formik.d.cts +1 -1
  13. package/dist/integrations/formik.d.ts +1 -1
  14. package/dist/integrations/react-hook-form.d.cts +1 -1
  15. package/dist/integrations/react-hook-form.d.ts +1 -1
  16. package/dist/overlay.cjs +160 -96
  17. package/dist/overlay.d.cts +2 -0
  18. package/dist/overlay.d.ts +2 -0
  19. package/dist/overlay.js +160 -96
  20. package/dist/preact.d.cts +1 -1
  21. package/dist/preact.d.ts +1 -1
  22. package/dist/qwik.cjs +387 -211
  23. package/dist/qwik.d.cts +4 -3
  24. package/dist/qwik.d.ts +4 -3
  25. package/dist/qwik.js +387 -211
  26. package/dist/react.cjs +472 -296
  27. package/dist/react.d.cts +3 -2
  28. package/dist/react.d.ts +3 -2
  29. package/dist/react.js +472 -296
  30. package/dist/solid.cjs +417 -215
  31. package/dist/solid.d.cts +21 -7
  32. package/dist/solid.d.ts +21 -7
  33. package/dist/solid.js +417 -215
  34. package/dist/solid.source.jsx +412 -209
  35. package/dist/svelte-core.cjs +328 -209
  36. package/dist/svelte-core.js +328 -209
  37. package/dist/svelte-src/BottomSheet.svelte +56 -0
  38. package/dist/svelte.cjs +365 -210
  39. package/dist/svelte.d.cts +4 -0
  40. package/dist/svelte.d.ts +4 -0
  41. package/dist/svelte.js +365 -210
  42. package/dist/vue.cjs +365 -217
  43. package/dist/vue.d.cts +6 -0
  44. package/dist/vue.d.ts +6 -0
  45. package/dist/vue.js +365 -217
  46. package/package.json +6 -6
package/dist/vue.js CHANGED
@@ -264,18 +264,25 @@ var SnapResolver = class {
264
264
  __publicField(this, "resolved", []);
265
265
  __publicField(this, "maxAxisSize", 0);
266
266
  __publicField(this, "rangeCache", null);
267
+ __publicField(this, "notified", false);
267
268
  this.raw = raw;
268
269
  this.allowedIds = allowed ?? raw.map((p) => p.id);
269
270
  this.recompute();
270
271
  }
271
272
  recompute() {
272
- this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
273
- this.maxAxisSize = this.resolved.reduce(
273
+ const measure = this.measureFit;
274
+ let measured;
275
+ const measureOnce = measure ? () => measured ?? (measured = measure()) : void 0;
276
+ this.resolved = resolveSnapList(this.raw, this.mode, measureOnce);
277
+ const nextMax = this.resolved.reduce(
274
278
  (m, s) => s.size > m ? s.size : m,
275
279
  0
276
280
  );
277
281
  this.rangeCache = null;
278
- this.onMaxAxisSizeChange?.(this.maxAxisSize);
282
+ const changed = nextMax !== this.maxAxisSize || !this.notified;
283
+ this.maxAxisSize = nextMax;
284
+ this.notified = true;
285
+ if (changed) this.onMaxAxisSizeChange?.(this.maxAxisSize);
279
286
  }
280
287
  setRaw(raw) {
281
288
  this.raw = raw;
@@ -428,7 +435,10 @@ var ensureHandler = () => {
428
435
  target.consumed = true;
429
436
  const idx = closers.indexOf(target);
430
437
  if (idx !== -1) closers.splice(idx, 1);
431
- target.close();
438
+ const settled = target;
439
+ void Promise.resolve(settled.close()).then(() => {
440
+ if (settled.isOpen()) settled.restore();
441
+ });
432
442
  });
433
443
  };
434
444
  var programmaticBack = () => {
@@ -464,16 +474,7 @@ function installRoute(deps) {
464
474
  pushed = false;
465
475
  }
466
476
  };
467
- const onOpen = () => {
468
- if (closer) return;
469
- closer = {
470
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
471
- isTop: () => deps.isTopSheet(),
472
- close: () => void deps.close(),
473
- consumed: false
474
- };
475
- ensureHandler();
476
- closers.push(closer);
477
+ const pushEntry = () => {
477
478
  try {
478
479
  if (deps.routedTo !== void 0) {
479
480
  history.pushState({ __bsRouted: deps.sheetId }, "", deps.routedTo);
@@ -485,6 +486,25 @@ function installRoute(deps) {
485
486
  pushed = false;
486
487
  }
487
488
  };
489
+ const onOpen = () => {
490
+ if (closer) return;
491
+ const self = {
492
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
493
+ isTop: () => deps.isTopSheet(),
494
+ close: () => deps.close(),
495
+ restore: () => {
496
+ if (deps.isDestroyed() || closer !== self) return;
497
+ self.consumed = false;
498
+ if (!closers.includes(self)) closers.push(self);
499
+ pushEntry();
500
+ },
501
+ consumed: false
502
+ };
503
+ closer = self;
504
+ ensureHandler();
505
+ closers.push(self);
506
+ pushEntry();
507
+ };
488
508
  const unsubscribeOpen = deps.on("open", onOpen);
489
509
  const unsubscribeClose = deps.on("close", teardownEntry);
490
510
  if (deps.getSize() > 0) onOpen();
@@ -531,11 +551,13 @@ var CONTENT_PULL_THRESHOLD = 60;
531
551
  function installContentSwipe(deps) {
532
552
  const { container } = deps;
533
553
  let startY = 0;
554
+ let startScrollTop = 0;
534
555
  let moved = false;
535
556
  const onTouchStart = (e) => {
536
557
  const t = e.touches[0];
537
558
  if (!t) return;
538
559
  startY = t.clientY;
560
+ startScrollTop = container.scrollTop;
539
561
  moved = false;
540
562
  };
541
563
  const onTouchMove = (e) => {
@@ -546,6 +568,7 @@ function installContentSwipe(deps) {
546
568
  const onTouchEnd = (e) => {
547
569
  if (!moved) return;
548
570
  if (deps.isDragging() || deps.isAnimating()) return;
571
+ if (container.scrollTop !== startScrollTop) return;
549
572
  const t = e.changedTouches[0];
550
573
  if (!t) return;
551
574
  const deltaY = t.clientY - startY;
@@ -657,6 +680,131 @@ function installResizeObserver(deps) {
657
680
  };
658
681
  }
659
682
 
683
+ // src/core/features/fit-observer.ts
684
+ var contentTargets = (scroller) => {
685
+ if (!scroller) return [];
686
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
687
+ const out = [];
688
+ for (const child of Array.from(scroller.children)) {
689
+ if (hasSlot && child instanceof HTMLSlotElement) {
690
+ for (const el of child.assignedElements()) out.push(el);
691
+ } else {
692
+ out.push(child);
693
+ }
694
+ }
695
+ return out;
696
+ };
697
+ function installFitObserver(deps) {
698
+ if (typeof ResizeObserver === "undefined") return () => {
699
+ };
700
+ if (!deps.hasFitSnap()) return () => {
701
+ };
702
+ let raf = 0;
703
+ const schedule = () => {
704
+ if (raf) return;
705
+ raf = requestAnimationFrame(() => {
706
+ raf = 0;
707
+ if (deps.isDestroyed() || deps.isDragging()) return;
708
+ deps.recompute();
709
+ });
710
+ };
711
+ const ro = new ResizeObserver(schedule);
712
+ ro.observe(deps.handle);
713
+ const scroller = deps.scrollContainer;
714
+ const observed = /* @__PURE__ */ new Set();
715
+ const slots = /* @__PURE__ */ new Set();
716
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
717
+ const onSlotChange = () => resync();
718
+ const trackSlots = () => {
719
+ if (!scroller || !hasSlot) return;
720
+ for (const child of Array.from(scroller.children)) {
721
+ if (child instanceof HTMLSlotElement && !slots.has(child)) {
722
+ child.addEventListener("slotchange", onSlotChange);
723
+ slots.add(child);
724
+ }
725
+ }
726
+ };
727
+ const resync = () => {
728
+ trackSlots();
729
+ const targets = contentTargets(scroller);
730
+ for (const el of observed) {
731
+ if (!targets.includes(el)) {
732
+ ro.unobserve(el);
733
+ observed.delete(el);
734
+ }
735
+ }
736
+ for (const el of targets) {
737
+ if (!observed.has(el)) {
738
+ ro.observe(el);
739
+ observed.add(el);
740
+ }
741
+ }
742
+ schedule();
743
+ };
744
+ let mo;
745
+ if (scroller) {
746
+ resync();
747
+ if (typeof MutationObserver !== "undefined") {
748
+ mo = new MutationObserver(resync);
749
+ mo.observe(scroller, { childList: true });
750
+ }
751
+ }
752
+ return () => {
753
+ ro.disconnect();
754
+ mo?.disconnect();
755
+ for (const slot of slots) {
756
+ slot.removeEventListener("slotchange", onSlotChange);
757
+ }
758
+ if (raf) cancelAnimationFrame(raf);
759
+ };
760
+ }
761
+
762
+ // src/core/features/fit-measurement.ts
763
+ var measureSheetNatural = (element, content, vertical) => {
764
+ if (!content) {
765
+ return vertical ? element.offsetHeight : element.offsetWidth;
766
+ }
767
+ const axis = vertical ? "height" : "width";
768
+ const sheetStyle = element.style;
769
+ const cs = content.style;
770
+ const prevSheet = sheetStyle[axis];
771
+ const prevFlex = cs.flex;
772
+ const prevContent = vertical ? cs.height : cs.width;
773
+ sheetStyle[axis] = "auto";
774
+ cs.flex = "none";
775
+ if (vertical) cs.height = "auto";
776
+ else cs.width = "auto";
777
+ const poked = vertical ? element.offsetHeight : element.offsetWidth;
778
+ const contentBox = vertical ? content.clientHeight : content.clientWidth;
779
+ const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
780
+ const natural = Math.max(poked, poked - contentBox + contentScroll);
781
+ sheetStyle[axis] = prevSheet;
782
+ cs.flex = prevFlex;
783
+ if (vertical) cs.height = prevContent;
784
+ else cs.width = prevContent;
785
+ return natural;
786
+ };
787
+ var containingExtent = (element, vertical) => {
788
+ const op = element.offsetParent;
789
+ if (op && op !== document.body && op !== document.documentElement) {
790
+ return vertical ? op.clientHeight : op.clientWidth;
791
+ }
792
+ return vertical ? window.innerHeight : window.innerWidth;
793
+ };
794
+ var measureFitSize = (deps) => {
795
+ const vertical = layoutAxis(deps.mode) === "height";
796
+ const natural = measureSheetNatural(
797
+ deps.element,
798
+ deps.scrollContainer,
799
+ vertical
800
+ );
801
+ const cap = deps.getMaxHeightCap();
802
+ const capped = cap !== void 0 ? Math.min(natural, cap) : natural;
803
+ if (typeof window === "undefined") return capped;
804
+ const viewport = containingExtent(deps.element, vertical);
805
+ return viewport > 0 ? Math.min(capped, viewport) : capped;
806
+ };
807
+
660
808
  // src/core/features/slider-keyboard.ts
661
809
  function installSliderKeyboard(deps) {
662
810
  const isVerticalAxis = deps.mode === "bottom" || deps.mode === "top";
@@ -1143,6 +1291,9 @@ var ScrimController = class {
1143
1291
  const s = this.screenComponent.style;
1144
1292
  s.opacity = "";
1145
1293
  s.display = "";
1294
+ s.position = "";
1295
+ s.inset = "";
1296
+ s.pointerEvents = "";
1146
1297
  }
1147
1298
  if (this.backdrop) {
1148
1299
  this.backdrop.style.opacity = "";
@@ -1391,7 +1542,12 @@ var AnimationRunner = class {
1391
1542
  this.currentSpring?.cancel();
1392
1543
  }
1393
1544
  async animateTo(target, velocityPxPerMs) {
1394
- if (target === this.getSize()) return;
1545
+ if (target === this.getSize()) {
1546
+ if (!this.isAnimating && !this.isDragging()) {
1547
+ this.element.style.willChange = "";
1548
+ }
1549
+ return;
1550
+ }
1395
1551
  this.currentTween?.cancel();
1396
1552
  this.currentSpring?.cancel();
1397
1553
  this.getRootEl()?.setAttribute("data-animating", "true");
@@ -1399,7 +1555,7 @@ var AnimationRunner = class {
1399
1555
  if (this.reducedMotion) {
1400
1556
  this.applySizeFn(target);
1401
1557
  this.getRootEl()?.removeAttribute("data-animating");
1402
- if (!this.isDragging()) this.element.style.willChange = "auto";
1558
+ if (!this.isDragging()) this.element.style.willChange = "";
1403
1559
  return;
1404
1560
  }
1405
1561
  if (this.animationKind === "spring") {
@@ -1427,7 +1583,7 @@ var AnimationRunner = class {
1427
1583
  }
1428
1584
  if (!this.isAnimating) {
1429
1585
  this.getRootEl()?.removeAttribute("data-animating");
1430
- if (!this.isDragging()) this.element.style.willChange = "auto";
1586
+ if (!this.isDragging()) this.element.style.willChange = "";
1431
1587
  }
1432
1588
  }
1433
1589
  destroy() {
@@ -1458,9 +1614,35 @@ var isVisible = (el) => {
1458
1614
  }
1459
1615
  return el.offsetParent !== null;
1460
1616
  };
1461
- var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1617
+ var collectFocusables = (root) => {
1618
+ const out = Array.from(root.querySelectorAll(FOCUSABLE));
1619
+ if (typeof HTMLSlotElement !== "undefined") {
1620
+ for (const slot of Array.from(root.querySelectorAll("slot"))) {
1621
+ for (const assigned of slot.assignedElements()) {
1622
+ if (assigned.matches(FOCUSABLE)) out.push(assigned);
1623
+ out.push(...collectFocusables(assigned));
1624
+ }
1625
+ }
1626
+ }
1627
+ return out;
1628
+ };
1629
+ var focusables = (root) => collectFocusables(root).filter(
1462
1630
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1463
1631
  );
1632
+ var containsComposed = (container, node) => {
1633
+ let n = node;
1634
+ while (n) {
1635
+ if (n === container) return true;
1636
+ const slot = n.assignedSlot;
1637
+ if (slot) {
1638
+ n = slot;
1639
+ continue;
1640
+ }
1641
+ const parent = n.parentNode;
1642
+ n = parent instanceof ShadowRoot ? parent.host : parent;
1643
+ }
1644
+ return false;
1645
+ };
1464
1646
  var trapStack = [];
1465
1647
  var installFocusTrap = (container, options = {}) => {
1466
1648
  if (typeof document === "undefined") return () => {
@@ -1470,7 +1652,13 @@ var installFocusTrap = (container, options = {}) => {
1470
1652
  const isActive = () => trapStack[trapStack.length - 1] === token;
1471
1653
  const previouslyFocused = document.activeElement;
1472
1654
  const focusInitial = () => {
1473
- const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
1655
+ const spec = options.initialFocus;
1656
+ if (spec === false) {
1657
+ if (!container.hasAttribute("tabindex")) container.tabIndex = -1;
1658
+ container.focus({ preventScroll: true });
1659
+ return;
1660
+ }
1661
+ const initial = typeof spec === "string" ? container.querySelector(spec) : spec ?? focusables(container)[0] ?? container;
1474
1662
  initial?.focus({ preventScroll: true });
1475
1663
  };
1476
1664
  focusInitial();
@@ -1500,7 +1688,7 @@ var installFocusTrap = (container, options = {}) => {
1500
1688
  const handleFocusIn = (e) => {
1501
1689
  if (!isActive()) return;
1502
1690
  const target = e.target;
1503
- if (target && !container.contains(target)) {
1691
+ if (target && !containsComposed(container, target)) {
1504
1692
  const list = focusables(container);
1505
1693
  list[0]?.focus({ preventScroll: true });
1506
1694
  }
@@ -1537,8 +1725,14 @@ var lockBodyScroll = () => {
1537
1725
  position: body.style.position,
1538
1726
  top: body.style.top,
1539
1727
  width: body.style.width,
1728
+ paddingRight: body.style.paddingRight,
1540
1729
  scrollY: window.scrollY
1541
1730
  };
1731
+ const scrollbarGap = window.innerWidth - document.documentElement.clientWidth;
1732
+ if (scrollbarGap > 0) {
1733
+ const current = parseFloat(getComputedStyle(body).paddingRight) || 0;
1734
+ body.style.paddingRight = `${current + scrollbarGap}px`;
1735
+ }
1542
1736
  body.style.overflow = "hidden";
1543
1737
  body.style.position = "fixed";
1544
1738
  body.style.top = `-${savedStyles.scrollY}px`;
@@ -1556,6 +1750,7 @@ var lockBodyScroll = () => {
1556
1750
  body.style.position = savedStyles.position;
1557
1751
  body.style.top = savedStyles.top;
1558
1752
  body.style.width = savedStyles.width;
1753
+ body.style.paddingRight = savedStyles.paddingRight;
1559
1754
  window.scrollTo(0, savedStyles.scrollY);
1560
1755
  savedStyles = null;
1561
1756
  }
@@ -1768,12 +1963,14 @@ var installGestures = (handle, mode, callbacks) => {
1768
1963
  handle.addEventListener("pointermove", onPointerMove);
1769
1964
  handle.addEventListener("pointerup", finishGesture);
1770
1965
  handle.addEventListener("pointercancel", onPointerCancel);
1966
+ const prevTouchAction = handle.style.touchAction;
1771
1967
  handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1772
1968
  return () => {
1773
1969
  handle.removeEventListener("pointerdown", onPointerDown);
1774
1970
  handle.removeEventListener("pointermove", onPointerMove);
1775
1971
  handle.removeEventListener("pointerup", finishGesture);
1776
1972
  handle.removeEventListener("pointercancel", onPointerCancel);
1973
+ handle.style.touchAction = prevTouchAction;
1777
1974
  if (activePointerId !== null && handle.hasPointerCapture(activePointerId)) {
1778
1975
  handle.releasePointerCapture(activePointerId);
1779
1976
  }
@@ -1900,7 +2097,7 @@ var GestureController = class {
1900
2097
  this.isDragging_ = false;
1901
2098
  this.keyboardDismissed = false;
1902
2099
  this.deps.getRoot()?.removeAttribute("data-dragging");
1903
- this.deps.element.style.willChange = "auto";
2100
+ this.deps.element.style.willChange = "";
1904
2101
  }
1905
2102
  };
1906
2103
 
@@ -2299,8 +2496,7 @@ function resolveEngineOptions(opts, extras) {
2299
2496
  };
2300
2497
  }
2301
2498
 
2302
- // src/core/BottomSheetEngine.ts
2303
- var HAPTIC_DURATION_MS = 8;
2499
+ // src/core/primitives/teardown-stack.ts
2304
2500
  var TeardownStack = class {
2305
2501
  constructor() {
2306
2502
  __publicField(this, "fns", []);
@@ -2320,6 +2516,9 @@ var TeardownStack = class {
2320
2516
  }
2321
2517
  }
2322
2518
  };
2519
+
2520
+ // src/core/BottomSheetEngine.ts
2521
+ var HAPTIC_DURATION_MS = 8;
2323
2522
  var BottomSheetEngine = class {
2324
2523
  constructor(opts) {
2325
2524
  __publicField(this, "id", nextInstanceId("bs"));
@@ -2344,6 +2543,7 @@ var BottomSheetEngine = class {
2344
2543
  __publicField(this, "disableDragFlag");
2345
2544
  __publicField(this, "closeOnRouteChange");
2346
2545
  __publicField(this, "maxHeightCap");
2546
+ __publicField(this, "maxHeightRaw");
2347
2547
  __publicField(this, "snapPointsRaw");
2348
2548
  __publicField(this, "snaps");
2349
2549
  __publicField(this, "activeId");
@@ -2379,6 +2579,7 @@ var BottomSheetEngine = class {
2379
2579
  __publicField(this, "detachScrimStages", null);
2380
2580
  __publicField(this, "stackEffectEnabled", false);
2381
2581
  __publicField(this, "stackEffectPrimed", false);
2582
+ __publicField(this, "fitDeps");
2382
2583
  __publicField(this, "transformTemplate");
2383
2584
  const resolved = resolveEngineOptions(opts, {
2384
2585
  shouldApplyInertSiblings: () => {
@@ -2451,12 +2652,19 @@ var BottomSheetEngine = class {
2451
2652
  }
2452
2653
  this.activeId = resolved.initialId;
2453
2654
  auditVhUsage(this.snapPointsRaw);
2655
+ this.auditDuplicateSnapIds(this.snapPointsRaw);
2454
2656
  this.rootEl = this.element.closest(".bs-root");
2657
+ this.fitDeps = {
2658
+ element: this.element,
2659
+ scrollContainer: this.scrollContainer,
2660
+ mode: this.mode,
2661
+ getMaxHeightCap: () => this.maxHeightCap
2662
+ };
2455
2663
  this.snaps = new SnapResolver(
2456
2664
  this.snapPointsRaw,
2457
2665
  resolved.initialAllowed,
2458
2666
  this.mode,
2459
- () => this.measureFitSize(),
2667
+ () => measureFitSize(this.fitDeps),
2460
2668
  (maxAxisSize) => {
2461
2669
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2462
2670
  }
@@ -2490,7 +2698,18 @@ var BottomSheetEngine = class {
2490
2698
  io.observe(this.element);
2491
2699
  this.teardowns.add(() => io.disconnect());
2492
2700
  }
2493
- this.installFitObserver();
2701
+ this.teardowns.add(
2702
+ installFitObserver({
2703
+ handle: this.handle,
2704
+ scrollContainer: this.scrollContainer,
2705
+ hasFitSnap: () => this.snapPointsRaw.some(
2706
+ (p) => p.size === "fit" || p.size === "content"
2707
+ ),
2708
+ isDestroyed: () => this.destroyed,
2709
+ isDragging: () => this.gesture?.isDragging ?? false,
2710
+ recompute: () => this.recompute()
2711
+ })
2712
+ );
2494
2713
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2495
2714
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2496
2715
  this.applyAutoAriaLabelledBy();
@@ -2641,27 +2860,33 @@ var BottomSheetEngine = class {
2641
2860
  } else {
2642
2861
  await this.animation.animateTo(target.size, velocityPxPerMs);
2643
2862
  }
2644
- this.opening = false;
2645
2863
  externalSignal?.removeEventListener("abort", onExternalAbort);
2646
2864
  if (signal.aborted) return;
2647
- this.scrollCache.restore(id, previousSize, target.size);
2865
+ this.opening = false;
2866
+ this.completeSnap(id, target.size, previousSize, wasClosed, false);
2867
+ }
2868
+ completeSnap(id, targetSize, previousSize, wasClosed, withHaptic) {
2869
+ this.scrollCache.restore(id, previousSize, targetSize);
2648
2870
  this.updateAriaSlider();
2649
2871
  this.emit("snap", {
2650
2872
  id,
2651
2873
  size: this.size,
2652
2874
  progress: this.computeProgress(this.size)
2653
2875
  });
2654
- if (wasClosed && target.size > 0) {
2655
- this.emit("open", { id });
2656
- this.handleOpen();
2657
- notifyLinkedSheets(this.linkedSheets, this);
2658
- this.emit("opened", { id });
2659
- }
2660
- if (target.size === 0) {
2661
- this.emit("close", void 0);
2662
- this.handleClose();
2663
- this.emit("closed", void 0);
2664
- }
2876
+ if (withHaptic) this.haptic();
2877
+ if (wasClosed && targetSize > 0) this.emitOpenSequence(id);
2878
+ if (targetSize === 0) this.emitCloseSequence();
2879
+ }
2880
+ emitOpenSequence(id) {
2881
+ this.emit("open", { id });
2882
+ this.handleOpen();
2883
+ notifyLinkedSheets(this.linkedSheets, this);
2884
+ this.emit("opened", { id });
2885
+ }
2886
+ emitCloseSequence() {
2887
+ this.emit("close", void 0);
2888
+ this.handleClose();
2889
+ this.emit("closed", void 0);
2665
2890
  }
2666
2891
  async dragTo(targetSize, velocityOrOpts = 0) {
2667
2892
  if (this.destroyed) return;
@@ -2682,19 +2907,31 @@ var BottomSheetEngine = class {
2682
2907
  }
2683
2908
  open(id) {
2684
2909
  if (this.destroyed) return Promise.resolve();
2685
- const target = id ?? this.snaps.getAllowedIds().find((a) => a !== "closed") ?? this.activeId;
2910
+ const target = id ?? this.snaps.getAllowedIds().find((a) => (this.snaps.findById(a)?.size ?? 0) > 0) ?? this.activeId;
2686
2911
  return this.snapTo(target);
2687
2912
  }
2688
2913
  close(reason = "programmatic") {
2689
2914
  if (this.destroyed) return Promise.resolve();
2690
2915
  if (this.disableCloseFlag) return Promise.resolve();
2691
2916
  if (this.size > 0 && this.emitBeforeClose(reason)) return Promise.resolve();
2692
- const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds()[0];
2917
+ const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds().find((a) => this.snaps.findById(a)?.size === 0) ?? this.snaps.getAllowedIds()[0];
2693
2918
  return this.snapTo(closedId ?? this.activeId);
2694
2919
  }
2695
2920
  canDismiss() {
2696
2921
  return !this.persistent && !this.disableCloseFlag && !this.destroyed;
2697
2922
  }
2923
+ setPersistent(value) {
2924
+ if (this.destroyed) return;
2925
+ this.persistent = value;
2926
+ }
2927
+ setDisableClose(value) {
2928
+ if (this.destroyed) return;
2929
+ this.disableCloseFlag = value;
2930
+ }
2931
+ setDisableDrag(value) {
2932
+ if (this.destroyed) return;
2933
+ this.disableDragFlag = value;
2934
+ }
2698
2935
  isTop() {
2699
2936
  return this.isTopSheet;
2700
2937
  }
@@ -2731,18 +2968,23 @@ var BottomSheetEngine = class {
2731
2968
  axis === "height" ? "max-height" : "max-width",
2732
2969
  value
2733
2970
  );
2734
- if (typeof h === "number") {
2735
- this.maxHeightCap = h;
2736
- this.recompute();
2737
- } else if (typeof window !== "undefined") {
2738
- const probe2 = document.createElement("div");
2739
- probe2.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;${axis === "height" ? "height" : "width"}:${value};`;
2740
- document.body.appendChild(probe2);
2741
- const measured = axis === "height" ? probe2.offsetHeight : probe2.offsetWidth;
2742
- probe2.remove();
2743
- this.maxHeightCap = measured > 0 ? measured : void 0;
2744
- this.recompute();
2971
+ this.maxHeightRaw = h;
2972
+ this.resolveMaxHeightCap();
2973
+ this.recompute();
2974
+ }
2975
+ resolveMaxHeightCap() {
2976
+ const raw = this.maxHeightRaw;
2977
+ if (raw === void 0) {
2978
+ this.maxHeightCap = void 0;
2979
+ return;
2745
2980
  }
2981
+ if (typeof raw === "number") {
2982
+ this.maxHeightCap = raw;
2983
+ return;
2984
+ }
2985
+ if (typeof window === "undefined") return;
2986
+ const measured = resolveSnap(raw, this.mode);
2987
+ this.maxHeightCap = measured > 0 ? measured : void 0;
2746
2988
  }
2747
2989
  clampToMaxHeight() {
2748
2990
  if (this.maxHeightCap === void 0) return;
@@ -2873,6 +3115,7 @@ var BottomSheetEngine = class {
2873
3115
  this.newCycle();
2874
3116
  this.snapPointsRaw = points;
2875
3117
  auditVhUsage(points);
3118
+ this.auditDuplicateSnapIds(points);
2876
3119
  this.snaps.setRaw(points);
2877
3120
  if (allowed) this.snaps.setAllowedIds(allowed);
2878
3121
  this.scrollCache.clear();
@@ -2885,6 +3128,7 @@ var BottomSheetEngine = class {
2885
3128
  }
2886
3129
  recompute() {
2887
3130
  if (this.destroyed) return;
3131
+ this.resolveMaxHeightCap();
2888
3132
  this.snaps.recompute();
2889
3133
  this.clampToMaxHeight();
2890
3134
  this.scrim.invalidateOpacityCache();
@@ -2896,129 +3140,6 @@ var BottomSheetEngine = class {
2896
3140
  this.updateAriaSlider();
2897
3141
  }
2898
3142
  }
2899
- contentTargets() {
2900
- const scroller = this.scrollContainer;
2901
- if (!scroller) return [];
2902
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2903
- const out = [];
2904
- for (const child of Array.from(scroller.children)) {
2905
- if (hasSlot && child instanceof HTMLSlotElement) {
2906
- for (const el of child.assignedElements()) out.push(el);
2907
- } else {
2908
- out.push(child);
2909
- }
2910
- }
2911
- return out;
2912
- }
2913
- measureSheetNatural(vertical) {
2914
- const el = this.element;
2915
- const content = this.scrollContainer;
2916
- if (!content) {
2917
- return vertical ? el.offsetHeight : el.offsetWidth;
2918
- }
2919
- const axis = vertical ? "height" : "width";
2920
- const sheetStyle = el.style;
2921
- const cs = content.style;
2922
- const prevSheet = sheetStyle[axis];
2923
- const prevFlex = cs.flex;
2924
- const prevContent = vertical ? cs.height : cs.width;
2925
- sheetStyle[axis] = "auto";
2926
- cs.flex = "none";
2927
- if (vertical) cs.height = "auto";
2928
- else cs.width = "auto";
2929
- const poked = vertical ? el.offsetHeight : el.offsetWidth;
2930
- const contentBox = vertical ? content.clientHeight : content.clientWidth;
2931
- const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
2932
- const natural = Math.max(poked, poked - contentBox + contentScroll);
2933
- sheetStyle[axis] = prevSheet;
2934
- cs.flex = prevFlex;
2935
- if (vertical) cs.height = prevContent;
2936
- else cs.width = prevContent;
2937
- return natural;
2938
- }
2939
- measureFitSize() {
2940
- const vertical = layoutAxis(this.mode) === "height";
2941
- const natural = this.measureSheetNatural(vertical);
2942
- const capped = this.maxHeightCap !== void 0 ? Math.min(natural, this.maxHeightCap) : natural;
2943
- if (typeof window === "undefined") return capped;
2944
- const viewport = this.containingExtent(vertical);
2945
- return viewport > 0 ? Math.min(capped, viewport) : capped;
2946
- }
2947
- // The fit cap is the sheet's containing block, not always the window: a
2948
- // position:fixed sheet nested under a transform/filter/will-change ancestor
2949
- // is clipped to that ancestor, so capping by window.innerHeight oversizes the
2950
- // sheet and the content/footer get clipped. offsetParent resolves to that
2951
- // ancestor when present, and is null (→ viewport) for a plain fixed sheet.
2952
- containingExtent(vertical) {
2953
- const op = this.element.offsetParent;
2954
- if (op && op !== document.body && op !== document.documentElement) {
2955
- return vertical ? op.clientHeight : op.clientWidth;
2956
- }
2957
- return vertical ? window.innerHeight : window.innerWidth;
2958
- }
2959
- installFitObserver() {
2960
- if (typeof ResizeObserver === "undefined") return;
2961
- if (!this.snapPointsRaw.some((p) => p.size === "fit" || p.size === "content"))
2962
- return;
2963
- let raf = 0;
2964
- const schedule = () => {
2965
- if (raf) return;
2966
- raf = requestAnimationFrame(() => {
2967
- raf = 0;
2968
- if (this.destroyed || this.gesture?.isDragging) return;
2969
- this.recompute();
2970
- });
2971
- };
2972
- const ro = new ResizeObserver(schedule);
2973
- ro.observe(this.handle);
2974
- const scroller = this.scrollContainer;
2975
- const observed = /* @__PURE__ */ new Set();
2976
- const slots = /* @__PURE__ */ new Set();
2977
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2978
- const onSlotChange = () => resync();
2979
- const trackSlots = () => {
2980
- if (!scroller || !hasSlot) return;
2981
- for (const child of Array.from(scroller.children)) {
2982
- if (child instanceof HTMLSlotElement && !slots.has(child)) {
2983
- child.addEventListener("slotchange", onSlotChange);
2984
- slots.add(child);
2985
- }
2986
- }
2987
- };
2988
- const resync = () => {
2989
- trackSlots();
2990
- const targets = this.contentTargets();
2991
- for (const el of observed) {
2992
- if (!targets.includes(el)) {
2993
- ro.unobserve(el);
2994
- observed.delete(el);
2995
- }
2996
- }
2997
- for (const el of targets) {
2998
- if (!observed.has(el)) {
2999
- ro.observe(el);
3000
- observed.add(el);
3001
- }
3002
- }
3003
- schedule();
3004
- };
3005
- let mo;
3006
- if (scroller) {
3007
- resync();
3008
- if (typeof MutationObserver !== "undefined") {
3009
- mo = new MutationObserver(resync);
3010
- mo.observe(scroller, { childList: true });
3011
- }
3012
- }
3013
- this.teardowns.add(() => {
3014
- ro.disconnect();
3015
- mo?.disconnect();
3016
- for (const slot of slots) {
3017
- slot.removeEventListener("slotchange", onSlotChange);
3018
- }
3019
- if (raf) cancelAnimationFrame(raf);
3020
- });
3021
- }
3022
3143
  destroy() {
3023
3144
  this.destroyed = true;
3024
3145
  this.opening = false;
@@ -3036,9 +3157,10 @@ var BottomSheetEngine = class {
3036
3157
  this.backdrop.style.opacity = "";
3037
3158
  this.backdrop.style.pointerEvents = "";
3038
3159
  }
3039
- if (this.anchorHost) {
3040
- this.anchorHost.style.removeProperty("--bs-size");
3041
- this.anchorHost.style.removeProperty("--bs-progress");
3160
+ for (const host of [this.anchorHost, this.scrimParent, this.rootEl]) {
3161
+ if (!host) continue;
3162
+ host.style.removeProperty("--bs-size");
3163
+ host.style.removeProperty("--bs-progress");
3042
3164
  }
3043
3165
  if (this.screenComponent) {
3044
3166
  this.screenComponent.style.opacity = "";
@@ -3052,6 +3174,7 @@ var BottomSheetEngine = class {
3052
3174
  }
3053
3175
  newCycle() {
3054
3176
  this.allowOvershoot = false;
3177
+ this.opening = false;
3055
3178
  this.currentAbort.abort();
3056
3179
  this.currentAbort = new AbortController();
3057
3180
  return this.currentAbort.signal;
@@ -3107,8 +3230,14 @@ var BottomSheetEngine = class {
3107
3230
  this.element.setAttribute("aria-labelledby", titled.id);
3108
3231
  }
3109
3232
  recomputeSnaps() {
3233
+ this.resolveMaxHeightCap();
3234
+ this.snaps.setRaw(this.snapPointsRaw);
3235
+ this.clampToMaxHeight();
3236
+ this.scrim.invalidateOpacityCache();
3237
+ }
3238
+ auditDuplicateSnapIds(points) {
3110
3239
  const seen = /* @__PURE__ */ new Set();
3111
- for (const p of this.snapPointsRaw) {
3240
+ for (const p of points) {
3112
3241
  if (seen.has(p.id)) {
3113
3242
  console.warn(
3114
3243
  `[BottomSheet] duplicate snap id "${p.id}" \u2014 only the first occurrence is reachable.`
@@ -3116,9 +3245,6 @@ var BottomSheetEngine = class {
3116
3245
  }
3117
3246
  seen.add(p.id);
3118
3247
  }
3119
- this.snaps.setRaw(this.snapPointsRaw);
3120
- this.clampToMaxHeight();
3121
- this.scrim.invalidateOpacityCache();
3122
3248
  }
3123
3249
  getAllowedRange() {
3124
3250
  return this.snaps.getAllowedRange();
@@ -3306,9 +3432,15 @@ var BottomSheetEngine = class {
3306
3432
  flickVelocity: this.flickVelocity,
3307
3433
  dragThreshold: this.dragThreshold
3308
3434
  });
3309
- if (!target) return;
3435
+ if (!target) {
3436
+ this.clearWillChangeIfIdle();
3437
+ return;
3438
+ }
3310
3439
  const previousId = this.activeId;
3311
- if (target.id === previousId && target.size === this.size) return;
3440
+ if (target.id === previousId && target.size === this.size) {
3441
+ this.clearWillChangeIfIdle();
3442
+ return;
3443
+ }
3312
3444
  if (this.emitBeforeSnap(target, previousId)) {
3313
3445
  const restore = this.snaps.findById(previousId);
3314
3446
  if (restore) {
@@ -3331,27 +3463,19 @@ var BottomSheetEngine = class {
3331
3463
  if (signal.aborted) return;
3332
3464
  this.allowOvershoot = false;
3333
3465
  this.opening = false;
3334
- this.scrollCache.restore(target.id, previousSize, target.size);
3335
- this.updateAriaSlider();
3336
- this.emit("snap", {
3337
- id: target.id,
3338
- size: this.size,
3339
- progress: this.computeProgress(this.size)
3340
- });
3341
- this.haptic();
3342
- if (previousSnapSize === 0 && target.size > 0) {
3343
- this.emit("open", { id: target.id });
3344
- this.handleOpen();
3345
- notifyLinkedSheets(this.linkedSheets, this);
3346
- this.emit("opened", { id: target.id });
3347
- }
3348
- if (target.size === 0) {
3349
- this.emit("close", void 0);
3350
- this.handleClose();
3351
- this.emit("closed", void 0);
3352
- }
3466
+ this.completeSnap(
3467
+ target.id,
3468
+ target.size,
3469
+ previousSize,
3470
+ previousSnapSize === 0,
3471
+ true
3472
+ );
3353
3473
  });
3354
3474
  }
3475
+ clearWillChangeIfIdle() {
3476
+ if (this.animation.isAnimating || this.gesture?.isDragging) return;
3477
+ this.element.style.willChange = "";
3478
+ }
3355
3479
  haptic() {
3356
3480
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
3357
3481
  try {
@@ -3432,14 +3556,9 @@ var BottomSheetEngine = class {
3432
3556
  progress: this.computeProgress(this.size)
3433
3557
  });
3434
3558
  if (target.size > 0 && !this.lifecycle.isInstalled) {
3435
- this.emit("open", { id: target.id });
3436
- this.handleOpen();
3437
- notifyLinkedSheets(this.linkedSheets, this);
3438
- this.emit("opened", { id: target.id });
3559
+ this.emitOpenSequence(target.id);
3439
3560
  } else if (target.size === 0 && this.lifecycle.isInstalled) {
3440
- this.emit("close", void 0);
3441
- this.handleClose();
3442
- this.emit("closed", void 0);
3561
+ this.emitCloseSequence();
3443
3562
  }
3444
3563
  }
3445
3564
  handleClose() {
@@ -3506,6 +3625,7 @@ function useBottomSheet(opts) {
3506
3625
  if (!sheetRef.value) return;
3507
3626
  const {
3508
3627
  onSnap,
3628
+ teleport,
3509
3629
  teleportTo,
3510
3630
  backdropColor,
3511
3631
  backdropOpacity,
@@ -3520,7 +3640,7 @@ function useBottomSheet(opts) {
3520
3640
  scrim: screenRef.value ?? void 0
3521
3641
  });
3522
3642
  sync();
3523
- const target = resolveTeleportTarget(teleportTo);
3643
+ const target = teleport === false ? null : resolveTeleportTarget(teleportTo);
3524
3644
  if (target) {
3525
3645
  restoreTeleport = teleportElements(
3526
3646
  [backdropRef.value, screenRef.value, sheetRef.value],
@@ -3692,8 +3812,9 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3692
3812
  required: false
3693
3813
  },
3694
3814
  initialFocus: {
3695
- type: null,
3696
- required: false
3815
+ type: [String, Boolean],
3816
+ required: false,
3817
+ skipCheck: true
3697
3818
  },
3698
3819
  closeOnEscape: {
3699
3820
  type: Boolean,
@@ -3785,6 +3906,11 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3785
3906
  required: false,
3786
3907
  default: "body"
3787
3908
  },
3909
+ teleport: {
3910
+ type: Boolean,
3911
+ required: false,
3912
+ default: true
3913
+ },
3788
3914
  backdropColor: {
3789
3915
  type: String,
3790
3916
  required: false
@@ -3820,11 +3946,6 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3820
3946
  type: null,
3821
3947
  required: false
3822
3948
  },
3823
- teleport: {
3824
- type: Boolean,
3825
- required: false,
3826
- default: true
3827
- },
3828
3949
  anchors: {
3829
3950
  type: Array,
3830
3951
  required: false
@@ -3842,6 +3963,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3842
3963
  "opened",
3843
3964
  "closed",
3844
3965
  "before-close",
3966
+ "before-snap",
3845
3967
  "drag-start",
3846
3968
  "drag-end",
3847
3969
  "update:open",
@@ -3867,6 +3989,12 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3867
3989
  on("opened", (payload) => emit("opened", payload.id));
3868
3990
  on("closed", () => emit("closed"));
3869
3991
  on("before-close", (payload) => emit("before-close", payload));
3992
+ on("before-snap", (payload) => emit("before-snap", {
3993
+ id: payload.id,
3994
+ size: payload.size,
3995
+ previousId: payload.previousId,
3996
+ cancel: payload.cancel
3997
+ }));
3870
3998
  on("dragstart", (payload) => emit("drag-start", payload));
3871
3999
  on("dragend", (payload) => emit("drag-end", payload));
3872
4000
  watch(() => props.open, (next) => {
@@ -3879,6 +4007,14 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3879
4007
  if (next === void 0 || next === state.activeId) return;
3880
4008
  void snapTo(next);
3881
4009
  });
4010
+ watch(() => props.snapPoints, (next) => {
4011
+ if (!next) return;
4012
+ setSnapPoints(next, props.allowed);
4013
+ }, { deep: true });
4014
+ watch(() => props.allowed, (next) => {
4015
+ if (!next) return;
4016
+ setAllowed(Array.from(next));
4017
+ });
3882
4018
  watch(() => props.backdropColor, (next) => {
3883
4019
  if (next === void 0) return;
3884
4020
  setScrimColor(next);
@@ -3895,6 +4031,18 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3895
4031
  if (next === void 0) return;
3896
4032
  setMaxHeight(next);
3897
4033
  });
4034
+ watch(() => props.persistent, (next) => {
4035
+ if (next === void 0) return;
4036
+ getEngine()?.setPersistent(next);
4037
+ });
4038
+ watch(() => props.disableClose, (next) => {
4039
+ if (next === void 0) return;
4040
+ getEngine()?.setDisableClose(next);
4041
+ });
4042
+ watch(() => props.disableDrag, (next) => {
4043
+ if (next === void 0) return;
4044
+ getEngine()?.setDisableDrag(next);
4045
+ });
3898
4046
  const mounted = ref(false);
3899
4047
  const anchorHosts = shallowRef({});
3900
4048
  const anchorDetachers = ref([]);