@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.
@@ -249,6 +249,9 @@ function releaseThemeColorDimmer(documentRef) {
249
249
  }
250
250
 
251
251
  // src/core/sheet-controller.ts
252
+ var inertDepth = /* @__PURE__ */ new WeakMap();
253
+ var inertOriginal = /* @__PURE__ */ new WeakMap();
254
+ var inertHolders = /* @__PURE__ */ new WeakMap();
252
255
  var defaultOptions = {
253
256
  contentPlacement: "bottom",
254
257
  sheetRole: "dialog",
@@ -433,24 +436,32 @@ var SheetController = class {
433
436
  pointerTravel = null;
434
437
  connected = false;
435
438
  lockedScroll = false;
436
- inertedElements = /* @__PURE__ */ new Map();
439
+ inertHeldElements = /* @__PURE__ */ new Set();
440
+ remeasureFrame = null;
441
+ activeMotion = null;
442
+ pendingSettle = null;
443
+ /** Entering completion deferred by a drag that interrupted present(); runs once the sheet settles. */
444
+ deferredEnteringComplete = null;
445
+ /** Resolves the in-flight present() after entering lifecycle completes or is abandoned. */
446
+ settlePresentPromise = null;
437
447
  previousFocus = null;
438
448
  hasThemeDimmer = false;
439
449
  stackId = null;
440
450
  documentListenersAttached = false;
441
451
  viewListenersElement = null;
442
452
  visualViewportListenersAttached = false;
443
- handleVisualViewportResize = () => {
453
+ handleVisualViewportResize = (event) => {
444
454
  const view = this.parts.view;
445
455
  const visualViewport = this.root.ownerDocument.defaultView?.visualViewport;
446
456
  if (!view) return;
457
+ const viewportChanged = event !== void 0;
447
458
  if (!visualViewport || this.options.nativeFocusScrollPrevention === false) {
448
- this.setKeyboardOffset("0em");
459
+ this.setKeyboardOffset("0em", viewportChanged);
449
460
  return;
450
461
  }
451
462
  const layoutHeight = this.root.ownerDocument.documentElement.clientHeight;
452
463
  const keyboardOffset = Math.max(0, layoutHeight - visualViewport.height - visualViewport.offsetTop);
453
- this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`);
464
+ this.setKeyboardOffset(`${toEm(view, keyboardOffset)}em`, viewportChanged);
454
465
  };
455
466
  handleFocusIn = (event) => {
456
467
  if (!this.presented || this.options.nativeFocusScrollPrevention === false) return;
@@ -481,10 +492,18 @@ var SheetController = class {
481
492
  if (!this.presented || this.options.swipe === false || this.options.contentPlacement === "center") return;
482
493
  if (!(event.target instanceof Element)) return;
483
494
  if (!this.parts.content?.contains(event.target) && event.target !== this.parts.backdrop) return;
495
+ if (this.pointerTravel && !event.isPrimary) return;
484
496
  if (this.options.nativeEdgeSwipePrevention !== false && this.isNativeEdgePointer(event)) {
485
497
  event.preventDefault();
486
498
  }
499
+ this.syncOffsetFromRenderedPosition();
500
+ const interruptedMotion = this.activeMotion ? this.pendingSettle : null;
501
+ this.pendingSettle = null;
502
+ this.cancelActiveMotion();
503
+ this.applyOffset(this.currentOffsetPx, this.status);
487
504
  this.remeasure();
505
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
506
+ const startPos = axis === "y" ? event.clientY : event.clientX;
488
507
  this.pointerTravel = {
489
508
  id: event.pointerId,
490
509
  startOffsetPx: this.currentOffsetPx,
@@ -494,7 +513,9 @@ var SheetController = class {
494
513
  lastY: event.clientY,
495
514
  lastTime: performance.now(),
496
515
  velocity: 0,
497
- dragging: false
516
+ dragging: false,
517
+ samples: [{ t: performance.now(), pos: startPos }],
518
+ interruptedMotion
498
519
  };
499
520
  };
500
521
  handlePointerMove = (event) => {
@@ -522,6 +543,8 @@ var SheetController = class {
522
543
  travel.lastX = event.clientX;
523
544
  travel.lastY = event.clientY;
524
545
  travel.lastTime = now;
546
+ travel.samples.push({ t: now, pos: current });
547
+ if (travel.samples.length > 24) travel.samples.shift();
525
548
  const nextOffset = this.limitOffset(travel.startOffsetPx + delta);
526
549
  this.applyOffset(nextOffset, "dragging");
527
550
  };
@@ -533,9 +556,17 @@ var SheetController = class {
533
556
  currentTarget.releasePointerCapture(event.pointerId);
534
557
  }
535
558
  this.pointerTravel = null;
536
- if (!travel.dragging) return;
537
- const nearest = this.nearestDetentForOffset(this.currentOffsetPx, travel.velocity);
559
+ if (!travel.dragging) {
560
+ if (travel.interruptedMotion && this.presented) {
561
+ void this.runSettleMotion(travel.interruptedMotion);
562
+ }
563
+ return;
564
+ }
565
+ const nearest = this.nearestDetentForOffset(this.currentOffsetPx, this.releaseVelocity(travel));
538
566
  dispatch(this.root, "cap-sheet-drag-end", this.getTravelEvent());
567
+ if (travel.interruptedMotion?.status === "entering") {
568
+ this.deferredEnteringComplete = travel.interruptedMotion.complete;
569
+ }
539
570
  if (nearest === 0 && this.options.swipeDismissal !== false) {
540
571
  void this.dismiss({ source: "gesture" });
541
572
  return;
@@ -620,6 +651,8 @@ var SheetController = class {
620
651
  this.remeasure();
621
652
  this.applyOffset(this.detentOffsetsPx[0] || this.hiddenOffsetPx, this.status);
622
653
  this.updateDomState(false);
654
+ } else if (this.presented && affectsDismissedOffset(options)) {
655
+ this.scheduleRemeasure();
623
656
  }
624
657
  this.registerStack();
625
658
  }
@@ -674,6 +707,7 @@ var SheetController = class {
674
707
  }
675
708
  this.previousFocus = this.root.ownerDocument.activeElement;
676
709
  this.presented = true;
710
+ this.abandonDeferredEntering();
677
711
  this.activeDetent = clamp(options.detent ?? this.resolveInitialDetent(), 1, this.detentOffsetsPx.length - 1);
678
712
  this.updateDetentState();
679
713
  this.updateDomState(true);
@@ -682,15 +716,33 @@ var SheetController = class {
682
716
  this.retainThemeDimmer();
683
717
  this.setStatus("entering");
684
718
  dispatch(this.root, "cap-sheet-present", this.getTravelEvent());
685
- await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, {
686
- ...this.options.enteringAnimationSettings || {},
687
- ...options.animation || {}
719
+ const source = options.source || "programmatic";
720
+ let resolvePresent;
721
+ const presentLifecycleSettled = new Promise((resolve) => {
722
+ resolvePresent = resolve;
688
723
  });
689
- this.setStatus("idle");
690
- this.focusInitialElement();
691
- this.emitPresentedChange(true, options.source || "programmatic");
692
- this.emitActiveDetentChange(this.activeDetent, 0);
693
- this.updateStack();
724
+ this.settlePresentPromise = () => {
725
+ resolvePresent();
726
+ };
727
+ const complete = () => {
728
+ this.setStatus("idle");
729
+ this.focusInitialElement();
730
+ this.emitPresentedChange(true, source);
731
+ this.emitActiveDetentChange(this.activeDetent, 0);
732
+ this.updateStack();
733
+ this.releasePresentAwait();
734
+ };
735
+ const finished = await this.runSettleMotion({
736
+ status: "entering",
737
+ animation: {
738
+ ...this.options.enteringAnimationSettings || {},
739
+ ...options.animation || {}
740
+ },
741
+ complete
742
+ });
743
+ if (!finished) {
744
+ await presentLifecycleSettled;
745
+ }
694
746
  }
695
747
  /** Dismiss the sheet. */
696
748
  async dismiss(options = {}) {
@@ -699,6 +751,8 @@ var SheetController = class {
699
751
  const previous = this.activeDetent;
700
752
  this.presented = false;
701
753
  this.activeDetent = 0;
754
+ this.pendingSettle = null;
755
+ this.abandonDeferredEntering();
702
756
  this.updateDetentState();
703
757
  this.setStatus("exiting");
704
758
  dispatch(this.root, "cap-sheet-dismiss", this.getTravelEvent());
@@ -739,15 +793,49 @@ var SheetController = class {
739
793
  const previous = this.activeDetent;
740
794
  this.activeDetent = target;
741
795
  this.updateDetentState();
742
- this.setStatus("settling");
743
- await this.animateTo(this.detentOffsetsPx[target] || 0, {
744
- ...this.options.steppingAnimationSettings || {},
745
- ...options.animation || {}
796
+ await this.runSettleMotion({
797
+ status: "settling",
798
+ animation: {
799
+ ...this.options.steppingAnimationSettings || {},
800
+ ...options.animation || {}
801
+ },
802
+ complete: () => {
803
+ const enteringComplete = this.deferredEnteringComplete;
804
+ if (enteringComplete) {
805
+ this.deferredEnteringComplete = null;
806
+ enteringComplete();
807
+ return;
808
+ }
809
+ this.setStatus("idle");
810
+ if (previous !== target) {
811
+ this.emitActiveDetentChange(target, previous);
812
+ }
813
+ }
746
814
  });
747
- this.setStatus("idle");
748
- if (previous !== target) {
749
- this.emitActiveDetentChange(target, previous);
750
- }
815
+ }
816
+ /**
817
+ * Animate to the active detent and run the completion once the motion finishes.
818
+ * When the motion is cancelled by a pointerdown, the settle stays pending so a tap
819
+ * that never becomes a drag can resume it.
820
+ */
821
+ releasePresentAwait() {
822
+ if (!this.settlePresentPromise) return;
823
+ const settle = this.settlePresentPromise;
824
+ this.settlePresentPromise = null;
825
+ settle();
826
+ }
827
+ abandonDeferredEntering() {
828
+ this.deferredEnteringComplete = null;
829
+ this.releasePresentAwait();
830
+ }
831
+ async runSettleMotion(motion) {
832
+ this.setStatus(motion.status);
833
+ this.pendingSettle = motion;
834
+ const finished = await this.animateTo(this.detentOffsetsPx[this.activeDetent] || 0, motion.animation);
835
+ if (!finished) return false;
836
+ if (this.pendingSettle === motion) this.pendingSettle = null;
837
+ motion.complete();
838
+ return true;
751
839
  }
752
840
  /** Step up or down one detent. */
753
841
  async step(direction = "up", options = {}) {
@@ -879,7 +967,7 @@ var SheetController = class {
879
967
  const resolved = resolveAnimation(settings);
880
968
  if (!content || shouldSkipMotion(resolved.skip)) {
881
969
  this.applyOffset(targetOffsetPx, "idle");
882
- return;
970
+ return true;
883
971
  }
884
972
  const startOffset = this.currentOffsetPx;
885
973
  const fromProgress = this.getProgressForOffset(startOffset);
@@ -904,7 +992,18 @@ var SheetController = class {
904
992
  );
905
993
  const started = performance.now();
906
994
  let frame;
995
+ let cancelled = false;
996
+ const cancelMotion = () => {
997
+ if (cancelled) return;
998
+ cancelled = true;
999
+ cancelAnimationFrame(frame);
1000
+ contentAnimation?.cancel();
1001
+ backdropAnimation?.cancel();
1002
+ this.activeMotion = null;
1003
+ };
1004
+ this.activeMotion = { cancel: cancelMotion };
907
1005
  const tick = () => {
1006
+ if (cancelled) return;
908
1007
  const progress = duration === 0 ? 1 : clamp((performance.now() - started) / duration, 0, 1);
909
1008
  const offset = startOffset + (targetOffsetPx - startOffset) * progress;
910
1009
  this.currentOffsetPx = offset;
@@ -918,10 +1017,13 @@ var SheetController = class {
918
1017
  contentAnimation?.finished,
919
1018
  backdropAnimation ? backdropAnimation.finished : Promise.resolve()
920
1019
  ]);
1020
+ if (cancelled) return false;
921
1021
  cancelAnimationFrame(frame);
922
1022
  contentAnimation?.cancel();
923
1023
  backdropAnimation?.cancel();
1024
+ this.activeMotion = null;
924
1025
  this.applyOffset(targetOffsetPx, "idle");
1026
+ return true;
925
1027
  }
926
1028
  getProgressForOffset(offsetPx) {
927
1029
  if (this.options.contentPlacement === "center") {
@@ -1145,47 +1247,152 @@ var SheetController = class {
1145
1247
  documentLockSnapshot.delete(doc);
1146
1248
  this.lockedScroll = false;
1147
1249
  }
1148
- setKeyboardOffset(value) {
1250
+ setKeyboardOffset(value, viewportChanged = false) {
1149
1251
  const view = this.parts.view;
1150
1252
  if (!view) return;
1151
- if (view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value) {
1152
- view.style.setProperty("--cap-sheet-keyboard-offset", value);
1153
- }
1253
+ const changed = view.style.getPropertyValue("--cap-sheet-keyboard-offset") !== value;
1254
+ if (changed) view.style.setProperty("--cap-sheet-keyboard-offset", value);
1255
+ if (changed || viewportChanged) this.scheduleRemeasure();
1256
+ }
1257
+ scheduleRemeasure() {
1154
1258
  if (!this.presented) return;
1155
- requestAnimationFrame(() => {
1259
+ if (this.remeasureFrame !== null) return;
1260
+ this.remeasureFrame = requestAnimationFrame(() => {
1261
+ this.remeasureFrame = null;
1156
1262
  if (!this.presented) return;
1157
1263
  this.remeasure();
1264
+ if (this.status === "dragging") return;
1158
1265
  this.applyOffset(this.detentOffsetsPx[this.activeDetent] || 0, this.status);
1159
1266
  });
1160
1267
  }
1268
+ cancelActiveMotion() {
1269
+ this.activeMotion?.cancel();
1270
+ this.activeMotion = null;
1271
+ }
1272
+ syncOffsetFromRenderedPosition() {
1273
+ const content = this.parts.content;
1274
+ if (!content || this.options.contentPlacement === "center") return;
1275
+ const offsetPx = this.readOffsetPxFromTransform(content);
1276
+ if (offsetPx === null) return;
1277
+ this.currentOffsetPx = offsetPx;
1278
+ }
1279
+ readOffsetPxFromTransform(content) {
1280
+ const axis = isVerticalTrack(this.activeTrack) ? "y" : "x";
1281
+ const computed = this.root.ownerDocument.defaultView?.getComputedStyle(content).transform;
1282
+ const inline = content.style.transform;
1283
+ const transform = computed && computed !== "none" ? computed : inline && inline !== "none" ? inline : "";
1284
+ if (!transform) return this.currentOffsetPx;
1285
+ const translateMatch = transform.match(/translate3d\(\s*([^,]+)\s*,\s*([^,]+)\s*,/);
1286
+ if (translateMatch) {
1287
+ const raw = axis === "y" ? translateMatch[2] : translateMatch[1];
1288
+ const emMatch = raw.trim().match(/^(-?[\d.]+)em$/);
1289
+ if (emMatch) return Number.parseFloat(emMatch[1]) * getEmPx(content);
1290
+ const pxMatch = raw.trim().match(/^(-?[\d.]+)px$/);
1291
+ if (pxMatch) return Number.parseFloat(pxMatch[1]);
1292
+ }
1293
+ const matrixMatch = transform.match(/^matrix\(([^)]+)\)$/);
1294
+ if (matrixMatch) {
1295
+ const values = matrixMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1296
+ const translation = axis === "y" ? values[5] : values[4];
1297
+ if (Number.isFinite(translation)) return translation;
1298
+ }
1299
+ const matrix3dMatch = transform.match(/^matrix3d\(([^)]+)\)$/);
1300
+ if (matrix3dMatch) {
1301
+ const values = matrix3dMatch[1].split(",").map((part) => Number.parseFloat(part.trim()));
1302
+ const translation = axis === "y" ? values[13] : values[12];
1303
+ if (Number.isFinite(translation)) return translation;
1304
+ }
1305
+ return null;
1306
+ }
1307
+ releaseVelocity(travel) {
1308
+ const windowMs = 100;
1309
+ const now = performance.now();
1310
+ const samples = travel.samples.filter((sample) => now - sample.t <= windowMs);
1311
+ if (samples.length < 2) return 0;
1312
+ const first = samples[0];
1313
+ const last = samples[samples.length - 1];
1314
+ const elapsed = Math.max(last.t - first.t, 1);
1315
+ return (last.pos - first.pos) / elapsed * 1e3;
1316
+ }
1317
+ getAllowedOutsideElements() {
1318
+ const allowed = /* @__PURE__ */ new Set();
1319
+ if (this.parts.view) allowed.add(this.parts.view);
1320
+ for (const element of this.parts.islands) allowed.add(element);
1321
+ for (const element of this.parts.externalOverlays) allowed.add(element);
1322
+ return allowed;
1323
+ }
1324
+ applyInertForElement(element, allowed) {
1325
+ const isAllowed = Array.from(allowed).some((candidate) => element === candidate || element.contains(candidate));
1326
+ if (isAllowed) {
1327
+ element.inert = false;
1328
+ element.removeAttribute("aria-hidden");
1329
+ return;
1330
+ }
1331
+ element.inert = true;
1332
+ element.setAttribute("aria-hidden", "true");
1333
+ }
1161
1334
  applyInert() {
1162
1335
  if (this.options.inertOutside === false || !this.parts.view) return;
1163
- const body = this.root.ownerDocument.body;
1164
- const allowed = /* @__PURE__ */ new Set([this.parts.view, ...this.parts.islands, ...this.parts.externalOverlays]);
1165
- this.clearInert();
1336
+ const doc = this.root.ownerDocument;
1337
+ const body = doc.body;
1338
+ const allowed = this.getAllowedOutsideElements();
1339
+ this.releaseInertHold();
1340
+ const holders = inertHolders.get(doc) || /* @__PURE__ */ new Set();
1341
+ holders.add(this);
1342
+ inertHolders.set(doc, holders);
1166
1343
  for (const child of Array.from(body.children)) {
1167
1344
  if (!(child instanceof HTMLElement)) continue;
1168
- this.inertedElements.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1169
- const isAllowed = Array.from(allowed).some((element) => child === element || child.contains(element));
1170
- if (isAllowed) {
1171
- child.inert = false;
1172
- child.removeAttribute("aria-hidden");
1173
- continue;
1345
+ const depth = inertDepth.get(child) ?? 0;
1346
+ if (depth === 0) {
1347
+ inertOriginal.set(child, { inert: child.inert, ariaHidden: child.getAttribute("aria-hidden") });
1174
1348
  }
1175
- child.inert = true;
1176
- child.setAttribute("aria-hidden", "true");
1349
+ inertDepth.set(child, depth + 1);
1350
+ this.inertHeldElements.add(child);
1351
+ this.applyInertForElement(child, allowed);
1177
1352
  }
1178
1353
  }
1179
1354
  clearInert() {
1180
- for (const [element, state] of this.inertedElements) {
1181
- element.inert = state.inert;
1182
- if (state.ariaHidden === null) {
1183
- element.removeAttribute("aria-hidden");
1355
+ this.releaseInertHold();
1356
+ }
1357
+ releaseInertHold() {
1358
+ if (this.inertHeldElements.size === 0) return;
1359
+ const doc = this.root.ownerDocument;
1360
+ const holders = inertHolders.get(doc);
1361
+ holders?.delete(this);
1362
+ if (holders?.size === 0) inertHolders.delete(doc);
1363
+ for (const element of this.inertHeldElements) {
1364
+ const depth = Math.max((inertDepth.get(element) ?? 1) - 1, 0);
1365
+ if (depth === 0) {
1366
+ inertDepth.delete(element);
1367
+ const snapshot = inertOriginal.get(element);
1368
+ if (snapshot) {
1369
+ element.inert = snapshot.inert;
1370
+ if (snapshot.ariaHidden === null) {
1371
+ element.removeAttribute("aria-hidden");
1372
+ } else {
1373
+ element.setAttribute("aria-hidden", snapshot.ariaHidden);
1374
+ }
1375
+ inertOriginal.delete(element);
1376
+ }
1184
1377
  } else {
1185
- element.setAttribute("aria-hidden", state.ariaHidden);
1378
+ inertDepth.set(element, depth);
1186
1379
  }
1187
1380
  }
1188
- this.inertedElements.clear();
1381
+ this.inertHeldElements.clear();
1382
+ this.reapplyDocumentInert(doc);
1383
+ }
1384
+ reapplyDocumentInert(doc) {
1385
+ const holders = inertHolders.get(doc);
1386
+ if (!holders?.size) return;
1387
+ const top = Array.from(holders).filter((controller) => controller.presented && controller.options.inertOutside !== false).sort((left, right) => right.getStackZIndex() - left.getStackZIndex())[0];
1388
+ if (!top) return;
1389
+ const allowed = top.getAllowedOutsideElements();
1390
+ const body = doc.body;
1391
+ for (const child of Array.from(body.children)) {
1392
+ if (!(child instanceof HTMLElement)) continue;
1393
+ if ((inertDepth.get(child) ?? 0) === 0) continue;
1394
+ top.applyInertForElement(child, allowed);
1395
+ }
1189
1396
  }
1190
1397
  retainThemeDimmer() {
1191
1398
  if (this.hasThemeDimmer || this.options.themeColorDimming !== "auto") return;
@@ -1859,4 +2066,4 @@ export {
1859
2066
  CapAutoFocusTarget,
1860
2067
  CapSheetPortal
1861
2068
  };
1862
- //# sourceMappingURL=chunk-75SRJO4V.mjs.map
2069
+ //# sourceMappingURL=chunk-VT3FZ2IC.mjs.map