@capgo/capacitor-sheets 8.1.0 → 8.2.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.
@@ -503,6 +503,9 @@ function releaseThemeColorDimmer(documentRef) {
503
503
  }
504
504
 
505
505
  // src/core/sheet-controller.ts
506
+ var inertDepth = /* @__PURE__ */ new WeakMap();
507
+ var inertOriginal = /* @__PURE__ */ new WeakMap();
508
+ var inertHolders = /* @__PURE__ */ new WeakMap();
506
509
  var defaultOptions = {
507
510
  contentPlacement: "bottom",
508
511
  sheetRole: "dialog",
@@ -687,24 +690,32 @@ var SheetController = class {
687
690
  pointerTravel = null;
688
691
  connected = false;
689
692
  lockedScroll = false;
690
- inertedElements = /* @__PURE__ */ new Map();
693
+ inertHeldElements = /* @__PURE__ */ new Set();
694
+ remeasureFrame = null;
695
+ activeMotion = null;
696
+ pendingSettle = null;
697
+ /** Entering completion deferred by a drag that interrupted present(); runs once the sheet settles. */
698
+ deferredEnteringComplete = null;
699
+ /** Resolves the in-flight present() after entering lifecycle completes or is abandoned. */
700
+ settlePresentPromise = null;
691
701
  previousFocus = null;
692
702
  hasThemeDimmer = false;
693
703
  stackId = null;
694
704
  documentListenersAttached = false;
695
705
  viewListenersElement = null;
696
706
  visualViewportListenersAttached = false;
697
- handleVisualViewportResize = () => {
707
+ handleVisualViewportResize = (event) => {
698
708
  const view = this.parts.view;
699
709
  const visualViewport = this.root.ownerDocument.defaultView?.visualViewport;
700
710
  if (!view) return;
711
+ const viewportChanged = event !== void 0;
701
712
  if (!visualViewport || this.options.nativeFocusScrollPrevention === false) {
702
- this.setKeyboardOffset("0em");
713
+ this.setKeyboardOffset("0em", viewportChanged);
703
714
  return;
704
715
  }
705
716
  const layoutHeight = this.root.ownerDocument.documentElement.clientHeight;
706
717
  const keyboardOffset = Math.max(0, layoutHeight - visualViewport.height - visualViewport.offsetTop);
707
- this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`);
718
+ this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`, viewportChanged);
708
719
  };
709
720
  handleFocusIn = (event) => {
710
721
  if (!this.presented || this.options.nativeFocusScrollPrevention === false) return;
@@ -735,10 +746,18 @@ var SheetController = class {
735
746
  if (!this.presented || this.options.swipe === false || this.options.contentPlacement === "center") return;
736
747
  if (!(event.target instanceof Element)) return;
737
748
  if (!this.parts.content?.contains(event.target) && event.target !== this.parts.backdrop) return;
749
+ if (this.pointerTravel && !event.isPrimary) return;
738
750
  if (this.options.nativeEdgeSwipePrevention !== false && this.isNativeEdgePointer(event)) {
739
751
  event.preventDefault();
740
752
  }
753
+ this.syncOffsetFromRenderedPosition();
754
+ const interruptedMotion = this.activeMotion ? this.pendingSettle : null;
755
+ this.pendingSettle = null;
756
+ this.cancelActiveMotion();
757
+ this.applyOffset(this.currentOffsetPx, this.status);
741
758
  this.remeasure();
759
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
760
+ const startPos = axis === "y" ? event.clientY : event.clientX;
742
761
  this.pointerTravel = {
743
762
  id: event.pointerId,
744
763
  startOffsetPx: this.currentOffsetPx,
@@ -748,7 +767,9 @@ var SheetController = class {
748
767
  lastY: event.clientY,
749
768
  lastTime: performance.now(),
750
769
  velocity: 0,
751
- dragging: false
770
+ dragging: false,
771
+ samples: [{ t: performance.now(), pos: startPos }],
772
+ interruptedMotion
752
773
  };
753
774
  };
754
775
  handlePointerMove = (event) => {
@@ -776,6 +797,8 @@ var SheetController = class {
776
797
  travel.lastX = event.clientX;
777
798
  travel.lastY = event.clientY;
778
799
  travel.lastTime = now;
800
+ travel.samples.push({ t: now, pos: current });
801
+ if (travel.samples.length > 24) travel.samples.shift();
779
802
  const nextOffset = this.limitOffset(travel.startOffsetPx + delta);
780
803
  this.applyOffset(nextOffset, "dragging");
781
804
  };
@@ -787,9 +810,17 @@ var SheetController = class {
787
810
  currentTarget.releasePointerCapture(event.pointerId);
788
811
  }
789
812
  this.pointerTravel = null;
790
- if (!travel.dragging) return;
791
- const nearest = this.nearestDetentForOffset(this.currentOffsetPx, travel.velocity);
813
+ if (!travel.dragging) {
814
+ if (travel.interruptedMotion && this.presented) {
815
+ void this.runSettleMotion(travel.interruptedMotion);
816
+ }
817
+ return;
818
+ }
819
+ const nearest = this.nearestDetentForOffset(this.currentOffsetPx, this.releaseVelocity(travel));
792
820
  dispatch(this.root, "cap-sheet-drag-end", this.getTravelEvent());
821
+ if (travel.interruptedMotion?.status === "entering") {
822
+ this.deferredEnteringComplete = travel.interruptedMotion.complete;
823
+ }
793
824
  if (nearest === 0 && this.options.swipeDismissal !== false) {
794
825
  void this.dismiss({ source: "gesture" });
795
826
  return;
@@ -874,6 +905,8 @@ var SheetController = class {
874
905
  this.remeasure();
875
906
  this.applyOffset(this.detentOffsetsPx[0] || this.hiddenOffsetPx, this.status);
876
907
  this.updateDomState(false);
908
+ } else if (this.presented && affectsDismissedOffset(options)) {
909
+ this.scheduleRemeasure();
877
910
  }
878
911
  this.registerStack();
879
912
  }
@@ -928,6 +961,7 @@ var SheetController = class {
928
961
  }
929
962
  this.previousFocus = this.root.ownerDocument.activeElement;
930
963
  this.presented = true;
964
+ this.abandonDeferredEntering();
931
965
  this.activeDetent = clamp(options.detent ?? this.resolveInitialDetent(), 1, this.detentOffsetsPx.length - 1);
932
966
  this.updateDetentState();
933
967
  this.updateDomState(true);
@@ -936,15 +970,33 @@ var SheetController = class {
936
970
  this.retainThemeDimmer();
937
971
  this.setStatus("entering");
938
972
  dispatch(this.root, "cap-sheet-present", this.getTravelEvent());
939
- await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, {
940
- ...this.options.enteringAnimationSettings || {},
941
- ...options.animation || {}
973
+ const source = options.source || "programmatic";
974
+ let resolvePresent;
975
+ const presentLifecycleSettled = new Promise((resolve) => {
976
+ resolvePresent = resolve;
942
977
  });
943
- this.setStatus("idle");
944
- this.focusInitialElement();
945
- this.emitPresentedChange(true, options.source || "programmatic");
946
- this.emitActiveDetentChange(this.activeDetent, 0);
947
- this.updateStack();
978
+ this.settlePresentPromise = () => {
979
+ resolvePresent();
980
+ };
981
+ const complete = () => {
982
+ this.setStatus("idle");
983
+ this.focusInitialElement();
984
+ this.emitPresentedChange(true, source);
985
+ this.emitActiveDetentChange(this.activeDetent, 0);
986
+ this.updateStack();
987
+ this.releasePresentAwait();
988
+ };
989
+ const finished = await this.runSettleMotion({
990
+ status: "entering",
991
+ animation: {
992
+ ...this.options.enteringAnimationSettings || {},
993
+ ...options.animation || {}
994
+ },
995
+ complete
996
+ });
997
+ if (!finished) {
998
+ await presentLifecycleSettled;
999
+ }
948
1000
  }
949
1001
  /** Dismiss the sheet. */
950
1002
  async dismiss(options = {}) {
@@ -953,6 +1005,8 @@ var SheetController = class {
953
1005
  const previous = this.activeDetent;
954
1006
  this.presented = false;
955
1007
  this.activeDetent = 0;
1008
+ this.pendingSettle = null;
1009
+ this.abandonDeferredEntering();
956
1010
  this.updateDetentState();
957
1011
  this.setStatus("exiting");
958
1012
  dispatch(this.root, "cap-sheet-dismiss", this.getTravelEvent());
@@ -993,15 +1047,49 @@ var SheetController = class {
993
1047
  const previous = this.activeDetent;
994
1048
  this.activeDetent = target;
995
1049
  this.updateDetentState();
996
- this.setStatus("settling");
997
- await this.animateTo(this.detentOffsetsPx[target] || 0, {
998
- ...this.options.steppingAnimationSettings || {},
999
- ...options.animation || {}
1050
+ await this.runSettleMotion({
1051
+ status: "settling",
1052
+ animation: {
1053
+ ...this.options.steppingAnimationSettings || {},
1054
+ ...options.animation || {}
1055
+ },
1056
+ complete: () => {
1057
+ const enteringComplete = this.deferredEnteringComplete;
1058
+ if (enteringComplete) {
1059
+ this.deferredEnteringComplete = null;
1060
+ enteringComplete();
1061
+ return;
1062
+ }
1063
+ this.setStatus("idle");
1064
+ if (previous !== target) {
1065
+ this.emitActiveDetentChange(target, previous);
1066
+ }
1067
+ }
1000
1068
  });
1001
- this.setStatus("idle");
1002
- if (previous !== target) {
1003
- this.emitActiveDetentChange(target, previous);
1004
- }
1069
+ }
1070
+ /**
1071
+ * Animate to the active detent and run the completion once the motion finishes.
1072
+ * When the motion is cancelled by a pointerdown, the settle stays pending so a tap
1073
+ * that never becomes a drag can resume it.
1074
+ */
1075
+ releasePresentAwait() {
1076
+ if (!this.settlePresentPromise) return;
1077
+ const settle = this.settlePresentPromise;
1078
+ this.settlePresentPromise = null;
1079
+ settle();
1080
+ }
1081
+ abandonDeferredEntering() {
1082
+ this.deferredEnteringComplete = null;
1083
+ this.releasePresentAwait();
1084
+ }
1085
+ async runSettleMotion(motion) {
1086
+ this.setStatus(motion.status);
1087
+ this.pendingSettle = motion;
1088
+ const finished = await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, motion.animation);
1089
+ if (!finished) return false;
1090
+ if (this.pendingSettle === motion) this.pendingSettle = null;
1091
+ motion.complete();
1092
+ return true;
1005
1093
  }
1006
1094
  /** Step up or down one detent. */
1007
1095
  async step(direction = "up", options = {}) {
@@ -1133,7 +1221,7 @@ var SheetController = class {
1133
1221
  const resolved = resolveAnimation(settings);
1134
1222
  if (!content || shouldSkipMotion(resolved.skip)) {
1135
1223
  this.applyOffset(targetOffsetPx, "idle");
1136
- return;
1224
+ return true;
1137
1225
  }
1138
1226
  const startOffset = this.currentOffsetPx;
1139
1227
  const fromProgress = this.getProgressForOffset(startOffset);
@@ -1158,7 +1246,18 @@ var SheetController = class {
1158
1246
  );
1159
1247
  const started = performance.now();
1160
1248
  let frame;
1249
+ let cancelled = false;
1250
+ const cancelMotion = () => {
1251
+ if (cancelled) return;
1252
+ cancelled = true;
1253
+ cancelAnimationFrame(frame);
1254
+ contentAnimation?.cancel();
1255
+ backdropAnimation?.cancel();
1256
+ this.activeMotion = null;
1257
+ };
1258
+ this.activeMotion = { cancel: cancelMotion };
1161
1259
  const tick = () => {
1260
+ if (cancelled) return;
1162
1261
  const progress = duration === 0 ? 1 : clamp((performance.now() - started) / duration, 0, 1);
1163
1262
  const offset = startOffset + (targetOffsetPx - startOffset) * progress;
1164
1263
  this.currentOffsetPx = offset;
@@ -1172,10 +1271,13 @@ var SheetController = class {
1172
1271
  contentAnimation?.finished,
1173
1272
  backdropAnimation ? backdropAnimation.finished : Promise.resolve()
1174
1273
  ]);
1274
+ if (cancelled) return false;
1175
1275
  cancelAnimationFrame(frame);
1176
1276
  contentAnimation?.cancel();
1177
1277
  backdropAnimation?.cancel();
1278
+ this.activeMotion = null;
1178
1279
  this.applyOffset(targetOffsetPx, "idle");
1280
+ return true;
1179
1281
  }
1180
1282
  getProgressForOffset(offsetPx) {
1181
1283
  if (this.options.contentPlacement === "center") {
@@ -1399,47 +1501,152 @@ var SheetController = class {
1399
1501
  documentLockSnapshot.delete(doc);
1400
1502
  this.lockedScroll = false;
1401
1503
  }
1402
- setKeyboardOffset(value) {
1504
+ setKeyboardOffset(value, viewportChanged = false) {
1403
1505
  const view = this.parts.view;
1404
1506
  if (!view) return;
1405
- if (view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value) {
1406
- view.style.setProperty("--cap-sheet-keyboard-offset", value);
1407
- }
1507
+ const changed = view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value;
1508
+ if (changed) view.style.setProperty("--cap-sheet-keyboard-offset", value);
1509
+ if (changed || viewportChanged) this.scheduleRemeasure();
1510
+ }
1511
+ scheduleRemeasure() {
1408
1512
  if (!this.presented) return;
1409
- requestAnimationFrame(() => {
1513
+ if (this.remeasureFrame !== null) return;
1514
+ this.remeasureFrame = requestAnimationFrame(() => {
1515
+ this.remeasureFrame = null;
1410
1516
  if (!this.presented) return;
1411
1517
  this.remeasure();
1518
+ if (this.status === "dragging") return;
1412
1519
  this.applyOffset(this.detentOffsetsPx[this.activeDetent] || 0, this.status);
1413
1520
  });
1414
1521
  }
1522
+ cancelActiveMotion() {
1523
+ this.activeMotion?.cancel();
1524
+ this.activeMotion = null;
1525
+ }
1526
+ syncOffsetFromRenderedPosition() {
1527
+ const content = this.parts.content;
1528
+ if (!content || this.options.contentPlacement === "center") return;
1529
+ const offsetPx = this.readOffsetPxFromTransform(content);
1530
+ if (offsetPx === null) return;
1531
+ this.currentOffsetPx = offsetPx;
1532
+ }
1533
+ readOffsetPxFromTransform(content) {
1534
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
1535
+ const computed = this.root.ownerDocument.defaultView?.getComputedStyle(content).transform;
1536
+ const inline = content.style.transform;
1537
+ const transform = computed && computed !== "none" ? computed : inline && inline !== "none" ? inline : "";
1538
+ if (!transform) return this.currentOffsetPx;
1539
+ const translateMatch = transform.match(/translate3d\(\s*([^,]+)\s*,\s*([^,]+)\s*,/);
1540
+ if (translateMatch) {
1541
+ const raw = axis === "y" ? translateMatch[2] : translateMatch[1];
1542
+ const emMatch = raw.trim().match(/^(-?[\d.]+)em$/);
1543
+ if (emMatch) return Number.parseFloat(emMatch[1]) * getEmPx(content);
1544
+ const pxMatch = raw.trim().match(/^(-?[\d.]+)px$/);
1545
+ if (pxMatch) return Number.parseFloat(pxMatch[1]);
1546
+ }
1547
+ const matrixMatch = transform.match(/^matrix\(([^)]+)\)$/);
1548
+ if (matrixMatch) {
1549
+ const values = matrixMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1550
+ const translation = axis === "y" ? values[5] : values[4];
1551
+ if (Number.isFinite(translation)) return translation;
1552
+ }
1553
+ const matrix3dMatch = transform.match(/^matrix3d\(([^)]+)\)$/);
1554
+ if (matrix3dMatch) {
1555
+ const values = matrix3dMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1556
+ const translation = axis === "y" ? values[13] : values[12];
1557
+ if (Number.isFinite(translation)) return translation;
1558
+ }
1559
+ return null;
1560
+ }
1561
+ releaseVelocity(travel) {
1562
+ const windowMs = 100;
1563
+ const now = performance.now();
1564
+ const samples = travel.samples.filter((sample) => now - sample.t <= windowMs);
1565
+ if (samples.length < 2) return 0;
1566
+ const first = samples[0];
1567
+ const last = samples[samples.length - 1];
1568
+ const elapsed = Math.max(last.t - first.t, 1);
1569
+ return (last.pos - first.pos) / elapsed * 1e3;
1570
+ }
1571
+ getAllowedOutsideElements() {
1572
+ const allowed = /* @__PURE__ */ new Set();
1573
+ if (this.parts.view) allowed.add(this.parts.view);
1574
+ for (const element of this.parts.islands) allowed.add(element);
1575
+ for (const element of this.parts.externalOverlays) allowed.add(element);
1576
+ return allowed;
1577
+ }
1578
+ applyInertForElement(element, allowed) {
1579
+ const isAllowed = Array.from(allowed).some((candidate) => element === candidate || element.contains(candidate));
1580
+ if (isAllowed) {
1581
+ element.inert = false;
1582
+ element.removeAttribute("aria-hidden");
1583
+ return;
1584
+ }
1585
+ element.inert = true;
1586
+ element.setAttribute("aria-hidden", "true");
1587
+ }
1415
1588
  applyInert() {
1416
1589
  if (this.options.inertOutside === false || !this.parts.view) return;
1417
- const body = this.root.ownerDocument.body;
1418
- const allowed = /* @__PURE__ */ new Set([this.parts.view, ...this.parts.islands, ...this.parts.externalOverlays]);
1419
- this.clearInert();
1590
+ const doc = this.root.ownerDocument;
1591
+ const body = doc.body;
1592
+ const allowed = this.getAllowedOutsideElements();
1593
+ this.releaseInertHold();
1594
+ const holders = inertHolders.get(doc) || /* @__PURE__ */ new Set();
1595
+ holders.add(this);
1596
+ inertHolders.set(doc, holders);
1420
1597
  for (const child of Array.from(body.children)) {
1421
1598
  if (!(child instanceof HTMLElement)) continue;
1422
- this.inertedElements.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1423
- const isAllowed = Array.from(allowed).some((element) => child === element || child.contains(element));
1424
- if (isAllowed) {
1425
- child.inert = false;
1426
- child.removeAttribute("aria-hidden");
1427
- continue;
1599
+ const depth = inertDepth.get(child) ?? 0;
1600
+ if (depth === 0) {
1601
+ inertOriginal.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1428
1602
  }
1429
- child.inert = true;
1430
- child.setAttribute("aria-hidden", "true");
1603
+ inertDepth.set(child, depth + 1);
1604
+ this.inertHeldElements.add(child);
1605
+ this.applyInertForElement(child, allowed);
1431
1606
  }
1432
1607
  }
1433
1608
  clearInert() {
1434
- for (const [element, state] of this.inertedElements) {
1435
- element.inert = state.inert;
1436
- if (state.ariaHidden === null) {
1437
- element.removeAttribute("aria-hidden");
1609
+ this.releaseInertHold();
1610
+ }
1611
+ releaseInertHold() {
1612
+ if (this.inertHeldElements.size === 0) return;
1613
+ const doc = this.root.ownerDocument;
1614
+ const holders = inertHolders.get(doc);
1615
+ holders?.delete(this);
1616
+ if (holders?.size === 0) inertHolders.delete(doc);
1617
+ for (const element of this.inertHeldElements) {
1618
+ const depth = Math.max((inertDepth.get(element) ?? 1) - 1, 0);
1619
+ if (depth === 0) {
1620
+ inertDepth.delete(element);
1621
+ const snapshot = inertOriginal.get(element);
1622
+ if (snapshot) {
1623
+ element.inert = snapshot.inert;
1624
+ if (snapshot.ariaHidden === null) {
1625
+ element.removeAttribute("aria-hidden");
1626
+ } else {
1627
+ element.setAttribute("aria-hidden", snapshot.ariaHidden);
1628
+ }
1629
+ inertOriginal.delete(element);
1630
+ }
1438
1631
  } else {
1439
- element.setAttribute("aria-hidden", state.ariaHidden);
1632
+ inertDepth.set(element, depth);
1440
1633
  }
1441
1634
  }
1442
- this.inertedElements.clear();
1635
+ this.inertHeldElements.clear();
1636
+ this.reapplyDocumentInert(doc);
1637
+ }
1638
+ reapplyDocumentInert(doc) {
1639
+ const holders = inertHolders.get(doc);
1640
+ if (!holders?.size) return;
1641
+ const top = Array.from(holders).filter((controller) => controller.presented && controller.options.inertOutside !== false).sort((left, right) => right.getStackZIndex() - left.getStackZIndex())[0];
1642
+ if (!top) return;
1643
+ const allowed = top.getAllowedOutsideElements();
1644
+ const body = doc.body;
1645
+ for (const child of Array.from(body.children)) {
1646
+ if (!(child instanceof HTMLElement)) continue;
1647
+ if ((inertDepth.get(child) ?? 0) === 0) continue;
1648
+ top.applyInertForElement(child, allowed);
1649
+ }
1443
1650
  }
1444
1651
  retainThemeDimmer() {
1445
1652
  if (this.hasThemeDimmer || this.options.themeColorDimming !== "auto") return;