@rogieking/figui3 11.6.0 → 11.6.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.
package/fig.js CHANGED
@@ -2907,7 +2907,11 @@ class FigPopup extends HTMLDialogElement {
2907
2907
  _wasDragged = false;
2908
2908
  _previousFocus = null;
2909
2909
  _boundDocumentKeydown;
2910
+ _boundDocumentPointerUp;
2910
2911
  _boundNativeClose;
2912
+ _boundNativeCancel;
2913
+ _suppressNativeCancel = false;
2914
+ _nativeCancelResetTimer = null;
2911
2915
 
2912
2916
  constructor() {
2913
2917
  super();
@@ -2929,7 +2933,10 @@ class FigPopup extends HTMLDialogElement {
2929
2933
  this._boundPointerMove = this.handlePointerMove.bind(this);
2930
2934
  this._boundPointerUp = this.handlePointerUp.bind(this);
2931
2935
  this._boundDocumentKeydown = this.handleDocumentKeydown.bind(this);
2936
+ this._boundDocumentPointerUp =
2937
+ this.handleDocumentPointerUp.bind(this);
2932
2938
  this._boundNativeClose = this.handleNativeClose.bind(this);
2939
+ this._boundNativeCancel = this.handleNativeCancel.bind(this);
2933
2940
  this._boundAutoClose = () => {
2934
2941
  this.open = false;
2935
2942
  };
@@ -2960,6 +2967,10 @@ class FigPopup extends HTMLDialogElement {
2960
2967
  if (typeof this._dragThreshold !== "number") this._dragThreshold = 3;
2961
2968
  if (typeof this._wasDragged === "undefined") this._wasDragged = false;
2962
2969
  if (typeof this._previousFocus === "undefined") this._previousFocus = null;
2970
+ if (typeof this._suppressNativeCancel === "undefined")
2971
+ this._suppressNativeCancel = false;
2972
+ if (typeof this._nativeCancelResetTimer === "undefined")
2973
+ this._nativeCancelResetTimer = null;
2963
2974
 
2964
2975
  if (typeof this._boundReposition !== "function") {
2965
2976
  this._boundReposition = () => this.queueReposition();
@@ -2995,9 +3006,16 @@ class FigPopup extends HTMLDialogElement {
2995
3006
  if (typeof this._boundDocumentKeydown !== "function") {
2996
3007
  this._boundDocumentKeydown = this.handleDocumentKeydown.bind(this);
2997
3008
  }
3009
+ if (typeof this._boundDocumentPointerUp !== "function") {
3010
+ this._boundDocumentPointerUp =
3011
+ this.handleDocumentPointerUp.bind(this);
3012
+ }
2998
3013
  if (typeof this._boundNativeClose !== "function") {
2999
3014
  this._boundNativeClose = this.handleNativeClose.bind(this);
3000
3015
  }
3016
+ if (typeof this._boundNativeCancel !== "function") {
3017
+ this._boundNativeCancel = this.handleNativeCancel.bind(this);
3018
+ }
3001
3019
  if (typeof this._boundAutoClose !== "function") {
3002
3020
  this._boundAutoClose = () => {
3003
3021
  this.open = false;
@@ -3092,6 +3110,8 @@ class FigPopup extends HTMLDialogElement {
3092
3110
 
3093
3111
  this.removeEventListener("close", this._boundNativeClose);
3094
3112
  this.addEventListener("close", this._boundNativeClose);
3113
+ this.removeEventListener("cancel", this._boundNativeCancel);
3114
+ this.addEventListener("cancel", this._boundNativeCancel);
3095
3115
 
3096
3116
  requestAnimationFrame(() => {
3097
3117
  this.setupDragListeners();
@@ -3114,7 +3134,22 @@ class FigPopup extends HTMLDialogElement {
3114
3134
  true,
3115
3135
  );
3116
3136
  document.removeEventListener("keydown", this._boundDocumentKeydown, true);
3137
+ document.removeEventListener(
3138
+ "pointerup",
3139
+ this._boundDocumentPointerUp,
3140
+ true,
3141
+ );
3142
+ document.removeEventListener(
3143
+ "pointercancel",
3144
+ this._boundDocumentPointerUp,
3145
+ true,
3146
+ );
3117
3147
  this.removeEventListener("close", this._boundNativeClose);
3148
+ this.removeEventListener("cancel", this._boundNativeCancel);
3149
+ if (this._nativeCancelResetTimer !== null) {
3150
+ clearTimeout(this._nativeCancelResetTimer);
3151
+ this._nativeCancelResetTimer = null;
3152
+ }
3118
3153
  if (this._rafId !== null) {
3119
3154
  cancelAnimationFrame(this._rafId);
3120
3155
  this._rafId = null;
@@ -3210,6 +3245,16 @@ class FigPopup extends HTMLDialogElement {
3210
3245
  true,
3211
3246
  );
3212
3247
  document.addEventListener("keydown", this._boundDocumentKeydown, true);
3248
+ document.addEventListener(
3249
+ "pointerup",
3250
+ this._boundDocumentPointerUp,
3251
+ true,
3252
+ );
3253
+ document.addEventListener(
3254
+ "pointercancel",
3255
+ this._boundDocumentPointerUp,
3256
+ true,
3257
+ );
3213
3258
  this._wasDragged = false;
3214
3259
  this.queueReposition();
3215
3260
  this._isPopupActive = true;
@@ -3241,6 +3286,21 @@ class FigPopup extends HTMLDialogElement {
3241
3286
  true,
3242
3287
  );
3243
3288
  document.removeEventListener("keydown", this._boundDocumentKeydown, true);
3289
+ document.removeEventListener(
3290
+ "pointerup",
3291
+ this._boundDocumentPointerUp,
3292
+ true,
3293
+ );
3294
+ document.removeEventListener(
3295
+ "pointercancel",
3296
+ this._boundDocumentPointerUp,
3297
+ true,
3298
+ );
3299
+ this._suppressNativeCancel = false;
3300
+ if (this._nativeCancelResetTimer !== null) {
3301
+ clearTimeout(this._nativeCancelResetTimer);
3302
+ this._nativeCancelResetTimer = null;
3303
+ }
3244
3304
 
3245
3305
  if (
3246
3306
  this.hasAttribute("popover") &&
@@ -3317,6 +3377,15 @@ class FigPopup extends HTMLDialogElement {
3317
3377
  if (this.hasAttribute("open")) this.removeAttribute("open");
3318
3378
  }
3319
3379
 
3380
+ handleNativeCancel(event) {
3381
+ if (
3382
+ this._suppressNativeCancel &&
3383
+ this.getAttribute("closedby") === "any"
3384
+ ) {
3385
+ event.preventDefault();
3386
+ }
3387
+ }
3388
+
3320
3389
  setupObservers() {
3321
3390
  this.teardownObservers();
3322
3391
 
@@ -3462,26 +3531,55 @@ class FigPopup extends HTMLDialogElement {
3462
3531
  // counts as inside — Node.contains() is false for slotted nodes.
3463
3532
  const path =
3464
3533
  typeof event.composedPath === "function" ? event.composedPath() : [];
3465
- if (path.includes(this)) return;
3466
- if (this.contains(target)) return;
3534
+ if (path.includes(this) || this.contains(target)) {
3535
+ this.markOwnedPointerInteraction();
3536
+ return;
3537
+ }
3467
3538
 
3468
3539
  const anchor = this.resolveAnchor();
3469
- if (anchor && (path.includes(anchor) || anchor.contains?.(target))) return;
3540
+ if (anchor && (path.includes(anchor) || anchor.contains?.(target))) {
3541
+ this.markOwnedPointerInteraction();
3542
+ return;
3543
+ }
3470
3544
 
3471
- if (this.isInsideDescendantPopup(target)) return;
3545
+ if (this.isInsideDescendantPopup(target, path)) {
3546
+ this.markOwnedPointerInteraction();
3547
+ return;
3548
+ }
3472
3549
 
3550
+ this._suppressNativeCancel = false;
3473
3551
  this.open = false;
3474
3552
  }
3475
3553
 
3554
+ markOwnedPointerInteraction() {
3555
+ this._suppressNativeCancel = true;
3556
+ if (this._nativeCancelResetTimer !== null) {
3557
+ clearTimeout(this._nativeCancelResetTimer);
3558
+ this._nativeCancelResetTimer = null;
3559
+ }
3560
+ }
3561
+
3562
+ handleDocumentPointerUp() {
3563
+ if (!this._suppressNativeCancel) return;
3564
+ if (this._nativeCancelResetTimer !== null) {
3565
+ clearTimeout(this._nativeCancelResetTimer);
3566
+ }
3567
+ // Native dialog light-dismiss runs after pointer dispatch. Keep the guard
3568
+ // through that algorithm, then allow later requestClose() calls normally.
3569
+ this._nativeCancelResetTimer = setTimeout(() => {
3570
+ this._suppressNativeCancel = false;
3571
+ this._nativeCancelResetTimer = null;
3572
+ }, 0);
3573
+ }
3574
+
3476
3575
  handleDocumentKeydown(event) {
3477
3576
  if (event.key !== "Escape" || event.defaultPrevented) return;
3478
3577
  if (!this.open) return;
3479
3578
  if (this.getAttribute("role") === "menu") return;
3480
3579
  const closedby = this.getAttribute("closedby");
3481
3580
  if (closedby === "none") return;
3482
- const openPopups = Array.from(
3483
- document.querySelectorAll('dialog[is="fig-popup"][open]'),
3484
- ).filter((popup) => popup.open);
3581
+ const openPopups = Array.from(document.querySelectorAll("dialog[open]"))
3582
+ .filter((popup) => popup instanceof FigPopup && popup.open);
3485
3583
  const topPopup = openPopups
3486
3584
  .map((popup) => ({
3487
3585
  popup,
@@ -3494,8 +3592,40 @@ class FigPopup extends HTMLDialogElement {
3494
3592
  this.open = false;
3495
3593
  }
3496
3594
 
3497
- isInsideDescendantPopup(target) {
3498
- const targetDialog = target.closest?.('dialog[is="fig-popup"]');
3595
+ getContainingPopup(node) {
3596
+ let current = node;
3597
+ const visited = new Set();
3598
+ while (current && !visited.has(current)) {
3599
+ visited.add(current);
3600
+ if (current instanceof FigPopup) return current;
3601
+ current =
3602
+ current.parentNode ??
3603
+ (current.getRootNode?.() instanceof ShadowRoot
3604
+ ? current.getRootNode().host
3605
+ : null);
3606
+ }
3607
+ return null;
3608
+ }
3609
+
3610
+ composedContains(node) {
3611
+ let current = node;
3612
+ const visited = new Set();
3613
+ while (current && !visited.has(current)) {
3614
+ if (current === this) return true;
3615
+ visited.add(current);
3616
+ current =
3617
+ current.parentNode ??
3618
+ (current.getRootNode?.() instanceof ShadowRoot
3619
+ ? current.getRootNode().host
3620
+ : null);
3621
+ }
3622
+ return false;
3623
+ }
3624
+
3625
+ isInsideDescendantPopup(target, path = []) {
3626
+ const targetDialog =
3627
+ path.find((node) => node instanceof FigPopup) ??
3628
+ this.getContainingPopup(target);
3499
3629
  if (!targetDialog || targetDialog === this) return false;
3500
3630
 
3501
3631
  let current = targetDialog;
@@ -3504,8 +3634,8 @@ class FigPopup extends HTMLDialogElement {
3504
3634
  visited.add(current);
3505
3635
  const popupAnchor = current.anchor;
3506
3636
  if (!(popupAnchor instanceof Element)) break;
3507
- if (this.contains(popupAnchor)) return true;
3508
- current = popupAnchor.closest?.('dialog[is="fig-popup"]');
3637
+ if (this.composedContains(popupAnchor)) return true;
3638
+ current = this.getContainingPopup(popupAnchor);
3509
3639
  }
3510
3640
  return false;
3511
3641
  }
@@ -12307,7 +12437,13 @@ class FigInputGradient extends HTMLElement {
12307
12437
 
12308
12438
  this.#track.addEventListener("pointerdown", (e) => {
12309
12439
  if (figBooleanAttribute(this, "disabled")) return;
12310
- if (e.target.closest("fig-handle:not(.fig-input-gradient-ghost)")) return;
12440
+ const handle = e.target.closest(
12441
+ "fig-handle:not(.fig-input-gradient-ghost)",
12442
+ );
12443
+ if (handle) {
12444
+ this.#activateStopHandle(handle);
12445
+ return;
12446
+ }
12311
12447
  if (e.button !== 0) return;
12312
12448
  e.preventDefault();
12313
12449
  e.stopPropagation();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "11.6.0",
3
+ "version": "11.6.1",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "SEE LICENSE IN LICENSE",