@rogieking/figui3 4.15.1 → 4.15.4

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
@@ -1784,6 +1784,10 @@ class FigPopup extends HTMLDialogElement {
1784
1784
  this.hasAttribute("popover") &&
1785
1785
  typeof this.showPopover === "function" &&
1786
1786
  !this.matches?.(":popover-open");
1787
+ const positionBeforeReveal = this.shouldAutoReposition();
1788
+ if (positionBeforeReveal) {
1789
+ this.style.visibility = "hidden";
1790
+ }
1787
1791
  if (usePopover) {
1788
1792
  try {
1789
1793
  this.showPopover();
@@ -1798,6 +1802,10 @@ class FigPopup extends HTMLDialogElement {
1798
1802
  // Ignore when dialog cannot be shown yet.
1799
1803
  }
1800
1804
  }
1805
+ if (positionBeforeReveal && (this.matches?.(":open") || this.matches?.(":popover-open"))) {
1806
+ this.positionPopup();
1807
+ this.style.visibility = "";
1808
+ }
1801
1809
 
1802
1810
  this.setupObservers();
1803
1811
  document.addEventListener(
@@ -1817,6 +1825,7 @@ class FigPopup extends HTMLDialogElement {
1817
1825
  const anchor = this.resolveAnchor();
1818
1826
  if (anchor?.classList) anchor.classList.remove("has-popup-open");
1819
1827
 
1828
+ this.style.visibility = "";
1820
1829
  this._isPopupActive = false;
1821
1830
  this._wasDragged = false;
1822
1831
  this.teardownObservers();
@@ -1837,7 +1846,10 @@ class FigPopup extends HTMLDialogElement {
1837
1846
  // Ignore.
1838
1847
  }
1839
1848
  }
1840
- if (super.open) {
1849
+ // Use :open, not super.open — the custom open getter reads the attribute
1850
+ // removed just before hidePopup(), so super.open can be false while the
1851
+ // native dialog is still open and no "close" event fires.
1852
+ if (this.matches?.(":open")) {
1841
1853
  try {
1842
1854
  this.close();
1843
1855
  } catch (e) {
@@ -2484,7 +2496,7 @@ class FigPopup extends HTMLDialogElement {
2484
2496
  }
2485
2497
 
2486
2498
  positionPopup() {
2487
- if (!this.open || !super.open) return;
2499
+ if (!this.open || !this.matches?.(":open")) return;
2488
2500
 
2489
2501
  const popupRect = this.getBoundingClientRect();
2490
2502
  const offset = this.parseOffset();
@@ -5123,6 +5135,14 @@ class FigInputColor extends HTMLElement {
5123
5135
  // Setup swatch (native picker)
5124
5136
  if (this.#swatch) {
5125
5137
  this.#swatch.disabled = this.hasAttribute("disabled");
5138
+ const swatchInput = this.#swatch.querySelector('input[type="color"]');
5139
+ if (this.#textInput || this.hasAttribute("swatch-disabled")) {
5140
+ swatchInput?.setAttribute("tabindex", "-1");
5141
+ }
5142
+ if (this.hasAttribute("swatch-disabled")) {
5143
+ swatchInput?.setAttribute("disabled", "");
5144
+ if (swatchInput) swatchInput.style.pointerEvents = "none";
5145
+ }
5126
5146
  this.#swatch.addEventListener("input", this.#handleInput.bind(this));
5127
5147
  }
5128
5148
 
@@ -5240,7 +5260,11 @@ class FigInputColor extends HTMLElement {
5240
5260
  }
5241
5261
 
5242
5262
  focus() {
5243
- this.#swatch.focus();
5263
+ if (this.#textInput) {
5264
+ this.#textInput.focus();
5265
+ return;
5266
+ }
5267
+ this.#swatch?.focus();
5244
5268
  }
5245
5269
 
5246
5270
  #handleInput(event) {
@@ -6618,6 +6642,14 @@ class FigInputPalette extends HTMLElement {
6618
6642
 
6619
6643
  const inlineWrap = document.createElement("div");
6620
6644
  inlineWrap.className = "palette-colors-inline";
6645
+ inlineWrap.addEventListener("click", () => {
6646
+ if (
6647
+ this.hasAttribute("disabled") &&
6648
+ this.getAttribute("disabled") !== "false"
6649
+ )
6650
+ return;
6651
+ this.open = true;
6652
+ });
6621
6653
 
6622
6654
  const wrap = document.createElement("div");
6623
6655
  wrap.className = "palette-colors";
@@ -6655,9 +6687,10 @@ class FigInputPalette extends HTMLElement {
6655
6687
  if (inline) {
6656
6688
  ic.setAttribute("text", "false");
6657
6689
  ic.setAttribute("alpha", "true");
6690
+ ic.setAttribute("swatch-disabled", "");
6658
6691
  } else {
6659
6692
  ic.setAttribute("text", "true");
6660
- ic.setAttribute("alpha", this.#isFixed ? "true" : "false");
6693
+ ic.setAttribute("alpha", "true");
6661
6694
  ic.setAttribute("full", "");
6662
6695
  }
6663
6696
  if (disabled) ic.setAttribute("disabled", "");
@@ -6712,7 +6745,10 @@ class FigInputPalette extends HTMLElement {
6712
6745
  if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
6713
6746
  this.#removeColor(index);
6714
6747
  });
6715
- return btn;
6748
+ const tooltip = document.createElement("fig-tooltip");
6749
+ tooltip.setAttribute("text", "Remove color");
6750
+ tooltip.appendChild(btn);
6751
+ return tooltip;
6716
6752
  }
6717
6753
 
6718
6754
  #removeColor(index) {
@@ -6742,6 +6778,7 @@ class FigInputPalette extends HTMLElement {
6742
6778
  )
6743
6779
  return;
6744
6780
  if (this.#colors.length >= this.#max) return;
6781
+ this.open = true;
6745
6782
  this.#addColor({ color: "#D9D9D9", alpha: 1 });
6746
6783
  });
6747
6784
  const tooltip = document.createElement("fig-tooltip");
@@ -6775,6 +6812,7 @@ class FigInputPalette extends HTMLElement {
6775
6812
  const addBtn = this.querySelector(".palette-add-btn");
6776
6813
  if (addBtn) addBtn.setAttribute("disabled", "");
6777
6814
  }
6815
+ this.#syncRemoveButtons(disabled);
6778
6816
  this.#emitChange();
6779
6817
  }
6780
6818
 
@@ -6814,9 +6852,18 @@ class FigInputPalette extends HTMLElement {
6814
6852
  });
6815
6853
  const addBtn = this.querySelector(".palette-add-btn");
6816
6854
  if (addBtn) {
6817
- if (disabled) addBtn.setAttribute("disabled", "");
6855
+ if (disabled || this.#colors.length >= this.#max) addBtn.setAttribute("disabled", "");
6818
6856
  else addBtn.removeAttribute("disabled");
6819
6857
  }
6858
+ this.#syncRemoveButtons(disabled);
6859
+ }
6860
+
6861
+ #syncRemoveButtons(disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") {
6862
+ const shouldDisable = disabled || this.#colors.length <= this.#min;
6863
+ this.querySelectorAll(".palette-remove-btn").forEach((btn) => {
6864
+ if (shouldDisable) btn.setAttribute("disabled", "");
6865
+ else btn.removeAttribute("disabled");
6866
+ });
6820
6867
  }
6821
6868
 
6822
6869
  #emitInput() {
@@ -7038,7 +7085,7 @@ class FigInputGradient extends HTMLElement {
7038
7085
  return this.#gradient.stops
7039
7086
  .map(
7040
7087
  (stop, i) =>
7041
- `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color" color="${this.#stopColorCSS(stop)}" value="${stop.position}% 50%" hit-area="4" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
7088
+ `<fig-tooltip action="manual" text="${Math.round(stop.position)}%"><fig-handle drag drag-axes="x" drag-surface=".fig-input-gradient-track" type="color" color-tip color="${this.#stopColorCSS(stop)}" value="${stop.position}% 50%" hit-area="4" data-stop-index="${i}"${disabled ? " disabled" : ""}></fig-handle></fig-tooltip>`,
7042
7089
  )
7043
7090
  .join("");
7044
7091
  }
@@ -7119,6 +7166,7 @@ class FigInputGradient extends HTMLElement {
7119
7166
  const ghost = document.createElement("fig-handle");
7120
7167
  ghost.classList.add("fig-input-gradient-ghost");
7121
7168
  ghost.setAttribute("type", "color");
7169
+ ghost.setAttribute("color-tip", "");
7122
7170
  ghost.setAttribute("control", "add");
7123
7171
  ghost.style.position = "absolute";
7124
7172
  ghost.style.top = "50%";
@@ -12123,6 +12171,14 @@ class FigFillPicker extends HTMLElement {
12123
12171
  });
12124
12172
  }
12125
12173
 
12174
+ open() {
12175
+ this.#openDialog();
12176
+ }
12177
+
12178
+ close() {
12179
+ if (this.#dialog) this.#dialog.open = false;
12180
+ }
12181
+
12126
12182
  #createDialog() {
12127
12183
  // Collect slotted custom mode content before any DOM changes
12128
12184
  this.#customSlots = {};
@@ -12270,6 +12326,7 @@ class FigFillPicker extends HTMLElement {
12270
12326
  const onDialogClose = () => {
12271
12327
  if (this.#chit) this.#chit.removeAttribute("selected");
12272
12328
  this.#emitChange();
12329
+ this.dispatchEvent(new CustomEvent("close"));
12273
12330
  };
12274
12331
  this.#dialog.addEventListener("close", onDialogClose);
12275
12332
 
@@ -14936,6 +14993,7 @@ class FigHandle extends HTMLElement {
14936
14993
  "value",
14937
14994
  "type",
14938
14995
  "control",
14996
+ "color-tip",
14939
14997
  "hit-area",
14940
14998
  "hit-area-mode",
14941
14999
  ];
@@ -14945,6 +15003,7 @@ class FigHandle extends HTMLElement {
14945
15003
  #boundPointerDown = null;
14946
15004
  #applyingValue = false;
14947
15005
  #colorTip = null;
15006
+ #directColorPicker = null;
14948
15007
  #hitAreaEl = null;
14949
15008
 
14950
15009
  get #controlMode() {
@@ -14955,6 +15014,14 @@ class FigHandle extends HTMLElement {
14955
15014
  return this.#controlMode === "add" || this.#controlMode === "remove";
14956
15015
  }
14957
15016
 
15017
+ get #usesColorTip() {
15018
+ return (
15019
+ this.#hasControlMode ||
15020
+ (this.hasAttribute("color-tip") &&
15021
+ this.getAttribute("color-tip") !== "false")
15022
+ );
15023
+ }
15024
+
14958
15025
  get #isGhost() {
14959
15026
  return this.classList.contains("fig-input-gradient-ghost");
14960
15027
  }
@@ -15174,6 +15241,7 @@ class FigHandle extends HTMLElement {
15174
15241
  disconnectedCallback() {
15175
15242
  this.#teardownDrag();
15176
15243
  this.#hideColorTip();
15244
+ this.#removeDirectColorPicker();
15177
15245
  if (this.#hitAreaEl) {
15178
15246
  this.#hitAreaEl.remove();
15179
15247
  this.#hitAreaEl = null;
@@ -15186,7 +15254,7 @@ class FigHandle extends HTMLElement {
15186
15254
  select() {
15187
15255
  if (this.hasAttribute("disabled")) return;
15188
15256
  this.setAttribute("selected", "");
15189
- if (this.getAttribute("type") === "color" && !this.#isDragging)
15257
+ if (this.getAttribute("type") === "color" && !this.#isDragging && this.#usesColorTip)
15190
15258
  this.#showColorTip();
15191
15259
  }
15192
15260
 
@@ -15201,23 +15269,30 @@ class FigHandle extends HTMLElement {
15201
15269
  this.#didDrag = false;
15202
15270
  return;
15203
15271
  }
15272
+ if (this.getAttribute("type") === "color" && !this.#usesColorTip) {
15273
+ this.#openDirectColorPicker();
15274
+ return;
15275
+ }
15204
15276
  this.select();
15205
15277
  };
15206
15278
 
15207
15279
  #handleDeselect = (e) => {
15208
15280
  if (this.#hasControlMode) return;
15209
15281
  if (this.contains(e.target)) return;
15210
- if (this.#colorTip && e.target.closest?.("dialog, [popover]")) return;
15282
+ if ((this.#colorTip || this.#directColorPicker) && e.target.closest?.("dialog, [popover]")) return;
15211
15283
  this.deselect();
15212
15284
  };
15213
15285
 
15214
15286
  #handleKeyDown = (e) => {
15215
- if (e.key !== "Enter") return;
15287
+ if (e.key !== "Enter" && e.key !== " ") return;
15216
15288
  if (!this.hasAttribute("selected")) return;
15217
15289
  if (this.getAttribute("type") !== "color") return;
15218
- if (this.#colorTip) return;
15219
15290
  e.preventDefault();
15220
- this.#showColorTip();
15291
+ if (this.#usesColorTip) {
15292
+ if (!this.#colorTip) this.#showColorTip();
15293
+ } else {
15294
+ this.#openDirectColorPicker();
15295
+ }
15221
15296
  };
15222
15297
 
15223
15298
  attributeChangedCallback(name, _old, value) {
@@ -15230,6 +15305,7 @@ class FigHandle extends HTMLElement {
15230
15305
  if (this.#colorTip && value) {
15231
15306
  this.#colorTip.setAttribute("value", value);
15232
15307
  }
15308
+ this.#syncDirectColorPickerValue();
15233
15309
  }
15234
15310
  if (name === "drag") this.#syncDrag();
15235
15311
  if (name === "hit-area") this.#syncHitArea();
@@ -15238,12 +15314,20 @@ class FigHandle extends HTMLElement {
15238
15314
  }
15239
15315
  if (name === "control") {
15240
15316
  if (this.#hasControlMode) {
15317
+ this.#removeDirectColorPicker();
15241
15318
  this.#hideColorTip();
15242
15319
  this.#showColorTip();
15243
15320
  } else {
15244
15321
  this.#hideColorTip();
15245
15322
  }
15246
15323
  }
15324
+ if (name === "color-tip") {
15325
+ if (this.#usesColorTip) {
15326
+ this.#removeDirectColorPicker();
15327
+ } else {
15328
+ this.#hideColorTip();
15329
+ }
15330
+ }
15247
15331
  }
15248
15332
 
15249
15333
  #syncDrag() {
@@ -15400,6 +15484,117 @@ class FigHandle extends HTMLElement {
15400
15484
  this.#colorTip.style.display = "none";
15401
15485
  }
15402
15486
 
15487
+ #normalizeColorForPicker(rawValue = this.getAttribute("color")) {
15488
+ const fallback = { color: "#D9D9D9", opacity: 100 };
15489
+ const value = String(rawValue || "").trim();
15490
+ if (!value) return fallback;
15491
+
15492
+ const normalizeHex = (hex) => {
15493
+ const raw = hex.replace("#", "").trim();
15494
+ if (raw.length === 3 || raw.length === 4) {
15495
+ const [r, g, b, a] = raw;
15496
+ return {
15497
+ color: `#${r}${r}${g}${g}${b}${b}`.toUpperCase(),
15498
+ opacity: a ? Math.round((parseInt(`${a}${a}`, 16) / 255) * 100) : 100,
15499
+ };
15500
+ }
15501
+ if (raw.length === 6 || raw.length === 8) {
15502
+ return {
15503
+ color: `#${raw.slice(0, 6)}`.toUpperCase(),
15504
+ opacity:
15505
+ raw.length === 8
15506
+ ? Math.round((parseInt(raw.slice(6, 8), 16) / 255) * 100)
15507
+ : 100,
15508
+ };
15509
+ }
15510
+ return fallback;
15511
+ };
15512
+
15513
+ const rgbToHex = (r, g, b) => {
15514
+ const toHex = (v) =>
15515
+ Math.max(0, Math.min(255, Math.round(Number(v))))
15516
+ .toString(16)
15517
+ .padStart(2, "0");
15518
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
15519
+ };
15520
+
15521
+ if (value.startsWith("#")) return normalizeHex(value);
15522
+
15523
+ try {
15524
+ const { ctx } = figGetSharedCanvas(1, 1);
15525
+ ctx.fillStyle = "#000000";
15526
+ ctx.fillStyle = value;
15527
+ const resolved = ctx.fillStyle;
15528
+ if (resolved.startsWith("#")) return normalizeHex(resolved);
15529
+ const rgb = resolved.match(
15530
+ /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?/i,
15531
+ );
15532
+ if (rgb) {
15533
+ return {
15534
+ color: rgbToHex(rgb[1], rgb[2], rgb[3]),
15535
+ opacity: rgb[4] !== undefined ? Math.round(parseFloat(rgb[4]) * 100) : 100,
15536
+ };
15537
+ }
15538
+ } catch {
15539
+ // Fall through to fallback.
15540
+ }
15541
+
15542
+ return fallback;
15543
+ }
15544
+
15545
+ #directColorPickerValue() {
15546
+ const { color, opacity } = this.#normalizeColorForPicker();
15547
+ return JSON.stringify(
15548
+ opacity < 100 ? { type: "solid", color, opacity } : { type: "solid", color },
15549
+ );
15550
+ }
15551
+
15552
+ #syncDirectColorPickerValue() {
15553
+ if (!this.#directColorPicker) return;
15554
+ this.#directColorPicker.setAttribute("value", this.#directColorPickerValue());
15555
+ }
15556
+
15557
+ #ensureDirectColorPicker() {
15558
+ if (this.#directColorPicker) return this.#directColorPicker;
15559
+
15560
+ const picker = document.createElement("fig-fill-picker");
15561
+ picker.setAttribute("mode", "solid");
15562
+ picker.setAttribute("alpha", "true");
15563
+ picker.setAttribute("value", this.#directColorPickerValue());
15564
+ picker.anchorElement = this;
15565
+
15566
+ const trigger = document.createElement("span");
15567
+ trigger.hidden = true;
15568
+ picker.appendChild(trigger);
15569
+
15570
+ picker.addEventListener("input", this.#handleDirectColorPickerInput);
15571
+ picker.addEventListener("change", this.#handleDirectColorPickerChange);
15572
+ picker.addEventListener("close", this.#handleDirectColorPickerClose);
15573
+ this.appendChild(picker);
15574
+ this.#directColorPicker = picker;
15575
+ return picker;
15576
+ }
15577
+
15578
+ #openDirectColorPicker() {
15579
+ if (this.hasAttribute("disabled")) return;
15580
+ this.#hideColorTip();
15581
+ const picker = this.#ensureDirectColorPicker();
15582
+ this.setAttribute("selected", "");
15583
+ this.#syncDirectColorPickerValue();
15584
+ picker.open();
15585
+ }
15586
+
15587
+ #removeDirectColorPicker() {
15588
+ if (!this.#directColorPicker) return;
15589
+ this.#directColorPicker.removeEventListener("input", this.#handleDirectColorPickerInput);
15590
+ this.#directColorPicker.removeEventListener("change", this.#handleDirectColorPickerChange);
15591
+ this.#directColorPicker.removeEventListener("close", this.#handleDirectColorPickerClose);
15592
+ this.#directColorPicker.close();
15593
+ this.#directColorPicker.remove();
15594
+ this.#directColorPicker = null;
15595
+ this.removeAttribute("selected");
15596
+ }
15597
+
15403
15598
  #showColorTip() {
15404
15599
  if (this.#colorTip) return;
15405
15600
  const tip = document.createElement("fig-color-tip");
@@ -15434,6 +15629,47 @@ class FigHandle extends HTMLElement {
15434
15629
  return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
15435
15630
  }
15436
15631
 
15632
+ #detailFromPicker(detail) {
15633
+ if (!detail?.color) return null;
15634
+ const opacity =
15635
+ detail.opacity !== undefined
15636
+ ? detail.opacity
15637
+ : detail.alpha !== undefined
15638
+ ? Math.round(detail.alpha * 100)
15639
+ : undefined;
15640
+ return { color: detail.color, opacity };
15641
+ }
15642
+
15643
+ #handleDirectColorPickerInput = (e) => {
15644
+ e.stopPropagation();
15645
+ const detail = this.#detailFromPicker(e.detail);
15646
+ if (!detail) return;
15647
+ this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
15648
+ this.dispatchEvent(
15649
+ new CustomEvent("input", {
15650
+ bubbles: true,
15651
+ detail,
15652
+ }),
15653
+ );
15654
+ };
15655
+
15656
+ #handleDirectColorPickerChange = (e) => {
15657
+ e.stopPropagation();
15658
+ const detail = this.#detailFromPicker(e.detail);
15659
+ if (!detail) return;
15660
+ this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
15661
+ this.dispatchEvent(
15662
+ new CustomEvent("change", {
15663
+ bubbles: true,
15664
+ detail,
15665
+ }),
15666
+ );
15667
+ };
15668
+
15669
+ #handleDirectColorPickerClose = () => {
15670
+ this.removeAttribute("selected");
15671
+ };
15672
+
15437
15673
  #handleColorTipInput = (e) => {
15438
15674
  e.stopPropagation();
15439
15675
  if (e.detail?.color) {
@@ -15528,7 +15764,7 @@ class FigMenu extends HTMLElement {
15528
15764
  #observer = null;
15529
15765
  #boundTriggerClick;
15530
15766
  #boundPopupClick;
15531
- #boundPopupKeydown;
15767
+ #boundMenuKeydown;
15532
15768
  #boundPopupClose;
15533
15769
  #focusedIndex = -1;
15534
15770
 
@@ -15540,7 +15776,7 @@ class FigMenu extends HTMLElement {
15540
15776
  super();
15541
15777
  this.#boundTriggerClick = this.#handleTriggerClick.bind(this);
15542
15778
  this.#boundPopupClick = this.#handlePopupClick.bind(this);
15543
- this.#boundPopupKeydown = this.#handlePopupKeydown.bind(this);
15779
+ this.#boundMenuKeydown = this.#handleMenuKeydown.bind(this);
15544
15780
  this.#boundPopupClose = this.#handlePopupClose.bind(this);
15545
15781
  }
15546
15782
 
@@ -15661,6 +15897,7 @@ class FigMenu extends HTMLElement {
15661
15897
  }
15662
15898
 
15663
15899
  #setupListeners() {
15900
+ this.addEventListener("keydown", this.#boundMenuKeydown);
15664
15901
  if (this.#trigger) {
15665
15902
  this.#trigger.addEventListener("click", this.#boundTriggerClick);
15666
15903
  this.#trigger.setAttribute("aria-haspopup", "menu");
@@ -15668,17 +15905,16 @@ class FigMenu extends HTMLElement {
15668
15905
  }
15669
15906
  if (this.#popup) {
15670
15907
  this.#popup.addEventListener("click", this.#boundPopupClick);
15671
- this.#popup.addEventListener("keydown", this.#boundPopupKeydown);
15672
15908
  }
15673
15909
  }
15674
15910
 
15675
15911
  #teardownListeners() {
15912
+ this.removeEventListener("keydown", this.#boundMenuKeydown);
15676
15913
  if (this.#trigger) {
15677
15914
  this.#trigger.removeEventListener("click", this.#boundTriggerClick);
15678
15915
  }
15679
15916
  if (this.#popup) {
15680
15917
  this.#popup.removeEventListener("click", this.#boundPopupClick);
15681
- this.#popup.removeEventListener("keydown", this.#boundPopupKeydown);
15682
15918
  }
15683
15919
  }
15684
15920
 
@@ -15713,6 +15949,27 @@ class FigMenu extends HTMLElement {
15713
15949
  return Array.from(this.#popup.querySelectorAll("fig-menu-item:not([disabled]):not([disabled='true'])"));
15714
15950
  }
15715
15951
 
15952
+ #syncFocusedIndex() {
15953
+ const items = this.#getItems();
15954
+ if (!items.length) {
15955
+ this.#focusedIndex = -1;
15956
+ return;
15957
+ }
15958
+ const active = document.activeElement;
15959
+ const idx = items.findIndex(
15960
+ (item) => item === active || item.contains(active),
15961
+ );
15962
+ this.#focusedIndex = idx >= 0 ? idx : -1;
15963
+ }
15964
+
15965
+ #focusItemAt(index) {
15966
+ const items = this.#getItems();
15967
+ if (!items.length) return;
15968
+ const clamped = Math.max(0, Math.min(index, items.length - 1));
15969
+ this.#focusedIndex = clamped;
15970
+ items[clamped].focus();
15971
+ }
15972
+
15716
15973
  #syncDisabled() {
15717
15974
  if (!this.#trigger) return;
15718
15975
  const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
@@ -15726,7 +15983,12 @@ class FigMenu extends HTMLElement {
15726
15983
  #handleTriggerClick(e) {
15727
15984
  if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
15728
15985
  e.stopPropagation();
15729
- if (this.open) {
15986
+ const popupShowing = this.#popup?.matches?.(":open") ?? false;
15987
+ if (this.open && !popupShowing) {
15988
+ this.removeAttribute("open");
15989
+ }
15990
+ const effectiveOpen = this.open && popupShowing;
15991
+ if (effectiveOpen) {
15730
15992
  this.open = false;
15731
15993
  } else {
15732
15994
  this.open = true;
@@ -15741,40 +16003,42 @@ class FigMenu extends HTMLElement {
15741
16003
  this.#selectItem(item);
15742
16004
  }
15743
16005
 
15744
- #handlePopupKeydown(e) {
16006
+ #handleMenuKeydown(e) {
16007
+ if (!this.open || !this.#popup?.matches?.(":open")) return;
16008
+
15745
16009
  const items = this.#getItems();
15746
16010
  if (!items.length) return;
15747
16011
 
15748
16012
  switch (e.key) {
15749
16013
  case "ArrowDown": {
15750
16014
  e.preventDefault();
15751
- this.#focusedIndex = Math.min(this.#focusedIndex + 1, items.length - 1);
15752
- items[this.#focusedIndex]?.focus();
16015
+ this.#syncFocusedIndex();
16016
+ this.#focusItemAt(this.#focusedIndex + 1);
15753
16017
  break;
15754
16018
  }
15755
16019
  case "ArrowUp": {
15756
16020
  e.preventDefault();
15757
- this.#focusedIndex = Math.max(this.#focusedIndex - 1, 0);
15758
- items[this.#focusedIndex]?.focus();
16021
+ this.#syncFocusedIndex();
16022
+ this.#focusItemAt(this.#focusedIndex - 1);
15759
16023
  break;
15760
16024
  }
15761
16025
  case "Home": {
15762
16026
  e.preventDefault();
15763
- this.#focusedIndex = 0;
15764
- items[0]?.focus();
16027
+ this.#focusItemAt(0);
15765
16028
  break;
15766
16029
  }
15767
16030
  case "End": {
15768
16031
  e.preventDefault();
15769
- this.#focusedIndex = items.length - 1;
15770
- items[this.#focusedIndex]?.focus();
16032
+ this.#focusItemAt(items.length - 1);
15771
16033
  break;
15772
16034
  }
15773
16035
  case "Enter":
15774
16036
  case " ": {
15775
- e.preventDefault();
16037
+ this.#syncFocusedIndex();
15776
16038
  const focused = items[this.#focusedIndex];
15777
- if (focused) this.#selectItem(focused);
16039
+ if (!focused) return;
16040
+ e.preventDefault();
16041
+ this.#selectItem(focused);
15778
16042
  break;
15779
16043
  }
15780
16044
  }
@@ -15809,10 +16073,10 @@ class FigMenu extends HTMLElement {
15809
16073
  if (this.#trigger) {
15810
16074
  this.#trigger.setAttribute("aria-expanded", "true");
15811
16075
  }
15812
- this.#focusedIndex = 0;
16076
+ this.#focusedIndex = -1;
15813
16077
  requestAnimationFrame(() => {
15814
- const items = this.#getItems();
15815
- if (items.length) items[0].focus();
16078
+ if (!this.#trigger?.matches?.(":focus-visible")) return;
16079
+ this.#focusItemAt(0);
15816
16080
  });
15817
16081
  }
15818
16082
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "4.15.1",
3
+ "version": "4.15.4",
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": "MIT",