@rogieking/figui3 8.2.0 → 8.3.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.
package/fig-editor.css CHANGED
@@ -702,7 +702,7 @@ fig-select-options,
702
702
 
703
703
  }
704
704
 
705
- fig-select-options > fig-separator {
705
+ fig-select-options > :is(fig-separator, fig-menu-separator) {
706
706
  /* Align labels with select option text after the checkmark column. */
707
707
  &[label]:not([label=""]) {
708
708
  --fig-menu-inline-padding: 0 var(--spacer-2) 0 var(--spacer-5);
package/fig-lab.js CHANGED
@@ -4267,7 +4267,7 @@ class PropskitSlider extends HTMLElement {
4267
4267
  }
4268
4268
 
4269
4269
  #pushExternalValueToSlider() {
4270
- if (!this.#slider) return;
4270
+ if (!this.#slider || !this.hasAttribute("value")) return;
4271
4271
  const next = this.getAttribute("value") ?? "";
4272
4272
  if (String(this.#slider.value) !== next) {
4273
4273
  this.#slider.value = next;
package/fig.js CHANGED
@@ -5173,7 +5173,6 @@ figDefineElement("fig-options", FigOptions);
5173
5173
  class FigSlider extends HTMLElement {
5174
5174
  #isInteracting = false;
5175
5175
  #pendingRegeneration = false;
5176
- #showEmptyTextValue = false;
5177
5176
  #isSyncingValueAttribute = false;
5178
5177
  #value = "";
5179
5178
  #a11yAttributes = [
@@ -5278,10 +5277,6 @@ class FigSlider extends HTMLElement {
5278
5277
  : this.type === "delta"
5279
5278
  ? 0
5280
5279
  : this.min;
5281
- this.#showEmptyTextValue =
5282
- this.type !== "range" &&
5283
- (rawValue === null ||
5284
- (typeof rawValue === "string" && rawValue.trim() === ""));
5285
5280
  this.value = this.#normalizeSliderValue(rawValue);
5286
5281
  }
5287
5282
 
@@ -5321,10 +5316,7 @@ class FigSlider extends HTMLElement {
5321
5316
  this.figInputNumber.setAttribute("max", String(this.max));
5322
5317
  this.figInputNumber.setAttribute("transform", String(this.transform));
5323
5318
  this.figInputNumber.setAttribute("step", String(this.step));
5324
- this.figInputNumber.setAttribute(
5325
- "value",
5326
- this.#showEmptyTextValue ? "" : String(this.value),
5327
- );
5319
+ this.figInputNumber.setAttribute("value", String(this.value));
5328
5320
  if (this.units) this.figInputNumber.setAttribute("units", this.units);
5329
5321
  else this.figInputNumber.removeAttribute("units");
5330
5322
  if (this.precision !== null) {
@@ -5411,7 +5403,7 @@ class FigSlider extends HTMLElement {
5411
5403
  max="${this.max}"
5412
5404
  transform="${this.transform}"
5413
5405
  step="${this.step}"
5414
- value="${this.#showEmptyTextValue ? "" : this.value}"
5406
+ value="${this.value}"
5415
5407
  ${this.units ? `units="${figEscapeAttribute(this.units)}"` : ""}
5416
5408
  ${this.precision !== null ? `precision="${this.precision}"` : ""}>
5417
5409
  </fig-input-number>`;
@@ -5487,8 +5479,6 @@ class FigSlider extends HTMLElement {
5487
5479
 
5488
5480
  set value(value) {
5489
5481
  const rawValue = value === null || value === undefined ? "" : String(value);
5490
- this.#showEmptyTextValue =
5491
- this.type !== "range" && rawValue.trim() === "";
5492
5482
  const hasParsedBounds = this.min !== undefined || this.max !== undefined;
5493
5483
  const normalized = hasParsedBounds
5494
5484
  ? String(this.#normalizeSliderValue(rawValue))
@@ -5504,10 +5494,7 @@ class FigSlider extends HTMLElement {
5504
5494
  this.input.setAttribute("aria-valuenow", normalized);
5505
5495
  }
5506
5496
  if (this.figInputNumber) {
5507
- this.figInputNumber.setAttribute(
5508
- "value",
5509
- this.#showEmptyTextValue ? "" : normalized,
5510
- );
5497
+ this.figInputNumber.setAttribute("value", normalized);
5511
5498
  }
5512
5499
  if (this.input) this.#syncProperties();
5513
5500
  }
@@ -5537,10 +5524,6 @@ class FigSlider extends HTMLElement {
5537
5524
  #handleTextInput() {
5538
5525
  if (this.figInputNumber) {
5539
5526
  const rawTextValue = this.figInputNumber.value;
5540
- this.#showEmptyTextValue =
5541
- rawTextValue === null ||
5542
- rawTextValue === undefined ||
5543
- (typeof rawTextValue === "string" && rawTextValue.trim() === "");
5544
5527
  const normalized = this.#normalizeSliderValue(rawTextValue);
5545
5528
  this.value = normalized;
5546
5529
  this.input.value = String(normalized);
@@ -5624,10 +5607,7 @@ class FigSlider extends HTMLElement {
5624
5607
  // Update ARIA value
5625
5608
  this.input.setAttribute("aria-valuenow", val);
5626
5609
  if (this.figInputNumber) {
5627
- this.figInputNumber.setAttribute(
5628
- "value",
5629
- this.#showEmptyTextValue ? "" : val,
5630
- );
5610
+ this.figInputNumber.setAttribute("value", val);
5631
5611
  }
5632
5612
  }
5633
5613
  #syncInputA11yAttributes() {
@@ -5665,7 +5645,6 @@ class FigSlider extends HTMLElement {
5665
5645
  }
5666
5646
 
5667
5647
  #handleInput() {
5668
- this.#showEmptyTextValue = false;
5669
5648
  this.#syncValue();
5670
5649
  this.dispatchEvent(
5671
5650
  new CustomEvent("input", { detail: this.value, bubbles: true }),
@@ -5674,7 +5653,6 @@ class FigSlider extends HTMLElement {
5674
5653
 
5675
5654
  #handleChange() {
5676
5655
  this.#isInteracting = false;
5677
- this.#showEmptyTextValue = false;
5678
5656
  this.#syncValue();
5679
5657
  this.dispatchEvent(
5680
5658
  new CustomEvent("change", { detail: this.value, bubbles: true }),
@@ -5690,7 +5668,6 @@ class FigSlider extends HTMLElement {
5690
5668
  }
5691
5669
 
5692
5670
  event.preventDefault();
5693
- this.#showEmptyTextValue = false;
5694
5671
 
5695
5672
  const direction =
5696
5673
  event.key === "ArrowRight" || event.key === "ArrowUp" ? 1 : -1;
@@ -5712,10 +5689,6 @@ class FigSlider extends HTMLElement {
5712
5689
  #handleTextChange() {
5713
5690
  if (this.figInputNumber) {
5714
5691
  const rawTextValue = this.figInputNumber.value;
5715
- this.#showEmptyTextValue =
5716
- rawTextValue === null ||
5717
- rawTextValue === undefined ||
5718
- (typeof rawTextValue === "string" && rawTextValue.trim() === "");
5719
5692
  const normalized = this.#normalizeSliderValue(rawTextValue);
5720
5693
  this.value = normalized;
5721
5694
  this.input.value = String(normalized);
@@ -9868,6 +9841,30 @@ class FigInputGradient extends HTMLElement {
9868
9841
  );
9869
9842
  }
9870
9843
 
9844
+ #activeStopHandle() {
9845
+ if (!this.#track) return null;
9846
+ const selected = this.#track.querySelector(
9847
+ "fig-handle[selected]:not(.fig-input-gradient-ghost)",
9848
+ );
9849
+ if (selected) return selected;
9850
+ const active = document.activeElement;
9851
+ if (!(active instanceof Element) || !this.#track.contains(active)) {
9852
+ return null;
9853
+ }
9854
+ return active.closest("fig-handle:not(.fig-input-gradient-ghost)");
9855
+ }
9856
+
9857
+ #activateStopHandle(handle) {
9858
+ if (!handle || !this.#track) return;
9859
+ this.#track
9860
+ .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
9861
+ .forEach((other) => {
9862
+ if (other !== handle) other.deselect();
9863
+ });
9864
+ handle.select();
9865
+ handle.focus();
9866
+ }
9867
+
9871
9868
  #syncFocusTarget() {
9872
9869
  const disabled =
9873
9870
  this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
@@ -9937,7 +9934,15 @@ class FigInputGradient extends HTMLElement {
9937
9934
 
9938
9935
  #onKeyDown = (e) => {
9939
9936
  const active = document.activeElement;
9940
- if (!(active instanceof Node) || !this.contains(active)) return;
9937
+ const activeIsInside = active instanceof Node && this.contains(active);
9938
+ const selectedHandle = this.#track?.querySelector(
9939
+ "fig-handle[selected]:not(.fig-input-gradient-ghost)",
9940
+ );
9941
+ const focusLostToBody =
9942
+ !active ||
9943
+ active === document.body ||
9944
+ active === document.documentElement;
9945
+ if (!activeIsInside && !(selectedHandle && focusLostToBody)) return;
9941
9946
  const isTyping =
9942
9947
  active &&
9943
9948
  (active.tagName === "INPUT" ||
@@ -9946,9 +9951,7 @@ class FigInputGradient extends HTMLElement {
9946
9951
  if (!this.#track) return;
9947
9952
 
9948
9953
  if (e.key === "Tab" && !isTyping) {
9949
- const selected = this.#track.querySelector(
9950
- "fig-handle[selected]:not(.fig-input-gradient-ghost)",
9951
- );
9954
+ const selected = this.#activeStopHandle();
9952
9955
  if (!selected) return;
9953
9956
  e.preventDefault();
9954
9957
  const handles = [
@@ -9961,14 +9964,12 @@ class FigInputGradient extends HTMLElement {
9961
9964
  ? (curIdx - 1 + handles.length) % handles.length
9962
9965
  : (curIdx + 1) % handles.length;
9963
9966
  selected.deselect();
9964
- handles[next].select();
9967
+ this.#activateStopHandle(handles[next]);
9965
9968
  return;
9966
9969
  }
9967
9970
 
9968
9971
  if ((e.key === "ArrowLeft" || e.key === "ArrowRight") && !isTyping) {
9969
- const selected = this.#track.querySelector(
9970
- "fig-handle[selected]:not(.fig-input-gradient-ghost)",
9971
- );
9972
+ const selected = this.#activeStopHandle();
9972
9973
  if (!selected) return;
9973
9974
  const idx = parseInt(selected.dataset.stopIndex, 10);
9974
9975
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
@@ -10000,19 +10001,21 @@ class FigInputGradient extends HTMLElement {
10000
10001
  if (e.key !== "Delete" && e.key !== "Backspace") return;
10001
10002
  if (isTyping) return;
10002
10003
  if (this.#gradient.stops.length <= 2) return;
10003
- const selected = this.#track.querySelector(
10004
- "fig-handle[selected]:not(.fig-input-gradient-ghost)",
10005
- );
10004
+ const selected = this.#activeStopHandle();
10006
10005
  if (!selected) return;
10007
10006
  const idx = parseInt(selected.dataset.stopIndex, 10);
10008
10007
  if (isNaN(idx) || !this.#gradient.stops[idx]) return;
10009
10008
  e.preventDefault();
10010
- selected.removeAttribute("selected");
10011
10009
  this.#gradient.stops.splice(idx, 1);
10012
10010
  this.#syncHandles();
10013
10011
  this.#syncSwatch();
10014
10012
  this.#emitInput();
10015
10013
  this.#emitChange();
10014
+ const nextIdx = Math.min(idx, this.#gradient.stops.length - 1);
10015
+ const next = this.#track.querySelectorAll(
10016
+ "fig-handle:not(.fig-input-gradient-ghost)",
10017
+ )[nextIdx];
10018
+ this.#activateStopHandle(next);
10016
10019
  };
10017
10020
 
10018
10021
  #onPickerKeyDown = (e) => {
@@ -10306,7 +10309,7 @@ class FigInputGradient extends HTMLElement {
10306
10309
  "fig-handle:not(.fig-input-gradient-ghost)",
10307
10310
  );
10308
10311
  const newHandle = handles[newIndex];
10309
- if (newHandle) newHandle.click();
10312
+ if (newHandle) this.#activateStopHandle(newHandle);
10310
10313
  });
10311
10314
  };
10312
10315
 
@@ -10473,12 +10476,7 @@ class FigInputGradient extends HTMLElement {
10473
10476
  );
10474
10477
  const newHandle = handles[newIndex];
10475
10478
  if (newHandle) {
10476
- this.#track
10477
- .querySelectorAll("fig-handle:not(.fig-input-gradient-ghost)")
10478
- .forEach((h) => {
10479
- if (h !== newHandle) h.deselect();
10480
- });
10481
- newHandle.select();
10479
+ this.#activateStopHandle(newHandle);
10482
10480
  newHandle.dispatchEvent(
10483
10481
  new PointerEvent("pointerdown", {
10484
10482
  bubbles: true,
@@ -17822,15 +17820,14 @@ class FigHandle extends HTMLElement {
17822
17820
  this.#didDrag = false;
17823
17821
  return;
17824
17822
  }
17823
+ this.select();
17825
17824
  if (
17826
17825
  this.getAttribute("type") === "color" &&
17827
17826
  this.#canOpenColorPicker &&
17828
17827
  !this.#tipMode
17829
17828
  ) {
17830
17829
  this.#openDirectColorPicker();
17831
- return;
17832
17830
  }
17833
- this.select();
17834
17831
  };
17835
17832
 
17836
17833
  #handleDeselect = (e) => {
@@ -18012,6 +18009,8 @@ class FigHandle extends HTMLElement {
18012
18009
  #onPointerDown(e) {
18013
18010
  if (!this.#dragEnabled || figBooleanAttribute(this, "disabled")) return;
18014
18011
  e.preventDefault();
18012
+ this.focus();
18013
+ this.select();
18015
18014
  const container = this.#getContainer();
18016
18015
  if (!container) return;
18017
18016
 
@@ -18411,17 +18410,19 @@ class FigHandle extends HTMLElement {
18411
18410
  e.stopPropagation();
18412
18411
  const detail = this.#detailFromNativeColor(e.target.value);
18413
18412
  this.setAttribute("color", this.#colorWithOpacity(detail.color, detail.opacity));
18414
- this.removeAttribute("selected");
18415
18413
  this.dispatchEvent(
18416
18414
  new CustomEvent("change", {
18417
18415
  bubbles: true,
18418
18416
  detail,
18419
18417
  }),
18420
18418
  );
18419
+ if (this.isConnected) this.focus();
18421
18420
  };
18422
18421
 
18423
18422
  #handleDirectColorPickerClose = () => {
18424
- this.removeAttribute("selected");
18423
+ if (!this.isConnected) return;
18424
+ this.setAttribute("selected", "");
18425
+ this.focus();
18425
18426
  };
18426
18427
 
18427
18428
  #handleColorTipInput = (e) => {
@@ -18585,6 +18586,19 @@ class FigSeparator extends HTMLElement {
18585
18586
  }
18586
18587
  figDefineElement("fig-separator", FigSeparator);
18587
18588
 
18589
+ // Backwards-compatible alias. Custom element constructors cannot be registered
18590
+ // under multiple tag names, so the legacy tag uses an otherwise empty subclass.
18591
+ class FigMenuSeparator extends FigSeparator {}
18592
+ figDefineElement("fig-menu-separator", FigMenuSeparator);
18593
+
18594
+ const FIG_MENU_CHILD_SELECTOR =
18595
+ ":scope > fig-menu-item, :scope > fig-separator, :scope > fig-menu-separator";
18596
+ const FIG_MENU_CHILD_TAGS = new Set([
18597
+ "FIG-MENU-ITEM",
18598
+ "FIG-SEPARATOR",
18599
+ "FIG-MENU-SEPARATOR",
18600
+ ]);
18601
+
18588
18602
  class FigMenu extends HTMLElement {
18589
18603
  #popup = null;
18590
18604
  #panel = null;
@@ -18661,9 +18675,7 @@ class FigMenu extends HTMLElement {
18661
18675
  if (this.#popup) {
18662
18676
  this.#popup.removeEventListener("close", this.#boundPopupClose);
18663
18677
  const items = Array.from(
18664
- this.#panel?.querySelectorAll(
18665
- ":scope > fig-menu-item, :scope > fig-separator",
18666
- ) ?? [],
18678
+ this.#panel?.querySelectorAll(FIG_MENU_CHILD_SELECTOR) ?? [],
18667
18679
  );
18668
18680
  for (const item of items) this.insertBefore(item, this.#popup);
18669
18681
  this.#popup.remove();
@@ -18705,7 +18717,9 @@ class FigMenu extends HTMLElement {
18705
18717
  #detectTrigger() {
18706
18718
  this.#trigger =
18707
18719
  this.querySelector("[fig-menu-trigger]") ||
18708
- this.querySelector(":scope > :not(fig-menu-item):not(fig-separator)");
18720
+ this.querySelector(
18721
+ ":scope > :not(fig-menu-item):not(fig-separator):not(fig-menu-separator)",
18722
+ );
18709
18723
  }
18710
18724
 
18711
18725
  #usesContextMenuTrigger() {
@@ -18744,9 +18758,7 @@ class FigMenu extends HTMLElement {
18744
18758
 
18745
18759
  #moveItemsToPopup() {
18746
18760
  if (!this.#panel) return;
18747
- const items = Array.from(this.querySelectorAll(
18748
- ":scope > fig-menu-item, :scope > fig-separator"
18749
- ));
18761
+ const items = Array.from(this.querySelectorAll(FIG_MENU_CHILD_SELECTOR));
18750
18762
  for (const item of items) {
18751
18763
  this.#panel.appendChild(item);
18752
18764
  }
@@ -18785,8 +18797,7 @@ class FigMenu extends HTMLElement {
18785
18797
  for (const node of mutation.addedNodes) {
18786
18798
  if (node.nodeType !== 1 || node === this.#popup) continue;
18787
18799
  if (
18788
- (node.tagName === "FIG-MENU-ITEM" ||
18789
- node.tagName === "FIG-SEPARATOR") &&
18800
+ FIG_MENU_CHILD_TAGS.has(node.tagName) &&
18790
18801
  node.parentElement === this
18791
18802
  ) {
18792
18803
  this.#panel?.appendChild(node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.2.0",
3
+ "version": "8.3.0",
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",