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