@rogieking/figui3 11.3.0 → 11.3.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
@@ -6342,6 +6342,7 @@ figDefineElement("fig-options", FigOptions);
6342
6342
  * @attr {number} min - The minimum value
6343
6343
  * @attr {number} max - The maximum value
6344
6344
  * @attr {number} step - The step increment
6345
+ * @attr {number} default - Explicit tick and native snap target
6345
6346
  * @attr {boolean} text - Whether to show a text input alongside the slider (default true)
6346
6347
  * @attr {string} placeholder - Placeholder for the number input when text is enabled
6347
6348
  * @attr {string} units - The units to display after the value
@@ -6634,6 +6635,8 @@ class FigSlider extends HTMLElement {
6634
6635
  this.datalist.append(option);
6635
6636
  this.inputContainer.append(this.datalist);
6636
6637
  this.input.setAttribute("list", this.datalist.getAttribute("id"));
6638
+ } else if (this.#hasExplicitValidDefault()) {
6639
+ this.#createDefaultDatalist();
6637
6640
  }
6638
6641
  this.#syncDefaultDatalist();
6639
6642
  this.#syncValue();
@@ -6754,6 +6757,25 @@ class FigSlider extends HTMLElement {
6754
6757
  const tolerance = Number.EPSILON * Math.max(1, Math.abs(steps)) * 16;
6755
6758
  return Math.abs(steps - nearest) <= tolerance;
6756
6759
  }
6760
+ #supportsGeneratedDefaultDatalist() {
6761
+ return ["range", "hue", "opacity"].includes(this.type);
6762
+ }
6763
+ #hasExplicitValidDefault() {
6764
+ if (
6765
+ !this.hasAttribute("default") ||
6766
+ !this.#supportsGeneratedDefaultDatalist()
6767
+ ) {
6768
+ return false;
6769
+ }
6770
+ const { min, max } = this.#getBounds();
6771
+ const parsed = this.#toFiniteNumber(this.getAttribute("default"));
6772
+ return (
6773
+ parsed !== null &&
6774
+ parsed >= min &&
6775
+ parsed <= max &&
6776
+ this.#isStepAligned(parsed)
6777
+ );
6778
+ }
6757
6779
  #resolveDefaultValue(rawDefault = this.getAttribute("default")) {
6758
6780
  const { min, max } = this.#getBounds();
6759
6781
  const parsed = this.#toFiniteNumber(rawDefault);
@@ -6959,8 +6981,24 @@ class FigSlider extends HTMLElement {
6959
6981
  }
6960
6982
 
6961
6983
  #syncDefaultDatalist() {
6984
+ const generatedDefault =
6985
+ this.datalist?.getAttribute("data-generated") === "slider-default";
6986
+ const shouldGenerateDefault = this.#hasExplicitValidDefault();
6987
+ if (generatedDefault && !shouldGenerateDefault) {
6988
+ this.datalist.remove();
6989
+ this.datalist = null;
6990
+ this.input?.removeAttribute("list");
6991
+ } else if (!this.datalist && shouldGenerateDefault) {
6992
+ this.#createDefaultDatalist();
6993
+ }
6962
6994
  if (!this.datalist) return;
6963
6995
  const defaultValue = String(this.default);
6996
+ if (generatedDefault) {
6997
+ const option =
6998
+ this.datalist.querySelector("option") ?? document.createElement("option");
6999
+ option.value = defaultValue;
7000
+ this.datalist.replaceChildren(option);
7001
+ }
6964
7002
  const options = [...this.datalist.querySelectorAll("option")];
6965
7003
  options.forEach((option) => option.removeAttribute("default"));
6966
7004
  let defaultOption = options.find((option) => option.value === defaultValue);
@@ -6976,6 +7014,17 @@ class FigSlider extends HTMLElement {
6976
7014
  defaultOption?.setAttribute("default", "true");
6977
7015
  }
6978
7016
 
7017
+ #createDefaultDatalist() {
7018
+ this.datalist = document.createElement("datalist");
7019
+ this.datalist.setAttribute("data-generated", "slider-default");
7020
+ this.datalist.setAttribute("id", figUniqueId());
7021
+ const option = document.createElement("option");
7022
+ option.value = String(this.default);
7023
+ this.datalist.append(option);
7024
+ this.inputContainer.append(this.datalist);
7025
+ this.input.setAttribute("list", this.datalist.id);
7026
+ }
7027
+
6979
7028
  #regenerateWhenIdle() {
6980
7029
  if (this.#isInteracting) {
6981
7030
  this.#pendingRegeneration = true;
@@ -7779,6 +7828,12 @@ class FigInputNumber extends HTMLElement {
7779
7828
  #precision;
7780
7829
  #isInteracting = false;
7781
7830
  #stepperEl = null;
7831
+ #stepperPointerId = null;
7832
+ #stepperRepeatTimeout = 0;
7833
+ #stepperRepeatInterval = 0;
7834
+ #stepperDidChange = false;
7835
+ #boundStepperPointerEnd;
7836
+ #boundStepperWindowBlur;
7782
7837
  #mutationObserver = null;
7783
7838
  #syncRaf = 0;
7784
7839
  #previousUserSelect = "";
@@ -7791,6 +7846,8 @@ class FigInputNumber extends HTMLElement {
7791
7846
  "aria-required",
7792
7847
  ];
7793
7848
  static #DEFAULT_UNITS_DISALLOW = "px";
7849
+ static #STEPPER_REPEAT_DELAY = 350;
7850
+ static #STEPPER_REPEAT_INTERVAL = 60;
7794
7851
 
7795
7852
  #parseUnitsDisallowList(value) {
7796
7853
  return (value || "")
@@ -7832,11 +7889,12 @@ class FigInputNumber extends HTMLElement {
7832
7889
  const btn = e.target.closest("button");
7833
7890
  if (!btn || this.disabled || btn.disabled) return;
7834
7891
  const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
7835
- this.#stepValue(dir);
7892
+ this.#startStepperRepeat(e, dir);
7836
7893
  this.input.focus();
7837
7894
  });
7838
7895
  this.append(this.#stepperEl);
7839
7896
  } else if (!hasSteppers && this.#stepperEl) {
7897
+ this.#stopStepperRepeat();
7840
7898
  this.#stepperEl.remove();
7841
7899
  this.#stepperEl = null;
7842
7900
  }
@@ -7867,13 +7925,60 @@ class FigInputNumber extends HTMLElement {
7867
7925
  down.disabled = disabled || atMin;
7868
7926
  }
7869
7927
 
7928
+ #startStepperRepeat(event, direction) {
7929
+ this.#stopStepperRepeat();
7930
+ this.#stepperPointerId = event.pointerId;
7931
+ this.#stepperDidChange = this.#stepValue(direction);
7932
+ window.addEventListener("pointerup", this.#boundStepperPointerEnd);
7933
+ window.addEventListener("pointercancel", this.#boundStepperPointerEnd);
7934
+ window.addEventListener("blur", this.#boundStepperWindowBlur);
7935
+ if (!this.#stepperDidChange) return;
7936
+
7937
+ this.#stepperRepeatTimeout = window.setTimeout(() => {
7938
+ this.#stepperRepeatTimeout = 0;
7939
+ this.#stepperRepeatInterval = window.setInterval(() => {
7940
+ const changed = this.#stepValue(direction);
7941
+ this.#stepperDidChange ||= changed;
7942
+ if (!changed) this.#clearStepperRepeatTimers();
7943
+ }, FigInputNumber.#STEPPER_REPEAT_INTERVAL);
7944
+ }, FigInputNumber.#STEPPER_REPEAT_DELAY);
7945
+ }
7946
+
7947
+ #clearStepperRepeatTimers() {
7948
+ if (this.#stepperRepeatTimeout) {
7949
+ clearTimeout(this.#stepperRepeatTimeout);
7950
+ this.#stepperRepeatTimeout = 0;
7951
+ }
7952
+ if (this.#stepperRepeatInterval) {
7953
+ clearInterval(this.#stepperRepeatInterval);
7954
+ this.#stepperRepeatInterval = 0;
7955
+ }
7956
+ }
7957
+
7958
+ #stopStepperRepeat(commit = true) {
7959
+ this.#clearStepperRepeatTimers();
7960
+ window.removeEventListener("pointerup", this.#boundStepperPointerEnd);
7961
+ window.removeEventListener("pointercancel", this.#boundStepperPointerEnd);
7962
+ window.removeEventListener("blur", this.#boundStepperWindowBlur);
7963
+ this.#stepperPointerId = null;
7964
+ if (commit && this.#stepperDidChange) {
7965
+ this.dispatchEvent(
7966
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
7967
+ );
7968
+ }
7969
+ this.#stepperDidChange = false;
7970
+ }
7971
+
7870
7972
  #stepValue(direction) {
7973
+ if (this.disabled) return false;
7871
7974
  const step = this.step || 1;
7975
+ const previousValue = this.value;
7872
7976
  let numericValue = this.#getNumericValue(this.input.value);
7873
7977
  let value =
7874
7978
  (numericValue !== "" ? Number(numericValue) / (this.transform || 1) : 0) +
7875
7979
  step * direction;
7876
7980
  value = this.#sanitizeInput(value, false);
7981
+ if (value === previousValue) return false;
7877
7982
  this.value = value;
7878
7983
  this.input.value = this.#formatWithUnit(this.value);
7879
7984
  this.#syncStepperState();
@@ -7881,9 +7986,7 @@ class FigInputNumber extends HTMLElement {
7881
7986
  this.dispatchEvent(
7882
7987
  new CustomEvent("input", { detail: this.value, bubbles: true }),
7883
7988
  );
7884
- this.dispatchEvent(
7885
- new CustomEvent("change", { detail: this.value, bubbles: true }),
7886
- );
7989
+ return true;
7887
7990
  }
7888
7991
 
7889
7992
  constructor() {
@@ -7910,6 +8013,16 @@ class FigInputNumber extends HTMLElement {
7910
8013
  this.#boundKeyDown = (e) => {
7911
8014
  this.#handleKeyDown(e);
7912
8015
  };
8016
+ this.#boundStepperPointerEnd = (e) => {
8017
+ if (
8018
+ this.#stepperPointerId !== null &&
8019
+ e.pointerId !== this.#stepperPointerId
8020
+ ) {
8021
+ return;
8022
+ }
8023
+ this.#stopStepperRepeat();
8024
+ };
8025
+ this.#boundStepperWindowBlur = () => this.#stopStepperRepeat();
7913
8026
  this.#boundFocusControl = this.focus.bind(this);
7914
8027
  this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
7915
8028
  }
@@ -8001,6 +8114,7 @@ class FigInputNumber extends HTMLElement {
8001
8114
  window.removeEventListener("pointermove", this.#boundMouseMove);
8002
8115
  window.removeEventListener("pointerup", this.#boundMouseUp);
8003
8116
  window.removeEventListener("blur", this.#boundWindowBlur);
8117
+ this.#stopStepperRepeat(false);
8004
8118
  this.#mutationObserver?.disconnect();
8005
8119
  this.#mutationObserver = null;
8006
8120
  if (this.#syncRaf) {
@@ -8375,6 +8489,7 @@ class FigInputNumber extends HTMLElement {
8375
8489
  case "disabled":
8376
8490
  this.disabled = this.input.disabled =
8377
8491
  newValue !== null && newValue !== "false";
8492
+ if (this.disabled) this.#stopStepperRepeat();
8378
8493
  this.#syncStepperState();
8379
8494
  break;
8380
8495
  case "units":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "11.3.0",
3
+ "version": "11.3.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",