@surdeddd/bottom-sheet 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +358 -211
  43. package/dist/vue.d.cts +6 -0
  44. package/dist/vue.d.ts +6 -0
  45. package/dist/vue.js +358 -211
  46. package/package.json +6 -6
@@ -274,14 +274,21 @@ var SnapResolver = class {
274
274
  resolved = [];
275
275
  maxAxisSize = 0;
276
276
  rangeCache = null;
277
+ notified = false;
277
278
  recompute() {
278
- this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
279
- this.maxAxisSize = this.resolved.reduce(
279
+ const measure = this.measureFit;
280
+ let measured;
281
+ const measureOnce = measure ? () => measured ??= measure() : void 0;
282
+ this.resolved = resolveSnapList(this.raw, this.mode, measureOnce);
283
+ const nextMax = this.resolved.reduce(
280
284
  (m, s) => s.size > m ? s.size : m,
281
285
  0
282
286
  );
283
287
  this.rangeCache = null;
284
- this.onMaxAxisSizeChange?.(this.maxAxisSize);
288
+ const changed = nextMax !== this.maxAxisSize || !this.notified;
289
+ this.maxAxisSize = nextMax;
290
+ this.notified = true;
291
+ if (changed) this.onMaxAxisSizeChange?.(this.maxAxisSize);
285
292
  }
286
293
  setRaw(raw) {
287
294
  this.raw = raw;
@@ -434,7 +441,10 @@ var ensureHandler = () => {
434
441
  target.consumed = true;
435
442
  const idx = closers.indexOf(target);
436
443
  if (idx !== -1) closers.splice(idx, 1);
437
- target.close();
444
+ const settled = target;
445
+ void Promise.resolve(settled.close()).then(() => {
446
+ if (settled.isOpen()) settled.restore();
447
+ });
438
448
  });
439
449
  };
440
450
  var programmaticBack = () => {
@@ -470,16 +480,7 @@ function installRoute(deps) {
470
480
  pushed = false;
471
481
  }
472
482
  };
473
- const onOpen = () => {
474
- if (closer) return;
475
- closer = {
476
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
477
- isTop: () => deps.isTopSheet(),
478
- close: () => void deps.close(),
479
- consumed: false
480
- };
481
- ensureHandler();
482
- closers.push(closer);
483
+ const pushEntry = () => {
483
484
  try {
484
485
  if (deps.routedTo !== void 0) {
485
486
  history.pushState({ __bsRouted: deps.sheetId }, "", deps.routedTo);
@@ -491,6 +492,25 @@ function installRoute(deps) {
491
492
  pushed = false;
492
493
  }
493
494
  };
495
+ const onOpen = () => {
496
+ if (closer) return;
497
+ const self = {
498
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
499
+ isTop: () => deps.isTopSheet(),
500
+ close: () => deps.close(),
501
+ restore: () => {
502
+ if (deps.isDestroyed() || closer !== self) return;
503
+ self.consumed = false;
504
+ if (!closers.includes(self)) closers.push(self);
505
+ pushEntry();
506
+ },
507
+ consumed: false
508
+ };
509
+ closer = self;
510
+ ensureHandler();
511
+ closers.push(self);
512
+ pushEntry();
513
+ };
494
514
  const unsubscribeOpen = deps.on("open", onOpen);
495
515
  const unsubscribeClose = deps.on("close", teardownEntry);
496
516
  if (deps.getSize() > 0) onOpen();
@@ -537,11 +557,13 @@ var CONTENT_PULL_THRESHOLD = 60;
537
557
  function installContentSwipe(deps) {
538
558
  const { container } = deps;
539
559
  let startY = 0;
560
+ let startScrollTop = 0;
540
561
  let moved = false;
541
562
  const onTouchStart = (e) => {
542
563
  const t = e.touches[0];
543
564
  if (!t) return;
544
565
  startY = t.clientY;
566
+ startScrollTop = container.scrollTop;
545
567
  moved = false;
546
568
  };
547
569
  const onTouchMove = (e) => {
@@ -552,6 +574,7 @@ function installContentSwipe(deps) {
552
574
  const onTouchEnd = (e) => {
553
575
  if (!moved) return;
554
576
  if (deps.isDragging() || deps.isAnimating()) return;
577
+ if (container.scrollTop !== startScrollTop) return;
555
578
  const t = e.changedTouches[0];
556
579
  if (!t) return;
557
580
  const deltaY = t.clientY - startY;
@@ -663,6 +686,131 @@ function installResizeObserver(deps) {
663
686
  };
664
687
  }
665
688
 
689
+ // src/core/features/fit-observer.ts
690
+ var contentTargets = (scroller) => {
691
+ if (!scroller) return [];
692
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
693
+ const out = [];
694
+ for (const child of Array.from(scroller.children)) {
695
+ if (hasSlot && child instanceof HTMLSlotElement) {
696
+ for (const el of child.assignedElements()) out.push(el);
697
+ } else {
698
+ out.push(child);
699
+ }
700
+ }
701
+ return out;
702
+ };
703
+ function installFitObserver(deps) {
704
+ if (typeof ResizeObserver === "undefined") return () => {
705
+ };
706
+ if (!deps.hasFitSnap()) return () => {
707
+ };
708
+ let raf = 0;
709
+ const schedule = () => {
710
+ if (raf) return;
711
+ raf = requestAnimationFrame(() => {
712
+ raf = 0;
713
+ if (deps.isDestroyed() || deps.isDragging()) return;
714
+ deps.recompute();
715
+ });
716
+ };
717
+ const ro = new ResizeObserver(schedule);
718
+ ro.observe(deps.handle);
719
+ const scroller = deps.scrollContainer;
720
+ const observed = /* @__PURE__ */ new Set();
721
+ const slots = /* @__PURE__ */ new Set();
722
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
723
+ const onSlotChange = () => resync();
724
+ const trackSlots = () => {
725
+ if (!scroller || !hasSlot) return;
726
+ for (const child of Array.from(scroller.children)) {
727
+ if (child instanceof HTMLSlotElement && !slots.has(child)) {
728
+ child.addEventListener("slotchange", onSlotChange);
729
+ slots.add(child);
730
+ }
731
+ }
732
+ };
733
+ const resync = () => {
734
+ trackSlots();
735
+ const targets = contentTargets(scroller);
736
+ for (const el of observed) {
737
+ if (!targets.includes(el)) {
738
+ ro.unobserve(el);
739
+ observed.delete(el);
740
+ }
741
+ }
742
+ for (const el of targets) {
743
+ if (!observed.has(el)) {
744
+ ro.observe(el);
745
+ observed.add(el);
746
+ }
747
+ }
748
+ schedule();
749
+ };
750
+ let mo;
751
+ if (scroller) {
752
+ resync();
753
+ if (typeof MutationObserver !== "undefined") {
754
+ mo = new MutationObserver(resync);
755
+ mo.observe(scroller, { childList: true });
756
+ }
757
+ }
758
+ return () => {
759
+ ro.disconnect();
760
+ mo?.disconnect();
761
+ for (const slot of slots) {
762
+ slot.removeEventListener("slotchange", onSlotChange);
763
+ }
764
+ if (raf) cancelAnimationFrame(raf);
765
+ };
766
+ }
767
+
768
+ // src/core/features/fit-measurement.ts
769
+ var measureSheetNatural = (element, content, vertical) => {
770
+ if (!content) {
771
+ return vertical ? element.offsetHeight : element.offsetWidth;
772
+ }
773
+ const axis = vertical ? "height" : "width";
774
+ const sheetStyle = element.style;
775
+ const cs = content.style;
776
+ const prevSheet = sheetStyle[axis];
777
+ const prevFlex = cs.flex;
778
+ const prevContent = vertical ? cs.height : cs.width;
779
+ sheetStyle[axis] = "auto";
780
+ cs.flex = "none";
781
+ if (vertical) cs.height = "auto";
782
+ else cs.width = "auto";
783
+ const poked = vertical ? element.offsetHeight : element.offsetWidth;
784
+ const contentBox = vertical ? content.clientHeight : content.clientWidth;
785
+ const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
786
+ const natural = Math.max(poked, poked - contentBox + contentScroll);
787
+ sheetStyle[axis] = prevSheet;
788
+ cs.flex = prevFlex;
789
+ if (vertical) cs.height = prevContent;
790
+ else cs.width = prevContent;
791
+ return natural;
792
+ };
793
+ var containingExtent = (element, vertical) => {
794
+ const op = element.offsetParent;
795
+ if (op && op !== document.body && op !== document.documentElement) {
796
+ return vertical ? op.clientHeight : op.clientWidth;
797
+ }
798
+ return vertical ? window.innerHeight : window.innerWidth;
799
+ };
800
+ var measureFitSize = (deps) => {
801
+ const vertical = layoutAxis(deps.mode) === "height";
802
+ const natural = measureSheetNatural(
803
+ deps.element,
804
+ deps.scrollContainer,
805
+ vertical
806
+ );
807
+ const cap = deps.getMaxHeightCap();
808
+ const capped = cap !== void 0 ? Math.min(natural, cap) : natural;
809
+ if (typeof window === "undefined") return capped;
810
+ const viewport = containingExtent(deps.element, vertical);
811
+ return viewport > 0 ? Math.min(capped, viewport) : capped;
812
+ };
813
+
666
814
  // src/core/features/slider-keyboard.ts
667
815
  function installSliderKeyboard(deps) {
668
816
  const isVerticalAxis = deps.mode === "bottom" || deps.mode === "top";
@@ -1147,6 +1295,9 @@ var ScrimController = class {
1147
1295
  const s = this.screenComponent.style;
1148
1296
  s.opacity = "";
1149
1297
  s.display = "";
1298
+ s.position = "";
1299
+ s.inset = "";
1300
+ s.pointerEvents = "";
1150
1301
  }
1151
1302
  if (this.backdrop) {
1152
1303
  this.backdrop.style.opacity = "";
@@ -1395,7 +1546,12 @@ var AnimationRunner = class {
1395
1546
  this.currentSpring?.cancel();
1396
1547
  }
1397
1548
  async animateTo(target, velocityPxPerMs) {
1398
- if (target === this.getSize()) return;
1549
+ if (target === this.getSize()) {
1550
+ if (!this.isAnimating && !this.isDragging()) {
1551
+ this.element.style.willChange = "";
1552
+ }
1553
+ return;
1554
+ }
1399
1555
  this.currentTween?.cancel();
1400
1556
  this.currentSpring?.cancel();
1401
1557
  this.getRootEl()?.setAttribute("data-animating", "true");
@@ -1403,7 +1559,7 @@ var AnimationRunner = class {
1403
1559
  if (this.reducedMotion) {
1404
1560
  this.applySizeFn(target);
1405
1561
  this.getRootEl()?.removeAttribute("data-animating");
1406
- if (!this.isDragging()) this.element.style.willChange = "auto";
1562
+ if (!this.isDragging()) this.element.style.willChange = "";
1407
1563
  return;
1408
1564
  }
1409
1565
  if (this.animationKind === "spring") {
@@ -1431,7 +1587,7 @@ var AnimationRunner = class {
1431
1587
  }
1432
1588
  if (!this.isAnimating) {
1433
1589
  this.getRootEl()?.removeAttribute("data-animating");
1434
- if (!this.isDragging()) this.element.style.willChange = "auto";
1590
+ if (!this.isDragging()) this.element.style.willChange = "";
1435
1591
  }
1436
1592
  }
1437
1593
  destroy() {
@@ -1462,9 +1618,35 @@ var isVisible = (el) => {
1462
1618
  }
1463
1619
  return el.offsetParent !== null;
1464
1620
  };
1465
- var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1621
+ var collectFocusables = (root) => {
1622
+ const out = Array.from(root.querySelectorAll(FOCUSABLE));
1623
+ if (typeof HTMLSlotElement !== "undefined") {
1624
+ for (const slot of Array.from(root.querySelectorAll("slot"))) {
1625
+ for (const assigned of slot.assignedElements()) {
1626
+ if (assigned.matches(FOCUSABLE)) out.push(assigned);
1627
+ out.push(...collectFocusables(assigned));
1628
+ }
1629
+ }
1630
+ }
1631
+ return out;
1632
+ };
1633
+ var focusables = (root) => collectFocusables(root).filter(
1466
1634
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1467
1635
  );
1636
+ var containsComposed = (container, node) => {
1637
+ let n = node;
1638
+ while (n) {
1639
+ if (n === container) return true;
1640
+ const slot = n.assignedSlot;
1641
+ if (slot) {
1642
+ n = slot;
1643
+ continue;
1644
+ }
1645
+ const parent = n.parentNode;
1646
+ n = parent instanceof ShadowRoot ? parent.host : parent;
1647
+ }
1648
+ return false;
1649
+ };
1468
1650
  var trapStack = [];
1469
1651
  var installFocusTrap = (container, options = {}) => {
1470
1652
  if (typeof document === "undefined") return () => {
@@ -1474,7 +1656,13 @@ var installFocusTrap = (container, options = {}) => {
1474
1656
  const isActive = () => trapStack[trapStack.length - 1] === token;
1475
1657
  const previouslyFocused = document.activeElement;
1476
1658
  const focusInitial = () => {
1477
- const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
1659
+ const spec = options.initialFocus;
1660
+ if (spec === false) {
1661
+ if (!container.hasAttribute("tabindex")) container.tabIndex = -1;
1662
+ container.focus({ preventScroll: true });
1663
+ return;
1664
+ }
1665
+ const initial = typeof spec === "string" ? container.querySelector(spec) : spec ?? focusables(container)[0] ?? container;
1478
1666
  initial?.focus({ preventScroll: true });
1479
1667
  };
1480
1668
  focusInitial();
@@ -1504,7 +1692,7 @@ var installFocusTrap = (container, options = {}) => {
1504
1692
  const handleFocusIn = (e) => {
1505
1693
  if (!isActive()) return;
1506
1694
  const target = e.target;
1507
- if (target && !container.contains(target)) {
1695
+ if (target && !containsComposed(container, target)) {
1508
1696
  const list = focusables(container);
1509
1697
  list[0]?.focus({ preventScroll: true });
1510
1698
  }
@@ -1541,8 +1729,14 @@ var lockBodyScroll = () => {
1541
1729
  position: body.style.position,
1542
1730
  top: body.style.top,
1543
1731
  width: body.style.width,
1732
+ paddingRight: body.style.paddingRight,
1544
1733
  scrollY: window.scrollY
1545
1734
  };
1735
+ const scrollbarGap = window.innerWidth - document.documentElement.clientWidth;
1736
+ if (scrollbarGap > 0) {
1737
+ const current = parseFloat(getComputedStyle(body).paddingRight) || 0;
1738
+ body.style.paddingRight = `${current + scrollbarGap}px`;
1739
+ }
1546
1740
  body.style.overflow = "hidden";
1547
1741
  body.style.position = "fixed";
1548
1742
  body.style.top = `-${savedStyles.scrollY}px`;
@@ -1560,6 +1754,7 @@ var lockBodyScroll = () => {
1560
1754
  body.style.position = savedStyles.position;
1561
1755
  body.style.top = savedStyles.top;
1562
1756
  body.style.width = savedStyles.width;
1757
+ body.style.paddingRight = savedStyles.paddingRight;
1563
1758
  window.scrollTo(0, savedStyles.scrollY);
1564
1759
  savedStyles = null;
1565
1760
  }
@@ -1772,12 +1967,14 @@ var installGestures = (handle, mode, callbacks) => {
1772
1967
  handle.addEventListener("pointermove", onPointerMove);
1773
1968
  handle.addEventListener("pointerup", finishGesture);
1774
1969
  handle.addEventListener("pointercancel", onPointerCancel);
1970
+ const prevTouchAction = handle.style.touchAction;
1775
1971
  handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1776
1972
  return () => {
1777
1973
  handle.removeEventListener("pointerdown", onPointerDown);
1778
1974
  handle.removeEventListener("pointermove", onPointerMove);
1779
1975
  handle.removeEventListener("pointerup", finishGesture);
1780
1976
  handle.removeEventListener("pointercancel", onPointerCancel);
1977
+ handle.style.touchAction = prevTouchAction;
1781
1978
  if (activePointerId !== null && handle.hasPointerCapture(activePointerId)) {
1782
1979
  handle.releasePointerCapture(activePointerId);
1783
1980
  }
@@ -1904,7 +2101,7 @@ var GestureController = class {
1904
2101
  this.isDragging_ = false;
1905
2102
  this.keyboardDismissed = false;
1906
2103
  this.deps.getRoot()?.removeAttribute("data-dragging");
1907
- this.deps.element.style.willChange = "auto";
2104
+ this.deps.element.style.willChange = "";
1908
2105
  }
1909
2106
  };
1910
2107
 
@@ -2304,8 +2501,7 @@ function resolveEngineOptions(opts, extras) {
2304
2501
  };
2305
2502
  }
2306
2503
 
2307
- // src/core/BottomSheetEngine.ts
2308
- var HAPTIC_DURATION_MS = 8;
2504
+ // src/core/primitives/teardown-stack.ts
2309
2505
  var TeardownStack = class {
2310
2506
  fns = [];
2311
2507
  add(fn) {
@@ -2323,6 +2519,9 @@ var TeardownStack = class {
2323
2519
  }
2324
2520
  }
2325
2521
  };
2522
+
2523
+ // src/core/BottomSheetEngine.ts
2524
+ var HAPTIC_DURATION_MS = 8;
2326
2525
  var BottomSheetEngine = class {
2327
2526
  id = nextInstanceId("bs");
2328
2527
  element;
@@ -2346,6 +2545,7 @@ var BottomSheetEngine = class {
2346
2545
  disableDragFlag;
2347
2546
  closeOnRouteChange;
2348
2547
  maxHeightCap;
2548
+ maxHeightRaw;
2349
2549
  snapPointsRaw;
2350
2550
  snaps;
2351
2551
  activeId;
@@ -2381,6 +2581,7 @@ var BottomSheetEngine = class {
2381
2581
  detachScrimStages = null;
2382
2582
  stackEffectEnabled = false;
2383
2583
  stackEffectPrimed = false;
2584
+ fitDeps;
2384
2585
  transformTemplate;
2385
2586
  constructor(opts) {
2386
2587
  const resolved = resolveEngineOptions(opts, {
@@ -2454,12 +2655,19 @@ var BottomSheetEngine = class {
2454
2655
  }
2455
2656
  this.activeId = resolved.initialId;
2456
2657
  auditVhUsage(this.snapPointsRaw);
2658
+ this.auditDuplicateSnapIds(this.snapPointsRaw);
2457
2659
  this.rootEl = this.element.closest(".bs-root");
2660
+ this.fitDeps = {
2661
+ element: this.element,
2662
+ scrollContainer: this.scrollContainer,
2663
+ mode: this.mode,
2664
+ getMaxHeightCap: () => this.maxHeightCap
2665
+ };
2458
2666
  this.snaps = new SnapResolver(
2459
2667
  this.snapPointsRaw,
2460
2668
  resolved.initialAllowed,
2461
2669
  this.mode,
2462
- () => this.measureFitSize(),
2670
+ () => measureFitSize(this.fitDeps),
2463
2671
  (maxAxisSize) => {
2464
2672
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2465
2673
  }
@@ -2493,7 +2701,18 @@ var BottomSheetEngine = class {
2493
2701
  io.observe(this.element);
2494
2702
  this.teardowns.add(() => io.disconnect());
2495
2703
  }
2496
- this.installFitObserver();
2704
+ this.teardowns.add(
2705
+ installFitObserver({
2706
+ handle: this.handle,
2707
+ scrollContainer: this.scrollContainer,
2708
+ hasFitSnap: () => this.snapPointsRaw.some(
2709
+ (p) => p.size === "fit" || p.size === "content"
2710
+ ),
2711
+ isDestroyed: () => this.destroyed,
2712
+ isDragging: () => this.gesture?.isDragging ?? false,
2713
+ recompute: () => this.recompute()
2714
+ })
2715
+ );
2497
2716
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2498
2717
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2499
2718
  this.applyAutoAriaLabelledBy();
@@ -2644,27 +2863,33 @@ var BottomSheetEngine = class {
2644
2863
  } else {
2645
2864
  await this.animation.animateTo(target.size, velocityPxPerMs);
2646
2865
  }
2647
- this.opening = false;
2648
2866
  externalSignal?.removeEventListener("abort", onExternalAbort);
2649
2867
  if (signal.aborted) return;
2650
- this.scrollCache.restore(id, previousSize, target.size);
2868
+ this.opening = false;
2869
+ this.completeSnap(id, target.size, previousSize, wasClosed, false);
2870
+ }
2871
+ completeSnap(id, targetSize, previousSize, wasClosed, withHaptic) {
2872
+ this.scrollCache.restore(id, previousSize, targetSize);
2651
2873
  this.updateAriaSlider();
2652
2874
  this.emit("snap", {
2653
2875
  id,
2654
2876
  size: this.size,
2655
2877
  progress: this.computeProgress(this.size)
2656
2878
  });
2657
- if (wasClosed && target.size > 0) {
2658
- this.emit("open", { id });
2659
- this.handleOpen();
2660
- notifyLinkedSheets(this.linkedSheets, this);
2661
- this.emit("opened", { id });
2662
- }
2663
- if (target.size === 0) {
2664
- this.emit("close", void 0);
2665
- this.handleClose();
2666
- this.emit("closed", void 0);
2667
- }
2879
+ if (withHaptic) this.haptic();
2880
+ if (wasClosed && targetSize > 0) this.emitOpenSequence(id);
2881
+ if (targetSize === 0) this.emitCloseSequence();
2882
+ }
2883
+ emitOpenSequence(id) {
2884
+ this.emit("open", { id });
2885
+ this.handleOpen();
2886
+ notifyLinkedSheets(this.linkedSheets, this);
2887
+ this.emit("opened", { id });
2888
+ }
2889
+ emitCloseSequence() {
2890
+ this.emit("close", void 0);
2891
+ this.handleClose();
2892
+ this.emit("closed", void 0);
2668
2893
  }
2669
2894
  async dragTo(targetSize, velocityOrOpts = 0) {
2670
2895
  if (this.destroyed) return;
@@ -2685,19 +2910,31 @@ var BottomSheetEngine = class {
2685
2910
  }
2686
2911
  open(id) {
2687
2912
  if (this.destroyed) return Promise.resolve();
2688
- const target = id ?? this.snaps.getAllowedIds().find((a) => a !== "closed") ?? this.activeId;
2913
+ const target = id ?? this.snaps.getAllowedIds().find((a) => (this.snaps.findById(a)?.size ?? 0) > 0) ?? this.activeId;
2689
2914
  return this.snapTo(target);
2690
2915
  }
2691
2916
  close(reason = "programmatic") {
2692
2917
  if (this.destroyed) return Promise.resolve();
2693
2918
  if (this.disableCloseFlag) return Promise.resolve();
2694
2919
  if (this.size > 0 && this.emitBeforeClose(reason)) return Promise.resolve();
2695
- const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds()[0];
2920
+ 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];
2696
2921
  return this.snapTo(closedId ?? this.activeId);
2697
2922
  }
2698
2923
  canDismiss() {
2699
2924
  return !this.persistent && !this.disableCloseFlag && !this.destroyed;
2700
2925
  }
2926
+ setPersistent(value) {
2927
+ if (this.destroyed) return;
2928
+ this.persistent = value;
2929
+ }
2930
+ setDisableClose(value) {
2931
+ if (this.destroyed) return;
2932
+ this.disableCloseFlag = value;
2933
+ }
2934
+ setDisableDrag(value) {
2935
+ if (this.destroyed) return;
2936
+ this.disableDragFlag = value;
2937
+ }
2701
2938
  isTop() {
2702
2939
  return this.isTopSheet;
2703
2940
  }
@@ -2734,18 +2971,23 @@ var BottomSheetEngine = class {
2734
2971
  axis === "height" ? "max-height" : "max-width",
2735
2972
  value
2736
2973
  );
2737
- if (typeof h === "number") {
2738
- this.maxHeightCap = h;
2739
- this.recompute();
2740
- } else if (typeof window !== "undefined") {
2741
- const probe2 = document.createElement("div");
2742
- probe2.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;${axis === "height" ? "height" : "width"}:${value};`;
2743
- document.body.appendChild(probe2);
2744
- const measured = axis === "height" ? probe2.offsetHeight : probe2.offsetWidth;
2745
- probe2.remove();
2746
- this.maxHeightCap = measured > 0 ? measured : void 0;
2747
- this.recompute();
2974
+ this.maxHeightRaw = h;
2975
+ this.resolveMaxHeightCap();
2976
+ this.recompute();
2977
+ }
2978
+ resolveMaxHeightCap() {
2979
+ const raw = this.maxHeightRaw;
2980
+ if (raw === void 0) {
2981
+ this.maxHeightCap = void 0;
2982
+ return;
2748
2983
  }
2984
+ if (typeof raw === "number") {
2985
+ this.maxHeightCap = raw;
2986
+ return;
2987
+ }
2988
+ if (typeof window === "undefined") return;
2989
+ const measured = resolveSnap(raw, this.mode);
2990
+ this.maxHeightCap = measured > 0 ? measured : void 0;
2749
2991
  }
2750
2992
  clampToMaxHeight() {
2751
2993
  if (this.maxHeightCap === void 0) return;
@@ -2876,6 +3118,7 @@ var BottomSheetEngine = class {
2876
3118
  this.newCycle();
2877
3119
  this.snapPointsRaw = points;
2878
3120
  auditVhUsage(points);
3121
+ this.auditDuplicateSnapIds(points);
2879
3122
  this.snaps.setRaw(points);
2880
3123
  if (allowed) this.snaps.setAllowedIds(allowed);
2881
3124
  this.scrollCache.clear();
@@ -2888,6 +3131,7 @@ var BottomSheetEngine = class {
2888
3131
  }
2889
3132
  recompute() {
2890
3133
  if (this.destroyed) return;
3134
+ this.resolveMaxHeightCap();
2891
3135
  this.snaps.recompute();
2892
3136
  this.clampToMaxHeight();
2893
3137
  this.scrim.invalidateOpacityCache();
@@ -2899,129 +3143,6 @@ var BottomSheetEngine = class {
2899
3143
  this.updateAriaSlider();
2900
3144
  }
2901
3145
  }
2902
- contentTargets() {
2903
- const scroller = this.scrollContainer;
2904
- if (!scroller) return [];
2905
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2906
- const out = [];
2907
- for (const child of Array.from(scroller.children)) {
2908
- if (hasSlot && child instanceof HTMLSlotElement) {
2909
- for (const el of child.assignedElements()) out.push(el);
2910
- } else {
2911
- out.push(child);
2912
- }
2913
- }
2914
- return out;
2915
- }
2916
- measureSheetNatural(vertical) {
2917
- const el = this.element;
2918
- const content = this.scrollContainer;
2919
- if (!content) {
2920
- return vertical ? el.offsetHeight : el.offsetWidth;
2921
- }
2922
- const axis = vertical ? "height" : "width";
2923
- const sheetStyle = el.style;
2924
- const cs = content.style;
2925
- const prevSheet = sheetStyle[axis];
2926
- const prevFlex = cs.flex;
2927
- const prevContent = vertical ? cs.height : cs.width;
2928
- sheetStyle[axis] = "auto";
2929
- cs.flex = "none";
2930
- if (vertical) cs.height = "auto";
2931
- else cs.width = "auto";
2932
- const poked = vertical ? el.offsetHeight : el.offsetWidth;
2933
- const contentBox = vertical ? content.clientHeight : content.clientWidth;
2934
- const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
2935
- const natural = Math.max(poked, poked - contentBox + contentScroll);
2936
- sheetStyle[axis] = prevSheet;
2937
- cs.flex = prevFlex;
2938
- if (vertical) cs.height = prevContent;
2939
- else cs.width = prevContent;
2940
- return natural;
2941
- }
2942
- measureFitSize() {
2943
- const vertical = layoutAxis(this.mode) === "height";
2944
- const natural = this.measureSheetNatural(vertical);
2945
- const capped = this.maxHeightCap !== void 0 ? Math.min(natural, this.maxHeightCap) : natural;
2946
- if (typeof window === "undefined") return capped;
2947
- const viewport = this.containingExtent(vertical);
2948
- return viewport > 0 ? Math.min(capped, viewport) : capped;
2949
- }
2950
- // The fit cap is the sheet's containing block, not always the window: a
2951
- // position:fixed sheet nested under a transform/filter/will-change ancestor
2952
- // is clipped to that ancestor, so capping by window.innerHeight oversizes the
2953
- // sheet and the content/footer get clipped. offsetParent resolves to that
2954
- // ancestor when present, and is null (→ viewport) for a plain fixed sheet.
2955
- containingExtent(vertical) {
2956
- const op = this.element.offsetParent;
2957
- if (op && op !== document.body && op !== document.documentElement) {
2958
- return vertical ? op.clientHeight : op.clientWidth;
2959
- }
2960
- return vertical ? window.innerHeight : window.innerWidth;
2961
- }
2962
- installFitObserver() {
2963
- if (typeof ResizeObserver === "undefined") return;
2964
- if (!this.snapPointsRaw.some((p) => p.size === "fit" || p.size === "content"))
2965
- return;
2966
- let raf = 0;
2967
- const schedule = () => {
2968
- if (raf) return;
2969
- raf = requestAnimationFrame(() => {
2970
- raf = 0;
2971
- if (this.destroyed || this.gesture?.isDragging) return;
2972
- this.recompute();
2973
- });
2974
- };
2975
- const ro = new ResizeObserver(schedule);
2976
- ro.observe(this.handle);
2977
- const scroller = this.scrollContainer;
2978
- const observed = /* @__PURE__ */ new Set();
2979
- const slots = /* @__PURE__ */ new Set();
2980
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2981
- const onSlotChange = () => resync();
2982
- const trackSlots = () => {
2983
- if (!scroller || !hasSlot) return;
2984
- for (const child of Array.from(scroller.children)) {
2985
- if (child instanceof HTMLSlotElement && !slots.has(child)) {
2986
- child.addEventListener("slotchange", onSlotChange);
2987
- slots.add(child);
2988
- }
2989
- }
2990
- };
2991
- const resync = () => {
2992
- trackSlots();
2993
- const targets = this.contentTargets();
2994
- for (const el of observed) {
2995
- if (!targets.includes(el)) {
2996
- ro.unobserve(el);
2997
- observed.delete(el);
2998
- }
2999
- }
3000
- for (const el of targets) {
3001
- if (!observed.has(el)) {
3002
- ro.observe(el);
3003
- observed.add(el);
3004
- }
3005
- }
3006
- schedule();
3007
- };
3008
- let mo;
3009
- if (scroller) {
3010
- resync();
3011
- if (typeof MutationObserver !== "undefined") {
3012
- mo = new MutationObserver(resync);
3013
- mo.observe(scroller, { childList: true });
3014
- }
3015
- }
3016
- this.teardowns.add(() => {
3017
- ro.disconnect();
3018
- mo?.disconnect();
3019
- for (const slot of slots) {
3020
- slot.removeEventListener("slotchange", onSlotChange);
3021
- }
3022
- if (raf) cancelAnimationFrame(raf);
3023
- });
3024
- }
3025
3146
  destroy() {
3026
3147
  this.destroyed = true;
3027
3148
  this.opening = false;
@@ -3039,9 +3160,10 @@ var BottomSheetEngine = class {
3039
3160
  this.backdrop.style.opacity = "";
3040
3161
  this.backdrop.style.pointerEvents = "";
3041
3162
  }
3042
- if (this.anchorHost) {
3043
- this.anchorHost.style.removeProperty("--bs-size");
3044
- this.anchorHost.style.removeProperty("--bs-progress");
3163
+ for (const host of [this.anchorHost, this.scrimParent, this.rootEl]) {
3164
+ if (!host) continue;
3165
+ host.style.removeProperty("--bs-size");
3166
+ host.style.removeProperty("--bs-progress");
3045
3167
  }
3046
3168
  if (this.screenComponent) {
3047
3169
  this.screenComponent.style.opacity = "";
@@ -3055,6 +3177,7 @@ var BottomSheetEngine = class {
3055
3177
  }
3056
3178
  newCycle() {
3057
3179
  this.allowOvershoot = false;
3180
+ this.opening = false;
3058
3181
  this.currentAbort.abort();
3059
3182
  this.currentAbort = new AbortController();
3060
3183
  return this.currentAbort.signal;
@@ -3110,8 +3233,14 @@ var BottomSheetEngine = class {
3110
3233
  this.element.setAttribute("aria-labelledby", titled.id);
3111
3234
  }
3112
3235
  recomputeSnaps() {
3236
+ this.resolveMaxHeightCap();
3237
+ this.snaps.setRaw(this.snapPointsRaw);
3238
+ this.clampToMaxHeight();
3239
+ this.scrim.invalidateOpacityCache();
3240
+ }
3241
+ auditDuplicateSnapIds(points) {
3113
3242
  const seen = /* @__PURE__ */ new Set();
3114
- for (const p of this.snapPointsRaw) {
3243
+ for (const p of points) {
3115
3244
  if (seen.has(p.id)) {
3116
3245
  console.warn(
3117
3246
  `[BottomSheet] duplicate snap id "${p.id}" \u2014 only the first occurrence is reachable.`
@@ -3119,9 +3248,6 @@ var BottomSheetEngine = class {
3119
3248
  }
3120
3249
  seen.add(p.id);
3121
3250
  }
3122
- this.snaps.setRaw(this.snapPointsRaw);
3123
- this.clampToMaxHeight();
3124
- this.scrim.invalidateOpacityCache();
3125
3251
  }
3126
3252
  getAllowedRange() {
3127
3253
  return this.snaps.getAllowedRange();
@@ -3309,9 +3435,15 @@ var BottomSheetEngine = class {
3309
3435
  flickVelocity: this.flickVelocity,
3310
3436
  dragThreshold: this.dragThreshold
3311
3437
  });
3312
- if (!target) return;
3438
+ if (!target) {
3439
+ this.clearWillChangeIfIdle();
3440
+ return;
3441
+ }
3313
3442
  const previousId = this.activeId;
3314
- if (target.id === previousId && target.size === this.size) return;
3443
+ if (target.id === previousId && target.size === this.size) {
3444
+ this.clearWillChangeIfIdle();
3445
+ return;
3446
+ }
3315
3447
  if (this.emitBeforeSnap(target, previousId)) {
3316
3448
  const restore = this.snaps.findById(previousId);
3317
3449
  if (restore) {
@@ -3334,27 +3466,19 @@ var BottomSheetEngine = class {
3334
3466
  if (signal.aborted) return;
3335
3467
  this.allowOvershoot = false;
3336
3468
  this.opening = false;
3337
- this.scrollCache.restore(target.id, previousSize, target.size);
3338
- this.updateAriaSlider();
3339
- this.emit("snap", {
3340
- id: target.id,
3341
- size: this.size,
3342
- progress: this.computeProgress(this.size)
3343
- });
3344
- this.haptic();
3345
- if (previousSnapSize === 0 && target.size > 0) {
3346
- this.emit("open", { id: target.id });
3347
- this.handleOpen();
3348
- notifyLinkedSheets(this.linkedSheets, this);
3349
- this.emit("opened", { id: target.id });
3350
- }
3351
- if (target.size === 0) {
3352
- this.emit("close", void 0);
3353
- this.handleClose();
3354
- this.emit("closed", void 0);
3355
- }
3469
+ this.completeSnap(
3470
+ target.id,
3471
+ target.size,
3472
+ previousSize,
3473
+ previousSnapSize === 0,
3474
+ true
3475
+ );
3356
3476
  });
3357
3477
  }
3478
+ clearWillChangeIfIdle() {
3479
+ if (this.animation.isAnimating || this.gesture?.isDragging) return;
3480
+ this.element.style.willChange = "";
3481
+ }
3358
3482
  haptic() {
3359
3483
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
3360
3484
  try {
@@ -3435,14 +3559,9 @@ var BottomSheetEngine = class {
3435
3559
  progress: this.computeProgress(this.size)
3436
3560
  });
3437
3561
  if (target.size > 0 && !this.lifecycle.isInstalled) {
3438
- this.emit("open", { id: target.id });
3439
- this.handleOpen();
3440
- notifyLinkedSheets(this.linkedSheets, this);
3441
- this.emit("opened", { id: target.id });
3562
+ this.emitOpenSequence(target.id);
3442
3563
  } else if (target.size === 0 && this.lifecycle.isInstalled) {
3443
- this.emit("close", void 0);
3444
- this.handleClose();
3445
- this.emit("closed", void 0);
3564
+ this.emitCloseSequence();
3446
3565
  }
3447
3566
  }
3448
3567
  handleClose() {
@@ -3452,6 +3571,38 @@ var BottomSheetEngine = class {
3452
3571
  }
3453
3572
  };
3454
3573
 
3574
+ // src/core/features/teleport.ts
3575
+ var resolveTeleportTarget = (target) => {
3576
+ if (!target) return null;
3577
+ if (typeof target !== "string") return target;
3578
+ if (typeof document === "undefined") return null;
3579
+ if (target === "body") return document.body;
3580
+ return document.querySelector(target);
3581
+ };
3582
+ var teleportElements = (els, target) => {
3583
+ const records = [];
3584
+ for (const el of els) {
3585
+ if (!el || el.parentNode === target) continue;
3586
+ records.push({ el, parent: el.parentNode, next: el.nextSibling });
3587
+ target.appendChild(el);
3588
+ }
3589
+ return () => {
3590
+ for (let i = records.length - 1; i >= 0; i--) {
3591
+ const { el, parent, next } = records[i];
3592
+ if (parent && parent.isConnected) {
3593
+ const before = next && next.parentNode === parent ? next : null;
3594
+ try {
3595
+ parent.insertBefore(el, before);
3596
+ } catch {
3597
+ el.parentNode?.removeChild(el);
3598
+ }
3599
+ } else {
3600
+ el.parentNode?.removeChild(el);
3601
+ }
3602
+ }
3603
+ };
3604
+ };
3605
+
3455
3606
  // src/solid/index.tsx
3456
3607
  var BottomSheet = (props) => {
3457
3608
  let sheetEl;
@@ -3468,6 +3619,7 @@ var BottomSheet = (props) => {
3468
3619
  progress: 0
3469
3620
  });
3470
3621
  let engine = null;
3622
+ let restoreTeleport = null;
3471
3623
  onMount(() => {
3472
3624
  if (!sheetEl) return;
3473
3625
  const engineOpts = {
@@ -3494,10 +3646,18 @@ var BottomSheet = (props) => {
3494
3646
  disableClose: props.disableClose,
3495
3647
  disableDrag: props.disableDrag,
3496
3648
  closeOnRouteChange: props.closeOnRouteChange,
3649
+ stackEffect: props.stackEffect,
3497
3650
  returnFocusTo: props.returnFocusTo
3498
3651
  };
3499
3652
  engine = new BottomSheetEngine(engineOpts);
3500
3653
  props.engineRef?.(engine);
3654
+ const teleportTarget = props.teleport === false ? null : resolveTeleportTarget(props.teleportTo);
3655
+ if (teleportTarget) {
3656
+ restoreTeleport = teleportElements(
3657
+ [backdropEl, scrimEl, sheetEl],
3658
+ teleportTarget
3659
+ );
3660
+ }
3501
3661
  const sync = () => {
3502
3662
  if (!engine) return;
3503
3663
  const s = engine.state;
@@ -3509,6 +3669,7 @@ var BottomSheet = (props) => {
3509
3669
  sync();
3510
3670
  props.onSnap?.(payload.id);
3511
3671
  });
3672
+ engine.on("before-snap", (payload) => props.onBeforeSnap?.(payload));
3512
3673
  engine.on("dragstart", sync);
3513
3674
  engine.on("dragend", sync);
3514
3675
  if (props.backdrop !== false && props.closeOnBackdrop !== false && backdropEl) {
@@ -3522,6 +3683,8 @@ var BottomSheet = (props) => {
3522
3683
  }
3523
3684
  });
3524
3685
  onCleanup(() => {
3686
+ restoreTeleport?.();
3687
+ restoreTeleport = null;
3525
3688
  engine?.destroy();
3526
3689
  engine = null;
3527
3690
  props.engineRef?.(null);
@@ -3549,6 +3712,36 @@ var BottomSheet = (props) => {
3549
3712
  { defer: true }
3550
3713
  )
3551
3714
  );
3715
+ createEffect(
3716
+ on(
3717
+ () => props.persistent,
3718
+ (value, prev) => {
3719
+ if (!engine || prev === void 0 || value === void 0) return;
3720
+ engine.setPersistent(value);
3721
+ },
3722
+ { defer: true }
3723
+ )
3724
+ );
3725
+ createEffect(
3726
+ on(
3727
+ () => props.disableClose,
3728
+ (value, prev) => {
3729
+ if (!engine || prev === void 0 || value === void 0) return;
3730
+ engine.setDisableClose(value);
3731
+ },
3732
+ { defer: true }
3733
+ )
3734
+ );
3735
+ createEffect(
3736
+ on(
3737
+ () => props.disableDrag,
3738
+ (value, prev) => {
3739
+ if (!engine || prev === void 0 || value === void 0) return;
3740
+ engine.setDisableDrag(value);
3741
+ },
3742
+ { defer: true }
3743
+ )
3744
+ );
3552
3745
  const showBackdrop = createMemo(() => props.backdrop !== false);
3553
3746
  const mode = createMemo(() => props.mode ?? "bottom");
3554
3747
  const ariaLabel = createMemo(() => props.ariaLabel ?? "Bottom sheet");
@@ -3592,6 +3785,16 @@ var BottomSheet = (props) => {
3592
3785
  {state().activeId}
3593
3786
  </span>
3594
3787
  </section>
3788
+ <Show when={props.leftButton}>
3789
+ <div class="bs-button-slot" data-side="left" data-mode={mode()}>
3790
+ {props.leftButton}
3791
+ </div>
3792
+ </Show>
3793
+ <Show when={props.rightButton}>
3794
+ <div class="bs-button-slot" data-side="right" data-mode={mode()}>
3795
+ {props.rightButton}
3796
+ </div>
3797
+ </Show>
3595
3798
  </div>;
3596
3799
  };
3597
3800
  export {