@capgo/capacitor-sheets 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -711,12 +711,15 @@ function tween(start, end, progress) {
711
711
  const next = startValue + (endValue - startValue) * progress;
712
712
  return `${next}${unit}`;
713
713
  }
714
- var defaultOptions, defaultAnimation, stackRegistry, documentLockDepth, documentLockSnapshot, unitlessInsetPattern, safeAreaEdges, SheetController;
714
+ var inertDepth, inertOriginal, inertHolders, defaultOptions, defaultAnimation, stackRegistry, documentLockDepth, documentLockSnapshot, unitlessInsetPattern, safeAreaEdges, SheetController;
715
715
  var init_sheet_controller = __esm({
716
716
  "src/core/sheet-controller.ts"() {
717
717
  "use strict";
718
718
  init_theme_color();
719
719
  init_utils();
720
+ inertDepth = /* @__PURE__ */ new WeakMap();
721
+ inertOriginal = /* @__PURE__ */ new WeakMap();
722
+ inertHolders = /* @__PURE__ */ new WeakMap();
720
723
  defaultOptions = {
721
724
  contentPlacement: "bottom",
722
725
  sheetRole: "dialog",
@@ -765,24 +768,32 @@ var init_sheet_controller = __esm({
765
768
  pointerTravel = null;
766
769
  connected = false;
767
770
  lockedScroll = false;
768
- inertedElements = /* @__PURE__ */ new Map();
771
+ inertHeldElements = /* @__PURE__ */ new Set();
772
+ remeasureFrame = null;
773
+ activeMotion = null;
774
+ pendingSettle = null;
775
+ /** Entering completion deferred by a drag that interrupted present(); runs once the sheet settles. */
776
+ deferredEnteringComplete = null;
777
+ /** Resolves the in-flight present() after entering lifecycle completes or is abandoned. */
778
+ settlePresentPromise = null;
769
779
  previousFocus = null;
770
780
  hasThemeDimmer = false;
771
781
  stackId = null;
772
782
  documentListenersAttached = false;
773
783
  viewListenersElement = null;
774
784
  visualViewportListenersAttached = false;
775
- handleVisualViewportResize = () => {
785
+ handleVisualViewportResize = (event) => {
776
786
  const view = this.parts.view;
777
787
  const visualViewport = this.root.ownerDocument.defaultView?.visualViewport;
778
788
  if (!view) return;
789
+ const viewportChanged = event !== void 0;
779
790
  if (!visualViewport || this.options.nativeFocusScrollPrevention === false) {
780
- this.setKeyboardOffset("0em");
791
+ this.setKeyboardOffset("0em", viewportChanged);
781
792
  return;
782
793
  }
783
794
  const layoutHeight = this.root.ownerDocument.documentElement.clientHeight;
784
795
  const keyboardOffset = Math.max(0, layoutHeight - visualViewport.height - visualViewport.offsetTop);
785
- this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`);
796
+ this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`, viewportChanged);
786
797
  };
787
798
  handleFocusIn = (event) => {
788
799
  if (!this.presented || this.options.nativeFocusScrollPrevention === false) return;
@@ -813,10 +824,18 @@ var init_sheet_controller = __esm({
813
824
  if (!this.presented || this.options.swipe === false || this.options.contentPlacement === "center") return;
814
825
  if (!(event.target instanceof Element)) return;
815
826
  if (!this.parts.content?.contains(event.target) && event.target !== this.parts.backdrop) return;
827
+ if (this.pointerTravel && !event.isPrimary) return;
816
828
  if (this.options.nativeEdgeSwipePrevention !== false && this.isNativeEdgePointer(event)) {
817
829
  event.preventDefault();
818
830
  }
831
+ this.syncOffsetFromRenderedPosition();
832
+ const interruptedMotion = this.activeMotion ? this.pendingSettle : null;
833
+ this.pendingSettle = null;
834
+ this.cancelActiveMotion();
835
+ this.applyOffset(this.currentOffsetPx, this.status);
819
836
  this.remeasure();
837
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
838
+ const startPos = axis === "y" ? event.clientY : event.clientX;
820
839
  this.pointerTravel = {
821
840
  id: event.pointerId,
822
841
  startOffsetPx: this.currentOffsetPx,
@@ -826,7 +845,9 @@ var init_sheet_controller = __esm({
826
845
  lastY: event.clientY,
827
846
  lastTime: performance.now(),
828
847
  velocity: 0,
829
- dragging: false
848
+ dragging: false,
849
+ samples: [{ t: performance.now(), pos: startPos }],
850
+ interruptedMotion
830
851
  };
831
852
  };
832
853
  handlePointerMove = (event) => {
@@ -854,6 +875,8 @@ var init_sheet_controller = __esm({
854
875
  travel.lastX = event.clientX;
855
876
  travel.lastY = event.clientY;
856
877
  travel.lastTime = now;
878
+ travel.samples.push({ t: now, pos: current });
879
+ if (travel.samples.length > 24) travel.samples.shift();
857
880
  const nextOffset = this.limitOffset(travel.startOffsetPx + delta);
858
881
  this.applyOffset(nextOffset, "dragging");
859
882
  };
@@ -865,9 +888,17 @@ var init_sheet_controller = __esm({
865
888
  currentTarget.releasePointerCapture(event.pointerId);
866
889
  }
867
890
  this.pointerTravel = null;
868
- if (!travel.dragging) return;
869
- const nearest = this.nearestDetentForOffset(this.currentOffsetPx, travel.velocity);
891
+ if (!travel.dragging) {
892
+ if (travel.interruptedMotion && this.presented) {
893
+ void this.runSettleMotion(travel.interruptedMotion);
894
+ }
895
+ return;
896
+ }
897
+ const nearest = this.nearestDetentForOffset(this.currentOffsetPx, this.releaseVelocity(travel));
870
898
  dispatch(this.root, "cap-sheet-drag-end", this.getTravelEvent());
899
+ if (travel.interruptedMotion?.status === "entering") {
900
+ this.deferredEnteringComplete = travel.interruptedMotion.complete;
901
+ }
871
902
  if (nearest === 0 && this.options.swipeDismissal !== false) {
872
903
  void this.dismiss({ source: "gesture" });
873
904
  return;
@@ -952,6 +983,8 @@ var init_sheet_controller = __esm({
952
983
  this.remeasure();
953
984
  this.applyOffset(this.detentOffsetsPx[0] || this.hiddenOffsetPx, this.status);
954
985
  this.updateDomState(false);
986
+ } else if (this.presented && affectsDismissedOffset(options)) {
987
+ this.scheduleRemeasure();
955
988
  }
956
989
  this.registerStack();
957
990
  }
@@ -1006,6 +1039,7 @@ var init_sheet_controller = __esm({
1006
1039
  }
1007
1040
  this.previousFocus = this.root.ownerDocument.activeElement;
1008
1041
  this.presented = true;
1042
+ this.abandonDeferredEntering();
1009
1043
  this.activeDetent = clamp(options.detent ?? this.resolveInitialDetent(), 1, this.detentOffsetsPx.length - 1);
1010
1044
  this.updateDetentState();
1011
1045
  this.updateDomState(true);
@@ -1014,15 +1048,33 @@ var init_sheet_controller = __esm({
1014
1048
  this.retainThemeDimmer();
1015
1049
  this.setStatus("entering");
1016
1050
  dispatch(this.root, "cap-sheet-present", this.getTravelEvent());
1017
- await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, {
1018
- ...this.options.enteringAnimationSettings || {},
1019
- ...options.animation || {}
1051
+ const source = options.source || "programmatic";
1052
+ let resolvePresent;
1053
+ const presentLifecycleSettled = new Promise((resolve) => {
1054
+ resolvePresent = resolve;
1020
1055
  });
1021
- this.setStatus("idle");
1022
- this.focusInitialElement();
1023
- this.emitPresentedChange(true, options.source || "programmatic");
1024
- this.emitActiveDetentChange(this.activeDetent, 0);
1025
- this.updateStack();
1056
+ this.settlePresentPromise = () => {
1057
+ resolvePresent();
1058
+ };
1059
+ const complete = () => {
1060
+ this.setStatus("idle");
1061
+ this.focusInitialElement();
1062
+ this.emitPresentedChange(true, source);
1063
+ this.emitActiveDetentChange(this.activeDetent, 0);
1064
+ this.updateStack();
1065
+ this.releasePresentAwait();
1066
+ };
1067
+ const finished = await this.runSettleMotion({
1068
+ status: "entering",
1069
+ animation: {
1070
+ ...this.options.enteringAnimationSettings || {},
1071
+ ...options.animation || {}
1072
+ },
1073
+ complete
1074
+ });
1075
+ if (!finished) {
1076
+ await presentLifecycleSettled;
1077
+ }
1026
1078
  }
1027
1079
  /** Dismiss the sheet. */
1028
1080
  async dismiss(options = {}) {
@@ -1031,6 +1083,8 @@ var init_sheet_controller = __esm({
1031
1083
  const previous = this.activeDetent;
1032
1084
  this.presented = false;
1033
1085
  this.activeDetent = 0;
1086
+ this.pendingSettle = null;
1087
+ this.abandonDeferredEntering();
1034
1088
  this.updateDetentState();
1035
1089
  this.setStatus("exiting");
1036
1090
  dispatch(this.root, "cap-sheet-dismiss", this.getTravelEvent());
@@ -1071,15 +1125,49 @@ var init_sheet_controller = __esm({
1071
1125
  const previous = this.activeDetent;
1072
1126
  this.activeDetent = target;
1073
1127
  this.updateDetentState();
1074
- this.setStatus("settling");
1075
- await this.animateTo(this.detentOffsetsPx[target] || 0, {
1076
- ...this.options.steppingAnimationSettings || {},
1077
- ...options.animation || {}
1128
+ await this.runSettleMotion({
1129
+ status: "settling",
1130
+ animation: {
1131
+ ...this.options.steppingAnimationSettings || {},
1132
+ ...options.animation || {}
1133
+ },
1134
+ complete: () => {
1135
+ const enteringComplete = this.deferredEnteringComplete;
1136
+ if (enteringComplete) {
1137
+ this.deferredEnteringComplete = null;
1138
+ enteringComplete();
1139
+ return;
1140
+ }
1141
+ this.setStatus("idle");
1142
+ if (previous !== target) {
1143
+ this.emitActiveDetentChange(target, previous);
1144
+ }
1145
+ }
1078
1146
  });
1079
- this.setStatus("idle");
1080
- if (previous !== target) {
1081
- this.emitActiveDetentChange(target, previous);
1082
- }
1147
+ }
1148
+ /**
1149
+ * Animate to the active detent and run the completion once the motion finishes.
1150
+ * When the motion is cancelled by a pointerdown, the settle stays pending so a tap
1151
+ * that never becomes a drag can resume it.
1152
+ */
1153
+ releasePresentAwait() {
1154
+ if (!this.settlePresentPromise) return;
1155
+ const settle = this.settlePresentPromise;
1156
+ this.settlePresentPromise = null;
1157
+ settle();
1158
+ }
1159
+ abandonDeferredEntering() {
1160
+ this.deferredEnteringComplete = null;
1161
+ this.releasePresentAwait();
1162
+ }
1163
+ async runSettleMotion(motion) {
1164
+ this.setStatus(motion.status);
1165
+ this.pendingSettle = motion;
1166
+ const finished = await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, motion.animation);
1167
+ if (!finished) return false;
1168
+ if (this.pendingSettle === motion) this.pendingSettle = null;
1169
+ motion.complete();
1170
+ return true;
1083
1171
  }
1084
1172
  /** Step up or down one detent. */
1085
1173
  async step(direction = "up", options = {}) {
@@ -1211,7 +1299,7 @@ var init_sheet_controller = __esm({
1211
1299
  const resolved = resolveAnimation(settings);
1212
1300
  if (!content || shouldSkipMotion(resolved.skip)) {
1213
1301
  this.applyOffset(targetOffsetPx, "idle");
1214
- return;
1302
+ return true;
1215
1303
  }
1216
1304
  const startOffset = this.currentOffsetPx;
1217
1305
  const fromProgress = this.getProgressForOffset(startOffset);
@@ -1236,7 +1324,18 @@ var init_sheet_controller = __esm({
1236
1324
  );
1237
1325
  const started = performance.now();
1238
1326
  let frame;
1327
+ let cancelled = false;
1328
+ const cancelMotion = () => {
1329
+ if (cancelled) return;
1330
+ cancelled = true;
1331
+ cancelAnimationFrame(frame);
1332
+ contentAnimation?.cancel();
1333
+ backdropAnimation?.cancel();
1334
+ this.activeMotion = null;
1335
+ };
1336
+ this.activeMotion = { cancel: cancelMotion };
1239
1337
  const tick = () => {
1338
+ if (cancelled) return;
1240
1339
  const progress = duration === 0 ? 1 : clamp((performance.now() - started) / duration, 0, 1);
1241
1340
  const offset = startOffset + (targetOffsetPx - startOffset) * progress;
1242
1341
  this.currentOffsetPx = offset;
@@ -1250,10 +1349,13 @@ var init_sheet_controller = __esm({
1250
1349
  contentAnimation?.finished,
1251
1350
  backdropAnimation ? backdropAnimation.finished : Promise.resolve()
1252
1351
  ]);
1352
+ if (cancelled) return false;
1253
1353
  cancelAnimationFrame(frame);
1254
1354
  contentAnimation?.cancel();
1255
1355
  backdropAnimation?.cancel();
1356
+ this.activeMotion = null;
1256
1357
  this.applyOffset(targetOffsetPx, "idle");
1358
+ return true;
1257
1359
  }
1258
1360
  getProgressForOffset(offsetPx) {
1259
1361
  if (this.options.contentPlacement === "center") {
@@ -1477,47 +1579,152 @@ var init_sheet_controller = __esm({
1477
1579
  documentLockSnapshot.delete(doc);
1478
1580
  this.lockedScroll = false;
1479
1581
  }
1480
- setKeyboardOffset(value) {
1582
+ setKeyboardOffset(value, viewportChanged = false) {
1481
1583
  const view = this.parts.view;
1482
1584
  if (!view) return;
1483
- if (view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value) {
1484
- view.style.setProperty("--cap-sheet-keyboard-offset", value);
1485
- }
1585
+ const changed = view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value;
1586
+ if (changed) view.style.setProperty("--cap-sheet-keyboard-offset", value);
1587
+ if (changed || viewportChanged) this.scheduleRemeasure();
1588
+ }
1589
+ scheduleRemeasure() {
1486
1590
  if (!this.presented) return;
1487
- requestAnimationFrame(() => {
1591
+ if (this.remeasureFrame !== null) return;
1592
+ this.remeasureFrame = requestAnimationFrame(() => {
1593
+ this.remeasureFrame = null;
1488
1594
  if (!this.presented) return;
1489
1595
  this.remeasure();
1596
+ if (this.status === "dragging") return;
1490
1597
  this.applyOffset(this.detentOffsetsPx[this.activeDetent] || 0, this.status);
1491
1598
  });
1492
1599
  }
1600
+ cancelActiveMotion() {
1601
+ this.activeMotion?.cancel();
1602
+ this.activeMotion = null;
1603
+ }
1604
+ syncOffsetFromRenderedPosition() {
1605
+ const content = this.parts.content;
1606
+ if (!content || this.options.contentPlacement === "center") return;
1607
+ const offsetPx = this.readOffsetPxFromTransform(content);
1608
+ if (offsetPx === null) return;
1609
+ this.currentOffsetPx = offsetPx;
1610
+ }
1611
+ readOffsetPxFromTransform(content) {
1612
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
1613
+ const computed = this.root.ownerDocument.defaultView?.getComputedStyle(content).transform;
1614
+ const inline = content.style.transform;
1615
+ const transform = computed && computed !== "none" ? computed : inline && inline !== "none" ? inline : "";
1616
+ if (!transform) return this.currentOffsetPx;
1617
+ const translateMatch = transform.match(/translate3d\(\s*([^,]+)\s*,\s*([^,]+)\s*,/);
1618
+ if (translateMatch) {
1619
+ const raw = axis === "y" ? translateMatch[2] : translateMatch[1];
1620
+ const emMatch = raw.trim().match(/^(-?[\d.]+)em$/);
1621
+ if (emMatch) return Number.parseFloat(emMatch[1]) * getEmPx(content);
1622
+ const pxMatch = raw.trim().match(/^(-?[\d.]+)px$/);
1623
+ if (pxMatch) return Number.parseFloat(pxMatch[1]);
1624
+ }
1625
+ const matrixMatch = transform.match(/^matrix\(([^)]+)\)$/);
1626
+ if (matrixMatch) {
1627
+ const values = matrixMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1628
+ const translation = axis === "y" ? values[5] : values[4];
1629
+ if (Number.isFinite(translation)) return translation;
1630
+ }
1631
+ const matrix3dMatch = transform.match(/^matrix3d\(([^)]+)\)$/);
1632
+ if (matrix3dMatch) {
1633
+ const values = matrix3dMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1634
+ const translation = axis === "y" ? values[13] : values[12];
1635
+ if (Number.isFinite(translation)) return translation;
1636
+ }
1637
+ return null;
1638
+ }
1639
+ releaseVelocity(travel) {
1640
+ const windowMs = 100;
1641
+ const now = performance.now();
1642
+ const samples = travel.samples.filter((sample) => now - sample.t <= windowMs);
1643
+ if (samples.length < 2) return 0;
1644
+ const first = samples[0];
1645
+ const last = samples[samples.length - 1];
1646
+ const elapsed = Math.max(last.t - first.t, 1);
1647
+ return (last.pos - first.pos) / elapsed * 1e3;
1648
+ }
1649
+ getAllowedOutsideElements() {
1650
+ const allowed = /* @__PURE__ */ new Set();
1651
+ if (this.parts.view) allowed.add(this.parts.view);
1652
+ for (const element of this.parts.islands) allowed.add(element);
1653
+ for (const element of this.parts.externalOverlays) allowed.add(element);
1654
+ return allowed;
1655
+ }
1656
+ applyInertForElement(element, allowed) {
1657
+ const isAllowed = Array.from(allowed).some((candidate) => element === candidate || element.contains(candidate));
1658
+ if (isAllowed) {
1659
+ element.inert = false;
1660
+ element.removeAttribute("aria-hidden");
1661
+ return;
1662
+ }
1663
+ element.inert = true;
1664
+ element.setAttribute("aria-hidden", "true");
1665
+ }
1493
1666
  applyInert() {
1494
1667
  if (this.options.inertOutside === false || !this.parts.view) return;
1495
- const body = this.root.ownerDocument.body;
1496
- const allowed = /* @__PURE__ */ new Set([this.parts.view, ...this.parts.islands, ...this.parts.externalOverlays]);
1497
- this.clearInert();
1668
+ const doc = this.root.ownerDocument;
1669
+ const body = doc.body;
1670
+ const allowed = this.getAllowedOutsideElements();
1671
+ this.releaseInertHold();
1672
+ const holders = inertHolders.get(doc) || /* @__PURE__ */ new Set();
1673
+ holders.add(this);
1674
+ inertHolders.set(doc, holders);
1498
1675
  for (const child of Array.from(body.children)) {
1499
1676
  if (!(child instanceof HTMLElement)) continue;
1500
- this.inertedElements.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1501
- const isAllowed = Array.from(allowed).some((element) => child === element || child.contains(element));
1502
- if (isAllowed) {
1503
- child.inert = false;
1504
- child.removeAttribute("aria-hidden");
1505
- continue;
1677
+ const depth = inertDepth.get(child) ?? 0;
1678
+ if (depth === 0) {
1679
+ inertOriginal.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1506
1680
  }
1507
- child.inert = true;
1508
- child.setAttribute("aria-hidden", "true");
1681
+ inertDepth.set(child, depth + 1);
1682
+ this.inertHeldElements.add(child);
1683
+ this.applyInertForElement(child, allowed);
1509
1684
  }
1510
1685
  }
1511
1686
  clearInert() {
1512
- for (const [element, state] of this.inertedElements) {
1513
- element.inert = state.inert;
1514
- if (state.ariaHidden === null) {
1515
- element.removeAttribute("aria-hidden");
1687
+ this.releaseInertHold();
1688
+ }
1689
+ releaseInertHold() {
1690
+ if (this.inertHeldElements.size === 0) return;
1691
+ const doc = this.root.ownerDocument;
1692
+ const holders = inertHolders.get(doc);
1693
+ holders?.delete(this);
1694
+ if (holders?.size === 0) inertHolders.delete(doc);
1695
+ for (const element of this.inertHeldElements) {
1696
+ const depth = Math.max((inertDepth.get(element) ?? 1) - 1, 0);
1697
+ if (depth === 0) {
1698
+ inertDepth.delete(element);
1699
+ const snapshot = inertOriginal.get(element);
1700
+ if (snapshot) {
1701
+ element.inert = snapshot.inert;
1702
+ if (snapshot.ariaHidden === null) {
1703
+ element.removeAttribute("aria-hidden");
1704
+ } else {
1705
+ element.setAttribute("aria-hidden", snapshot.ariaHidden);
1706
+ }
1707
+ inertOriginal.delete(element);
1708
+ }
1516
1709
  } else {
1517
- element.setAttribute("aria-hidden", state.ariaHidden);
1710
+ inertDepth.set(element, depth);
1518
1711
  }
1519
1712
  }
1520
- this.inertedElements.clear();
1713
+ this.inertHeldElements.clear();
1714
+ this.reapplyDocumentInert(doc);
1715
+ }
1716
+ reapplyDocumentInert(doc) {
1717
+ const holders = inertHolders.get(doc);
1718
+ if (!holders?.size) return;
1719
+ const top = Array.from(holders).filter((controller) => controller.presented && controller.options.inertOutside !== false).sort((left, right) => right.getStackZIndex() - left.getStackZIndex())[0];
1720
+ if (!top) return;
1721
+ const allowed = top.getAllowedOutsideElements();
1722
+ const body = doc.body;
1723
+ for (const child of Array.from(body.children)) {
1724
+ if (!(child instanceof HTMLElement)) continue;
1725
+ if ((inertDepth.get(child) ?? 0) === 0) continue;
1726
+ top.applyInertForElement(child, allowed);
1727
+ }
1521
1728
  }
1522
1729
  retainThemeDimmer() {
1523
1730
  if (this.hasThemeDimmer || this.options.themeColorDimming !== "auto") return;