@capgo/capacitor-sheets 8.1.0 → 8.2.2

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/dist/index.mjs CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  applySheetOptions,
25
25
  getSheetFromElement,
26
26
  updateThemeColor
27
- } from "./chunk-75SRJO4V.mjs";
27
+ } from "./chunk-VT3FZ2IC.mjs";
28
28
  import {
29
29
  CSS,
30
30
  injectCapSheetStyles
@@ -32,7 +32,7 @@ import {
32
32
 
33
33
  // src/index.ts
34
34
  function initCapSheets() {
35
- import("./components-6FNKBOX7.mjs");
35
+ import("./components-Y762NURC.mjs");
36
36
  if (typeof document !== "undefined") {
37
37
  import("./styles-P4J3AKBN.mjs").then(({ injectCapSheetStyles: injectCapSheetStyles2 }) => injectCapSheetStyles2(document));
38
38
  }
@@ -531,6 +531,9 @@ function releaseThemeColorDimmer(documentRef) {
531
531
  }
532
532
 
533
533
  // src/core/sheet-controller.ts
534
+ var inertDepth = /* @__PURE__ */ new WeakMap();
535
+ var inertOriginal = /* @__PURE__ */ new WeakMap();
536
+ var inertHolders = /* @__PURE__ */ new WeakMap();
534
537
  var defaultOptions = {
535
538
  contentPlacement: "bottom",
536
539
  sheetRole: "dialog",
@@ -715,24 +718,32 @@ var SheetController = class {
715
718
  pointerTravel = null;
716
719
  connected = false;
717
720
  lockedScroll = false;
718
- inertedElements = /* @__PURE__ */ new Map();
721
+ inertHeldElements = /* @__PURE__ */ new Set();
722
+ remeasureFrame = null;
723
+ activeMotion = null;
724
+ pendingSettle = null;
725
+ /** Entering completion deferred by a drag that interrupted present(); runs once the sheet settles. */
726
+ deferredEnteringComplete = null;
727
+ /** Resolves the in-flight present() after entering lifecycle completes or is abandoned. */
728
+ settlePresentPromise = null;
719
729
  previousFocus = null;
720
730
  hasThemeDimmer = false;
721
731
  stackId = null;
722
732
  documentListenersAttached = false;
723
733
  viewListenersElement = null;
724
734
  visualViewportListenersAttached = false;
725
- handleVisualViewportResize = () => {
735
+ handleVisualViewportResize = (event) => {
726
736
  const view = this.parts.view;
727
737
  const visualViewport = this.root.ownerDocument.defaultView?.visualViewport;
728
738
  if (!view) return;
739
+ const viewportChanged = event !== void 0;
729
740
  if (!visualViewport || this.options.nativeFocusScrollPrevention === false) {
730
- this.setKeyboardOffset("0em");
741
+ this.setKeyboardOffset("0em", viewportChanged);
731
742
  return;
732
743
  }
733
744
  const layoutHeight = this.root.ownerDocument.documentElement.clientHeight;
734
745
  const keyboardOffset = Math.max(0, layoutHeight - visualViewport.height - visualViewport.offsetTop);
735
- this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`);
746
+ this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`, viewportChanged);
736
747
  };
737
748
  handleFocusIn = (event) => {
738
749
  if (!this.presented || this.options.nativeFocusScrollPrevention === false) return;
@@ -763,10 +774,18 @@ var SheetController = class {
763
774
  if (!this.presented || this.options.swipe === false || this.options.contentPlacement === "center") return;
764
775
  if (!(event.target instanceof Element)) return;
765
776
  if (!this.parts.content?.contains(event.target) && event.target !== this.parts.backdrop) return;
777
+ if (this.pointerTravel && !event.isPrimary) return;
766
778
  if (this.options.nativeEdgeSwipePrevention !== false && this.isNativeEdgePointer(event)) {
767
779
  event.preventDefault();
768
780
  }
781
+ this.syncOffsetFromRenderedPosition();
782
+ const interruptedMotion = this.activeMotion ? this.pendingSettle : null;
783
+ this.pendingSettle = null;
784
+ this.cancelActiveMotion();
785
+ this.applyOffset(this.currentOffsetPx, this.status);
769
786
  this.remeasure();
787
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
788
+ const startPos = axis === "y" ? event.clientY : event.clientX;
770
789
  this.pointerTravel = {
771
790
  id: event.pointerId,
772
791
  startOffsetPx: this.currentOffsetPx,
@@ -776,7 +795,9 @@ var SheetController = class {
776
795
  lastY: event.clientY,
777
796
  lastTime: performance.now(),
778
797
  velocity: 0,
779
- dragging: false
798
+ dragging: false,
799
+ samples: [{ t: performance.now(), pos: startPos }],
800
+ interruptedMotion
780
801
  };
781
802
  };
782
803
  handlePointerMove = (event) => {
@@ -804,6 +825,8 @@ var SheetController = class {
804
825
  travel.lastX = event.clientX;
805
826
  travel.lastY = event.clientY;
806
827
  travel.lastTime = now;
828
+ travel.samples.push({ t: now, pos: current });
829
+ if (travel.samples.length > 24) travel.samples.shift();
807
830
  const nextOffset = this.limitOffset(travel.startOffsetPx + delta);
808
831
  this.applyOffset(nextOffset, "dragging");
809
832
  };
@@ -815,9 +838,17 @@ var SheetController = class {
815
838
  currentTarget.releasePointerCapture(event.pointerId);
816
839
  }
817
840
  this.pointerTravel = null;
818
- if (!travel.dragging) return;
819
- const nearest = this.nearestDetentForOffset(this.currentOffsetPx, travel.velocity);
841
+ if (!travel.dragging) {
842
+ if (travel.interruptedMotion && this.presented) {
843
+ void this.runSettleMotion(travel.interruptedMotion);
844
+ }
845
+ return;
846
+ }
847
+ const nearest = this.nearestDetentForOffset(this.currentOffsetPx, this.releaseVelocity(travel));
820
848
  dispatch(this.root, "cap-sheet-drag-end", this.getTravelEvent());
849
+ if (travel.interruptedMotion?.status === "entering") {
850
+ this.deferredEnteringComplete = travel.interruptedMotion.complete;
851
+ }
821
852
  if (nearest === 0 && this.options.swipeDismissal !== false) {
822
853
  void this.dismiss({ source: "gesture" });
823
854
  return;
@@ -902,6 +933,8 @@ var SheetController = class {
902
933
  this.remeasure();
903
934
  this.applyOffset(this.detentOffsetsPx[0] || this.hiddenOffsetPx, this.status);
904
935
  this.updateDomState(false);
936
+ } else if (this.presented && affectsDismissedOffset(options)) {
937
+ this.scheduleRemeasure();
905
938
  }
906
939
  this.registerStack();
907
940
  }
@@ -956,6 +989,7 @@ var SheetController = class {
956
989
  }
957
990
  this.previousFocus = this.root.ownerDocument.activeElement;
958
991
  this.presented = true;
992
+ this.abandonDeferredEntering();
959
993
  this.activeDetent = clamp(options.detent ?? this.resolveInitialDetent(), 1, this.detentOffsetsPx.length - 1);
960
994
  this.updateDetentState();
961
995
  this.updateDomState(true);
@@ -964,15 +998,33 @@ var SheetController = class {
964
998
  this.retainThemeDimmer();
965
999
  this.setStatus("entering");
966
1000
  dispatch(this.root, "cap-sheet-present", this.getTravelEvent());
967
- await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, {
968
- ...this.options.enteringAnimationSettings || {},
969
- ...options.animation || {}
1001
+ const source = options.source || "programmatic";
1002
+ let resolvePresent;
1003
+ const presentLifecycleSettled = new Promise((resolve) => {
1004
+ resolvePresent = resolve;
970
1005
  });
971
- this.setStatus("idle");
972
- this.focusInitialElement();
973
- this.emitPresentedChange(true, options.source || "programmatic");
974
- this.emitActiveDetentChange(this.activeDetent, 0);
975
- this.updateStack();
1006
+ this.settlePresentPromise = () => {
1007
+ resolvePresent();
1008
+ };
1009
+ const complete = () => {
1010
+ this.setStatus("idle");
1011
+ this.focusInitialElement();
1012
+ this.emitPresentedChange(true, source);
1013
+ this.emitActiveDetentChange(this.activeDetent, 0);
1014
+ this.updateStack();
1015
+ this.releasePresentAwait();
1016
+ };
1017
+ const finished = await this.runSettleMotion({
1018
+ status: "entering",
1019
+ animation: {
1020
+ ...this.options.enteringAnimationSettings || {},
1021
+ ...options.animation || {}
1022
+ },
1023
+ complete
1024
+ });
1025
+ if (!finished) {
1026
+ await presentLifecycleSettled;
1027
+ }
976
1028
  }
977
1029
  /** Dismiss the sheet. */
978
1030
  async dismiss(options = {}) {
@@ -981,6 +1033,8 @@ var SheetController = class {
981
1033
  const previous = this.activeDetent;
982
1034
  this.presented = false;
983
1035
  this.activeDetent = 0;
1036
+ this.pendingSettle = null;
1037
+ this.abandonDeferredEntering();
984
1038
  this.updateDetentState();
985
1039
  this.setStatus("exiting");
986
1040
  dispatch(this.root, "cap-sheet-dismiss", this.getTravelEvent());
@@ -1021,15 +1075,49 @@ var SheetController = class {
1021
1075
  const previous = this.activeDetent;
1022
1076
  this.activeDetent = target;
1023
1077
  this.updateDetentState();
1024
- this.setStatus("settling");
1025
- await this.animateTo(this.detentOffsetsPx[target] || 0, {
1026
- ...this.options.steppingAnimationSettings || {},
1027
- ...options.animation || {}
1078
+ await this.runSettleMotion({
1079
+ status: "settling",
1080
+ animation: {
1081
+ ...this.options.steppingAnimationSettings || {},
1082
+ ...options.animation || {}
1083
+ },
1084
+ complete: () => {
1085
+ const enteringComplete = this.deferredEnteringComplete;
1086
+ if (enteringComplete) {
1087
+ this.deferredEnteringComplete = null;
1088
+ enteringComplete();
1089
+ return;
1090
+ }
1091
+ this.setStatus("idle");
1092
+ if (previous !== target) {
1093
+ this.emitActiveDetentChange(target, previous);
1094
+ }
1095
+ }
1028
1096
  });
1029
- this.setStatus("idle");
1030
- if (previous !== target) {
1031
- this.emitActiveDetentChange(target, previous);
1032
- }
1097
+ }
1098
+ /**
1099
+ * Animate to the active detent and run the completion once the motion finishes.
1100
+ * When the motion is cancelled by a pointerdown, the settle stays pending so a tap
1101
+ * that never becomes a drag can resume it.
1102
+ */
1103
+ releasePresentAwait() {
1104
+ if (!this.settlePresentPromise) return;
1105
+ const settle = this.settlePresentPromise;
1106
+ this.settlePresentPromise = null;
1107
+ settle();
1108
+ }
1109
+ abandonDeferredEntering() {
1110
+ this.deferredEnteringComplete = null;
1111
+ this.releasePresentAwait();
1112
+ }
1113
+ async runSettleMotion(motion) {
1114
+ this.setStatus(motion.status);
1115
+ this.pendingSettle = motion;
1116
+ const finished = await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, motion.animation);
1117
+ if (!finished) return false;
1118
+ if (this.pendingSettle === motion) this.pendingSettle = null;
1119
+ motion.complete();
1120
+ return true;
1033
1121
  }
1034
1122
  /** Step up or down one detent. */
1035
1123
  async step(direction = "up", options = {}) {
@@ -1161,7 +1249,7 @@ var SheetController = class {
1161
1249
  const resolved = resolveAnimation(settings);
1162
1250
  if (!content || shouldSkipMotion(resolved.skip)) {
1163
1251
  this.applyOffset(targetOffsetPx, "idle");
1164
- return;
1252
+ return true;
1165
1253
  }
1166
1254
  const startOffset = this.currentOffsetPx;
1167
1255
  const fromProgress = this.getProgressForOffset(startOffset);
@@ -1186,7 +1274,18 @@ var SheetController = class {
1186
1274
  );
1187
1275
  const started = performance.now();
1188
1276
  let frame;
1277
+ let cancelled = false;
1278
+ const cancelMotion = () => {
1279
+ if (cancelled) return;
1280
+ cancelled = true;
1281
+ cancelAnimationFrame(frame);
1282
+ contentAnimation?.cancel();
1283
+ backdropAnimation?.cancel();
1284
+ this.activeMotion = null;
1285
+ };
1286
+ this.activeMotion = { cancel: cancelMotion };
1189
1287
  const tick = () => {
1288
+ if (cancelled) return;
1190
1289
  const progress = duration === 0 ? 1 : clamp((performance.now() - started) / duration, 0, 1);
1191
1290
  const offset = startOffset + (targetOffsetPx - startOffset) * progress;
1192
1291
  this.currentOffsetPx = offset;
@@ -1200,10 +1299,13 @@ var SheetController = class {
1200
1299
  contentAnimation?.finished,
1201
1300
  backdropAnimation ? backdropAnimation.finished : Promise.resolve()
1202
1301
  ]);
1302
+ if (cancelled) return false;
1203
1303
  cancelAnimationFrame(frame);
1204
1304
  contentAnimation?.cancel();
1205
1305
  backdropAnimation?.cancel();
1306
+ this.activeMotion = null;
1206
1307
  this.applyOffset(targetOffsetPx, "idle");
1308
+ return true;
1207
1309
  }
1208
1310
  getProgressForOffset(offsetPx) {
1209
1311
  if (this.options.contentPlacement === "center") {
@@ -1427,47 +1529,152 @@ var SheetController = class {
1427
1529
  documentLockSnapshot.delete(doc);
1428
1530
  this.lockedScroll = false;
1429
1531
  }
1430
- setKeyboardOffset(value) {
1532
+ setKeyboardOffset(value, viewportChanged = false) {
1431
1533
  const view = this.parts.view;
1432
1534
  if (!view) return;
1433
- if (view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value) {
1434
- view.style.setProperty("--cap-sheet-keyboard-offset", value);
1435
- }
1535
+ const changed = view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value;
1536
+ if (changed) view.style.setProperty("--cap-sheet-keyboard-offset", value);
1537
+ if (changed || viewportChanged) this.scheduleRemeasure();
1538
+ }
1539
+ scheduleRemeasure() {
1436
1540
  if (!this.presented) return;
1437
- requestAnimationFrame(() => {
1541
+ if (this.remeasureFrame !== null) return;
1542
+ this.remeasureFrame = requestAnimationFrame(() => {
1543
+ this.remeasureFrame = null;
1438
1544
  if (!this.presented) return;
1439
1545
  this.remeasure();
1546
+ if (this.status === "dragging") return;
1440
1547
  this.applyOffset(this.detentOffsetsPx[this.activeDetent] || 0, this.status);
1441
1548
  });
1442
1549
  }
1550
+ cancelActiveMotion() {
1551
+ this.activeMotion?.cancel();
1552
+ this.activeMotion = null;
1553
+ }
1554
+ syncOffsetFromRenderedPosition() {
1555
+ const content = this.parts.content;
1556
+ if (!content || this.options.contentPlacement === "center") return;
1557
+ const offsetPx = this.readOffsetPxFromTransform(content);
1558
+ if (offsetPx === null) return;
1559
+ this.currentOffsetPx = offsetPx;
1560
+ }
1561
+ readOffsetPxFromTransform(content) {
1562
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
1563
+ const computed = this.root.ownerDocument.defaultView?.getComputedStyle(content).transform;
1564
+ const inline = content.style.transform;
1565
+ const transform = computed && computed !== "none" ? computed : inline && inline !== "none" ? inline : "";
1566
+ if (!transform) return this.currentOffsetPx;
1567
+ const translateMatch = transform.match(/translate3d\(\s*([^,]+)\s*,\s*([^,]+)\s*,/);
1568
+ if (translateMatch) {
1569
+ const raw = axis === "y" ? translateMatch[2] : translateMatch[1];
1570
+ const emMatch = raw.trim().match(/^(-?[\d.]+)em$/);
1571
+ if (emMatch) return Number.parseFloat(emMatch[1]) * getEmPx(content);
1572
+ const pxMatch = raw.trim().match(/^(-?[\d.]+)px$/);
1573
+ if (pxMatch) return Number.parseFloat(pxMatch[1]);
1574
+ }
1575
+ const matrixMatch = transform.match(/^matrix\(([^)]+)\)$/);
1576
+ if (matrixMatch) {
1577
+ const values = matrixMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1578
+ const translation = axis === "y" ? values[5] : values[4];
1579
+ if (Number.isFinite(translation)) return translation;
1580
+ }
1581
+ const matrix3dMatch = transform.match(/^matrix3d\(([^)]+)\)$/);
1582
+ if (matrix3dMatch) {
1583
+ const values = matrix3dMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1584
+ const translation = axis === "y" ? values[13] : values[12];
1585
+ if (Number.isFinite(translation)) return translation;
1586
+ }
1587
+ return null;
1588
+ }
1589
+ releaseVelocity(travel) {
1590
+ const windowMs = 100;
1591
+ const now = performance.now();
1592
+ const samples = travel.samples.filter((sample) => now - sample.t <= windowMs);
1593
+ if (samples.length < 2) return 0;
1594
+ const first = samples[0];
1595
+ const last = samples[samples.length - 1];
1596
+ const elapsed = Math.max(last.t - first.t, 1);
1597
+ return (last.pos - first.pos) / elapsed * 1e3;
1598
+ }
1599
+ getAllowedOutsideElements() {
1600
+ const allowed = /* @__PURE__ */ new Set();
1601
+ if (this.parts.view) allowed.add(this.parts.view);
1602
+ for (const element of this.parts.islands) allowed.add(element);
1603
+ for (const element of this.parts.externalOverlays) allowed.add(element);
1604
+ return allowed;
1605
+ }
1606
+ applyInertForElement(element, allowed) {
1607
+ const isAllowed = Array.from(allowed).some((candidate) => element === candidate || element.contains(candidate));
1608
+ if (isAllowed) {
1609
+ element.inert = false;
1610
+ element.removeAttribute("aria-hidden");
1611
+ return;
1612
+ }
1613
+ element.inert = true;
1614
+ element.setAttribute("aria-hidden", "true");
1615
+ }
1443
1616
  applyInert() {
1444
1617
  if (this.options.inertOutside === false || !this.parts.view) return;
1445
- const body = this.root.ownerDocument.body;
1446
- const allowed = /* @__PURE__ */ new Set([this.parts.view, ...this.parts.islands, ...this.parts.externalOverlays]);
1447
- this.clearInert();
1618
+ const doc = this.root.ownerDocument;
1619
+ const body = doc.body;
1620
+ const allowed = this.getAllowedOutsideElements();
1621
+ this.releaseInertHold();
1622
+ const holders = inertHolders.get(doc) || /* @__PURE__ */ new Set();
1623
+ holders.add(this);
1624
+ inertHolders.set(doc, holders);
1448
1625
  for (const child of Array.from(body.children)) {
1449
1626
  if (!(child instanceof HTMLElement)) continue;
1450
- this.inertedElements.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1451
- const isAllowed = Array.from(allowed).some((element) => child === element || child.contains(element));
1452
- if (isAllowed) {
1453
- child.inert = false;
1454
- child.removeAttribute("aria-hidden");
1455
- continue;
1627
+ const depth = inertDepth.get(child) ?? 0;
1628
+ if (depth === 0) {
1629
+ inertOriginal.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1456
1630
  }
1457
- child.inert = true;
1458
- child.setAttribute("aria-hidden", "true");
1631
+ inertDepth.set(child, depth + 1);
1632
+ this.inertHeldElements.add(child);
1633
+ this.applyInertForElement(child, allowed);
1459
1634
  }
1460
1635
  }
1461
1636
  clearInert() {
1462
- for (const [element, state] of this.inertedElements) {
1463
- element.inert = state.inert;
1464
- if (state.ariaHidden === null) {
1465
- element.removeAttribute("aria-hidden");
1637
+ this.releaseInertHold();
1638
+ }
1639
+ releaseInertHold() {
1640
+ if (this.inertHeldElements.size === 0) return;
1641
+ const doc = this.root.ownerDocument;
1642
+ const holders = inertHolders.get(doc);
1643
+ holders?.delete(this);
1644
+ if (holders?.size === 0) inertHolders.delete(doc);
1645
+ for (const element of this.inertHeldElements) {
1646
+ const depth = Math.max((inertDepth.get(element) ?? 1) - 1, 0);
1647
+ if (depth === 0) {
1648
+ inertDepth.delete(element);
1649
+ const snapshot = inertOriginal.get(element);
1650
+ if (snapshot) {
1651
+ element.inert = snapshot.inert;
1652
+ if (snapshot.ariaHidden === null) {
1653
+ element.removeAttribute("aria-hidden");
1654
+ } else {
1655
+ element.setAttribute("aria-hidden", snapshot.ariaHidden);
1656
+ }
1657
+ inertOriginal.delete(element);
1658
+ }
1466
1659
  } else {
1467
- element.setAttribute("aria-hidden", state.ariaHidden);
1660
+ inertDepth.set(element, depth);
1468
1661
  }
1469
1662
  }
1470
- this.inertedElements.clear();
1663
+ this.inertHeldElements.clear();
1664
+ this.reapplyDocumentInert(doc);
1665
+ }
1666
+ reapplyDocumentInert(doc) {
1667
+ const holders = inertHolders.get(doc);
1668
+ if (!holders?.size) return;
1669
+ const top = Array.from(holders).filter((controller) => controller.presented && controller.options.inertOutside !== false).sort((left, right) => right.getStackZIndex() - left.getStackZIndex())[0];
1670
+ if (!top) return;
1671
+ const allowed = top.getAllowedOutsideElements();
1672
+ const body = doc.body;
1673
+ for (const child of Array.from(body.children)) {
1674
+ if (!(child instanceof HTMLElement)) continue;
1675
+ if ((inertDepth.get(child) ?? 0) === 0) continue;
1676
+ top.applyInertForElement(child, allowed);
1677
+ }
1471
1678
  }
1472
1679
  retainThemeDimmer() {
1473
1680
  if (this.hasThemeDimmer || this.options.themeColorDimming !== "auto") return;