@rogieking/figui3 8.9.32 → 8.9.33

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-lab.js CHANGED
@@ -5546,43 +5546,25 @@ class PropskitSlider extends HTMLElement {
5546
5546
  figLabDefineElement("propskit-slider", PropskitSlider);
5547
5547
 
5548
5548
  /**
5549
- * Full-surface numeric wheel: SVG ticks + fig-input-number.
5549
+ * Standalone interactive numeric wheel.
5550
5550
  *
5551
- * @attr {string} label - Field label. Omitted defaults to "Value"; authored values (including blank) are used as-is.
5552
- * @attr {string} units - Optional unit forwarded to fig-input-number. Time aliases are normalized.
5553
5551
  * @attr {number} value - Numeric value. Unbounded unless min/max are set.
5554
5552
  * @attr {number} min - Inclusive lower bound. Omitted = unbounded below.
5555
5553
  * @attr {number} max - Inclusive upper bound. Omitted = unbounded above.
5556
- * @attr {string} default - Reset target.
5557
- * @attr {number} step - Scrub/keyboard/wheel increment. Defaults: s=0.1, ms=100, otherwise 1.
5558
- * @attr {number} precision - Display decimals forwarded to fig-input-number. Defaults: s=2, otherwise 0.
5559
- * @attr {boolean|string} elastic - Enables resisted handle movement while scrubbing. Defaults to true.
5560
- * @attr {string} size - Defaults to the large row; set to "small" for compact.
5561
- * @attr {boolean|string} disabled - Disables wheel and number.
5562
- * @attr {string} variant - Set to "minimal" for compact chrome.
5563
- * @fires input - Composed event while dragging or typing.
5564
- * @fires change - Composed event on commit.
5554
+ * @attr {number} step - Increment. Defaults to 1.
5555
+ * @attr {boolean|string} elastic - Enables resisted handle movement. Defaults to true.
5556
+ * @attr {boolean|string} disabled - Disables interaction and focus.
5557
+ * @fires input - Numeric composed event during interaction.
5558
+ * @fires change - Numeric composed event on commit.
5565
5559
  */
5566
- class PropskitWheel extends HTMLElement {
5560
+ class FigInputWheel extends HTMLElement {
5567
5561
  static #TICK_COUNT = 33;
5568
5562
  static #HALF_FOV_DEG = 60;
5569
5563
  static #PERSPECTIVE_K = 0.55;
5570
- static #DRAGGING_BODY_CLASS = "fig-propskit-wheel-dragging";
5571
- static #RESERVED_ATTRS = new Set([
5572
- "label",
5573
- "size",
5574
- "disabled",
5575
- "variant",
5576
- "elastic",
5577
- "default",
5578
- "class",
5579
- "style",
5580
- "id",
5581
- "oninput",
5582
- "onchange",
5583
- ]);
5564
+ static #DRAGGING_BODY_CLASS = "fig-input-wheel-dragging";
5584
5565
 
5585
5566
  #surface = null;
5567
+ #track = null;
5586
5568
  #wheel = null;
5587
5569
  #svg = null;
5588
5570
  #tickPath = null;
@@ -5593,13 +5575,7 @@ class PropskitWheel extends HTMLElement {
5593
5575
  #layoutFrame = 0;
5594
5576
  #keyboardAnimationFrame = 0;
5595
5577
  #keyboardSettleTimer = 0;
5596
- #label = null;
5597
- #input = null;
5598
- #hasCustomLabel = false;
5599
- #observer = null;
5600
5578
  #resizeObserver = null;
5601
- #managedInputAttrs = new Set();
5602
- #initialValue = null;
5603
5579
  #isDragging = false;
5604
5580
  #dragPointerId = null;
5605
5581
  #dragStartX = 0;
@@ -5609,168 +5585,113 @@ class PropskitWheel extends HTMLElement {
5609
5585
  #elasticMaxPx = 0;
5610
5586
  #elasticRangeRect = null;
5611
5587
  #elasticHostWidth = 0;
5612
- #numberPointerId = null;
5613
- #numberPointerStartX = 0;
5614
- #numberPointerStartY = 0;
5615
- #numberPointerStartValue = 0;
5616
- #isNumberScrubbing = false;
5617
- #suppressNumberClick = false;
5618
- #numberClickResetTimer = 0;
5619
- #boundHandleInput = null;
5620
- #boundHandleChange = null;
5621
5588
  #boundPointerDown = this.#handlePointerDown.bind(this);
5622
5589
  #boundPointerMove = this.#handlePointerMove.bind(this);
5623
5590
  #boundPointerUp = this.#handlePointerUp.bind(this);
5624
- #boundNumberPointerDown = this.#handleNumberPointerDown.bind(this);
5625
- #boundNumberPointerMove = this.#handleNumberPointerMove.bind(this);
5626
- #boundNumberPointerEnd = this.#handleNumberPointerEnd.bind(this);
5627
5591
  #boundWheel = this.#handleWheel.bind(this);
5628
5592
  #boundKeyDown = this.#handleKeyDown.bind(this);
5629
- #boundClick = this.#handleClick.bind(this);
5630
5593
 
5631
5594
  static get observedAttributes() {
5632
- return ["label", "units", "value", "disabled", "step", "precision", "min", "max"];
5595
+ return [
5596
+ "value",
5597
+ "disabled",
5598
+ "step",
5599
+ "min",
5600
+ "max",
5601
+ "elastic",
5602
+ ];
5633
5603
  }
5634
5604
 
5635
5605
  connectedCallback() {
5636
5606
  if (!this.#surface) this.#initialize();
5637
- this.#syncLabel();
5638
5607
  this.#syncDisabled();
5639
- this.#syncInputAttributes();
5640
5608
  this.#syncValueFromHost();
5641
5609
  this.#bindEvents();
5642
- figLabConnectPropskitResetMenu(this);
5643
- this.#observer?.disconnect();
5644
- this.#observer = new MutationObserver((mutations) => {
5645
- let syncInput = false;
5646
- for (const mutation of mutations) {
5647
- if (mutation.type !== "attributes") continue;
5648
- const name = mutation.attributeName;
5649
- if (
5650
- !PropskitWheel.#RESERVED_ATTRS.has(name) &&
5651
- !PropskitWheel.observedAttributes.includes(name) &&
5652
- !name?.startsWith("data-")
5653
- ) {
5654
- syncInput = true;
5655
- }
5656
- }
5657
- if (syncInput) this.#syncInputAttributes();
5658
- });
5659
- this.#observer.observe(this, { attributes: true });
5660
5610
  this.#resizeObserver?.disconnect();
5661
- this.#resizeObserver = new ResizeObserver((entries) => {
5662
- const rect = entries.at(-1)?.contentRect;
5663
- if (rect) this.#setWheelSize(rect.width, rect.height);
5664
- });
5665
- this.#resizeObserver.observe(this.#surface);
5611
+ if (globalThis.ResizeObserver) {
5612
+ this.#resizeObserver = new ResizeObserver((entries) => {
5613
+ const rect = entries.at(-1)?.contentRect;
5614
+ if (rect) this.#setWheelSize(rect.width, rect.height);
5615
+ });
5616
+ this.#resizeObserver.observe(this);
5617
+ }
5666
5618
  this.#queueWheelLayout();
5667
5619
  }
5668
5620
 
5669
5621
  disconnectedCallback() {
5670
- this.#observer?.disconnect();
5671
5622
  this.#resizeObserver?.disconnect();
5672
5623
  if (this.#layoutFrame) cancelAnimationFrame(this.#layoutFrame);
5673
5624
  this.#layoutFrame = 0;
5674
5625
  this.#stopKeyboardAnimation();
5675
5626
  this.#unbindEvents();
5676
5627
  this.#stopDrag();
5677
- figLabDisconnectPropskitResetMenu(this);
5678
5628
  }
5679
5629
 
5680
5630
  attributeChangedCallback(name, oldValue, newValue) {
5681
5631
  if (oldValue === newValue || !this.#surface) return;
5682
- if (name === "label") this.#syncLabel();
5683
5632
  if (name === "disabled") this.#syncDisabled();
5684
5633
  if (name === "value" && !this.#isDragging) {
5685
5634
  this.#syncValueFromHost();
5686
5635
  }
5687
- if (name === "units" || name === "step" || name === "precision") {
5688
- this.#syncInputAttributes();
5636
+ if (name === "step") {
5637
+ this.#syncWheelAria();
5689
5638
  this.#queueWheelLayout();
5690
5639
  }
5691
5640
  if (name === "min" || name === "max") {
5692
- this.#syncInputAttributes();
5693
5641
  this.#commitValue(this.#numericValue());
5694
5642
  }
5695
5643
  }
5696
5644
 
5697
5645
  #initialize() {
5698
- this.#initialValue = this.getAttribute("value") ?? "0";
5699
- const initialChildren = Array.from(this.childNodes).filter(
5700
- (node) =>
5701
- node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
5702
- );
5703
- const customLabel = initialChildren.find(
5704
- (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
5705
- );
5706
-
5707
- const surface = figLabCreateElement("div", {
5708
- className: "propskit-wheel-surface",
5709
- });
5710
- const wheel = figLabCreateElement("div", {
5711
- className: "propskit-wheel-wheel",
5712
- tabindex: "0",
5713
- role: "spinbutton",
5714
- });
5646
+ this.#surface = this;
5647
+ this.#track = this;
5648
+ this.#wheel = this;
5649
+ this.setAttribute("role", "spinbutton");
5650
+ if (
5651
+ !this.hasAttribute("aria-label") &&
5652
+ !this.hasAttribute("aria-labelledby")
5653
+ ) {
5654
+ this.setAttribute("aria-label", "Value");
5655
+ }
5656
+ this.setAttribute("tabindex", "0");
5715
5657
  const svg = figLabCreateSvgElement("svg", {
5716
- className: "propskit-wheel-wheel-svg",
5658
+ className: "fig-input-wheel-svg",
5717
5659
  "aria-hidden": "true",
5718
5660
  });
5719
- const label = customLabel || document.createElement("label");
5720
- const input = document.createElement("fig-input-number");
5721
- const labelId = figLabUniqueId("propskit-wheel-label");
5722
- label.id = labelId;
5723
- wheel.setAttribute("aria-labelledby", labelId);
5724
-
5725
5661
  const handle = figLabCreateElement("div", {
5726
- className: "propskit-wheel-handle",
5662
+ className: "fig-input-wheel-handle",
5727
5663
  });
5728
- wheel.append(svg);
5729
- surface.append(wheel, handle, label, input);
5730
- this.#surface = surface;
5731
- this.#wheel = wheel;
5732
5664
  this.#svg = svg;
5733
- this.#label = label;
5734
- this.#input = input;
5735
- this.#hasCustomLabel = Boolean(customLabel);
5736
- this.replaceChildren(surface);
5665
+ this.replaceChildren(svg, handle);
5737
5666
 
5738
5667
  const metricProbe = document.createElement("div");
5739
5668
  metricProbe.setAttribute("aria-hidden", "true");
5740
5669
  metricProbe.style.cssText =
5741
5670
  "position:absolute;visibility:hidden;pointer-events:none;left:0;top:0";
5742
5671
  const maxEl = document.createElement("div");
5743
- maxEl.style.height = "var(--propskit-wheel-tick-height)";
5672
+ maxEl.style.height = "var(--fig-input-wheel-tick-height)";
5744
5673
  const minEl = document.createElement("div");
5745
- minEl.style.height = "var(--propskit-wheel-tick-height-min)";
5674
+ minEl.style.height = "var(--fig-input-wheel-tick-height-min)";
5746
5675
  const maxWidthEl = document.createElement("div");
5747
- maxWidthEl.style.width = "var(--propskit-wheel-tick-width)";
5676
+ maxWidthEl.style.width = "var(--fig-input-wheel-tick-width)";
5748
5677
  const minWidthEl = document.createElement("div");
5749
- minWidthEl.style.width = "var(--propskit-wheel-tick-width-min)";
5750
- const insetEl = document.createElement("div");
5751
- insetEl.style.width = "var(--propskit-wheel-tick-inset)";
5752
- metricProbe.append(maxEl, minEl, maxWidthEl, minWidthEl, insetEl);
5753
- surface.append(metricProbe);
5678
+ minWidthEl.style.width = "var(--fig-input-wheel-tick-width-min)";
5679
+ metricProbe.append(maxEl, minEl, maxWidthEl, minWidthEl);
5680
+ this.append(metricProbe);
5754
5681
  this.#tickMetricProbe = {
5755
5682
  maxEl,
5756
5683
  minEl,
5757
5684
  maxWidthEl,
5758
5685
  minWidthEl,
5759
- insetEl,
5760
5686
  };
5761
5687
 
5762
- for (const node of initialChildren) {
5763
- if (node === customLabel) continue;
5764
- input.appendChild(node);
5765
- }
5766
-
5767
5688
  this.#ensureTickPath();
5768
5689
  }
5769
5690
 
5770
5691
  #ensureTickPath() {
5771
5692
  if (!this.#svg || this.#tickPath) return;
5772
5693
  this.#tickPath = figLabCreateSvgElement("path", {
5773
- className: "propskit-wheel-tick",
5694
+ className: "fig-input-wheel-tick",
5774
5695
  });
5775
5696
  this.#svg.append(this.#tickPath);
5776
5697
  }
@@ -5784,60 +5705,20 @@ class PropskitWheel extends HTMLElement {
5784
5705
  this.#tickMetricProbe?.maxWidthEl.getBoundingClientRect().width || 2,
5785
5706
  minW:
5786
5707
  this.#tickMetricProbe?.minWidthEl.getBoundingClientRect().width || 1,
5787
- inset: this.#tickMetricProbe?.insetEl.getBoundingClientRect().width || 0,
5788
5708
  };
5789
5709
  return this.#tickMetrics;
5790
5710
  }
5791
5711
 
5792
- #units() {
5793
- const raw = (this.getAttribute("units") || "").trim();
5794
- const normalized = raw.toLowerCase();
5795
- if (
5796
- normalized === "ms" ||
5797
- normalized === "millisecond" ||
5798
- normalized === "milliseconds"
5799
- ) {
5800
- return "ms";
5801
- }
5802
- if (
5803
- normalized === "s" ||
5804
- normalized === "second" ||
5805
- normalized === "seconds"
5806
- ) {
5807
- return "s";
5808
- }
5809
- return raw;
5810
- }
5811
-
5812
- #defaultStep() {
5813
- const units = this.#units();
5814
- if (units === "s") return 0.1;
5815
- if (units === "ms") return 100;
5816
- return 1;
5817
- }
5818
-
5819
- #defaultPrecision() {
5820
- return this.#units() === "s" ? 2 : 0;
5821
- }
5822
-
5823
5712
  #step() {
5824
5713
  if (this.hasAttribute("step")) {
5825
5714
  const parsed = Number(this.getAttribute("step"));
5826
5715
  if (parsed > 0) return parsed;
5827
5716
  }
5828
- return this.#defaultStep();
5829
- }
5830
-
5831
- #precision() {
5832
- if (this.hasAttribute("precision")) {
5833
- const parsed = Number(this.getAttribute("precision"));
5834
- if (Number.isInteger(parsed) && parsed >= 0) return parsed;
5835
- }
5836
- return this.#defaultPrecision();
5717
+ return 1;
5837
5718
  }
5838
5719
 
5839
5720
  #numericValue() {
5840
- const parsed = Number(this.getAttribute("value") ?? this.#input?.value ?? 0);
5721
+ const parsed = Number(this.getAttribute("value") ?? 0);
5841
5722
  return Number.isFinite(parsed) ? parsed : 0;
5842
5723
  }
5843
5724
 
@@ -5894,9 +5775,6 @@ class PropskitWheel extends HTMLElement {
5894
5775
  if (this.getAttribute("value") !== asString) {
5895
5776
  this.setAttribute("value", asString);
5896
5777
  }
5897
- if (this.#input?.getAttribute("value") !== asString) {
5898
- this.#input?.setAttribute("value", asString);
5899
- }
5900
5778
  this.#syncWheelAria(next);
5901
5779
  this.#queueWheelLayout();
5902
5780
  if (emit) {
@@ -5914,53 +5792,35 @@ class PropskitWheel extends HTMLElement {
5914
5792
  #syncValueFromHost() {
5915
5793
  const value = this.#numericValue();
5916
5794
  const clamped = this.#clamp(value);
5917
- if (clamped !== value) {
5795
+ if (
5796
+ clamped !== value ||
5797
+ this.getAttribute("value") !== String(clamped)
5798
+ ) {
5918
5799
  this.#commitValue(clamped);
5919
5800
  return;
5920
5801
  }
5921
- const asString = String(value);
5922
- if (this.#input?.getAttribute("value") !== asString) {
5923
- this.#input?.setAttribute("value", asString);
5924
- }
5925
5802
  this.#syncWheelAria(value);
5926
5803
  this.#queueWheelLayout();
5927
5804
  }
5928
5805
 
5929
5806
  #syncWheelAria(value = this.#numericValue()) {
5930
5807
  if (!this.#wheel) return;
5931
- const units = this.#units();
5932
5808
  const min = this.#boundMin();
5933
5809
  const max = this.#boundMax();
5934
5810
  this.#wheel.setAttribute("aria-valuenow", String(value));
5935
- this.#wheel.setAttribute(
5936
- "aria-valuetext",
5937
- units
5938
- ? `${value} ${
5939
- units === "s" ? "seconds" : units === "ms" ? "milliseconds" : units
5940
- }`
5941
- : String(value),
5942
- );
5811
+ this.#wheel.setAttribute("aria-valuetext", String(value));
5943
5812
  if (min === null) this.#wheel.removeAttribute("aria-valuemin");
5944
5813
  else this.#wheel.setAttribute("aria-valuemin", String(min));
5945
5814
  if (max === null) this.#wheel.removeAttribute("aria-valuemax");
5946
5815
  else this.#wheel.setAttribute("aria-valuemax", String(max));
5947
5816
  }
5948
5817
 
5949
- #syncLabel() {
5950
- if (!this.#label) return;
5951
- if (this.#hasCustomLabel) return;
5952
- if (!this.hasAttribute("label")) {
5953
- this.#label.textContent = "Value";
5954
- return;
5955
- }
5956
- this.#label.textContent = this.getAttribute("label") ?? "";
5957
- }
5958
-
5959
5818
  #syncDisabled() {
5960
5819
  const disabled = figLabBooleanAttribute(this, "disabled");
5961
- this.#input?.toggleAttribute("disabled", disabled);
5962
5820
  if (!this.#wheel) return;
5963
5821
  if (disabled) {
5822
+ if (this.#isDragging) this.#stopDrag();
5823
+ this.#stopKeyboardAnimation();
5964
5824
  this.setAttribute("aria-disabled", "true");
5965
5825
  this.#wheel.setAttribute("tabindex", "-1");
5966
5826
  this.#wheel.setAttribute("aria-disabled", "true");
@@ -5971,39 +5831,6 @@ class PropskitWheel extends HTMLElement {
5971
5831
  }
5972
5832
  }
5973
5833
 
5974
- #getForwardedInputAttrNames() {
5975
- return this.getAttributeNames().filter(
5976
- (name) =>
5977
- !PropskitWheel.#RESERVED_ATTRS.has(name) && !name.startsWith("data-"),
5978
- );
5979
- }
5980
-
5981
- #syncInputAttributes() {
5982
- if (!this.#input) return;
5983
- const inputAttrs = this.#getForwardedInputAttrNames();
5984
- const nextManaged = new Set(inputAttrs);
5985
- for (const attrName of this.#managedInputAttrs) {
5986
- if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
5987
- }
5988
- for (const attrName of inputAttrs) {
5989
- this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
5990
- }
5991
- const units = this.#units();
5992
- if (units) this.#input.setAttribute("units", units);
5993
- else this.#input.removeAttribute("units");
5994
- nextManaged.add("units");
5995
- if (!this.hasAttribute("step")) {
5996
- this.#input.setAttribute("step", String(this.#defaultStep()));
5997
- nextManaged.add("step");
5998
- }
5999
- if (!this.hasAttribute("precision")) {
6000
- this.#input.setAttribute("precision", String(this.#defaultPrecision()));
6001
- nextManaged.add("precision");
6002
- }
6003
- this.#managedInputAttrs = nextManaged;
6004
- this.#syncWheelAria();
6005
- }
6006
-
6007
5834
  #setWheelSize(width, height) {
6008
5835
  if (!this.#svg || width < 1 || height < 1) return;
6009
5836
  if (width === this.#wheelWidth && height === this.#wheelHeight) return;
@@ -6039,22 +5866,21 @@ class PropskitWheel extends HTMLElement {
6039
5866
  if (width < 1 || height < 1) return;
6040
5867
 
6041
5868
  const value = this.#visualValue ?? this.#numericValue();
6042
- const tickStep = 360 / PropskitWheel.#TICK_COUNT;
5869
+ const tickStep = 360 / FigInputWheel.#TICK_COUNT;
6043
5870
  const step = this.#step();
6044
5871
  const base = this.#boundMin() ?? 0;
6045
5872
  const offsetDeg = ((value - base) / step) * tickStep;
6046
- const k = PropskitWheel.#PERSPECTIVE_K;
5873
+ const k = FigInputWheel.#PERSPECTIVE_K;
6047
5874
  const cx = width / 2;
6048
5875
  const cy = height / 2;
6049
5876
 
6050
- const { maxH, minH, maxW, minW, inset } = this.#readTickMetrics();
6051
- const drawableWidth = Math.max(0, width - inset * 2);
6052
- const radius = drawableWidth * 0.42;
6053
- const fov = PropskitWheel.#HALF_FOV_DEG;
6054
- const visibleHalf = Math.max(drawableWidth / 2, 1);
5877
+ const { maxH, minH, maxW, minW } = this.#readTickMetrics();
5878
+ const fov = FigInputWheel.#HALF_FOV_DEG;
5879
+ const visibleHalf = Math.max(width / 2, 1);
5880
+ const radius = visibleHalf * 0.84;
6055
5881
  const commands = [];
6056
5882
 
6057
- for (let index = 0; index < PropskitWheel.#TICK_COUNT; index += 1) {
5883
+ for (let index = 0; index < FigInputWheel.#TICK_COUNT; index += 1) {
6058
5884
  let theta = offsetDeg + index * tickStep;
6059
5885
  theta = ((((theta + 180) % 360) + 360) % 360) - 180;
6060
5886
  if (theta < -fov || theta >= fov) continue;
@@ -6084,106 +5910,93 @@ class PropskitWheel extends HTMLElement {
6084
5910
  }
6085
5911
 
6086
5912
  #bindEvents() {
6087
- this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
6088
- this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
6089
- this.#input?.addEventListener("input", this.#boundHandleInput);
6090
- this.#input?.addEventListener("change", this.#boundHandleChange);
6091
5913
  this.#surface?.addEventListener("pointerdown", this.#boundPointerDown);
6092
- this.addEventListener("pointerdown", this.#boundNumberPointerDown, {
6093
- capture: true,
6094
- });
6095
5914
  this.#wheel?.addEventListener("wheel", this.#boundWheel, { passive: false });
6096
5915
  this.#wheel?.addEventListener("keydown", this.#boundKeyDown);
6097
- this.addEventListener("click", this.#boundClick, true);
6098
5916
  }
6099
5917
 
6100
5918
  #unbindEvents() {
6101
- this.#input?.removeEventListener("input", this.#boundHandleInput);
6102
- this.#input?.removeEventListener("change", this.#boundHandleChange);
6103
5919
  this.#surface?.removeEventListener("pointerdown", this.#boundPointerDown);
6104
- this.removeEventListener("pointerdown", this.#boundNumberPointerDown, {
6105
- capture: true,
6106
- });
6107
5920
  this.#wheel?.removeEventListener("wheel", this.#boundWheel);
6108
5921
  this.#wheel?.removeEventListener("keydown", this.#boundKeyDown);
6109
- this.removeEventListener("click", this.#boundClick, true);
6110
- this.#stopNumberTracking();
6111
- clearTimeout(this.#numberClickResetTimer);
6112
- this.#numberClickResetTimer = 0;
6113
5922
  window.removeEventListener("pointermove", this.#boundPointerMove);
6114
5923
  window.removeEventListener("pointerup", this.#boundPointerUp);
6115
5924
  window.removeEventListener("pointercancel", this.#boundPointerUp);
6116
5925
  }
6117
5926
 
6118
- #forwardInputEvent(type, event) {
6119
- event.stopImmediatePropagation();
6120
- if (figLabBooleanAttribute(this, "disabled")) return;
6121
- const raw =
6122
- event instanceof CustomEvent && event.detail !== undefined
6123
- ? event.detail
6124
- : this.#input?.value;
6125
- const parsed = Number(raw);
6126
- const next = Number.isFinite(parsed) ? parsed : this.#numericValue();
6127
- this.#commitValue(next, { emit: type });
6128
- }
6129
-
6130
- #handleClick(event) {
6131
- if (figLabBooleanAttribute(this, "disabled")) return;
6132
- if (
6133
- event.target instanceof Element &&
6134
- event.target.closest("fig-input-number, fig-menu")
6135
- ) {
6136
- if (
6137
- this.#suppressNumberClick &&
6138
- event.target.closest("fig-input-number")
6139
- ) {
6140
- event.preventDefault();
6141
- event.stopImmediatePropagation();
6142
- this.#suppressNumberClick = false;
6143
- }
6144
- return;
6145
- }
6146
- this.#wheel?.focus();
6147
- }
6148
-
6149
5927
  #handlePointerDown(event) {
6150
5928
  if (figLabBooleanAttribute(this, "disabled")) return;
6151
5929
  if (event.button !== 0) return;
6152
- if (
6153
- event.target instanceof Element &&
6154
- event.target.closest("fig-input-number, fig-menu")
6155
- ) {
6156
- return;
6157
- }
6158
5930
  event.preventDefault();
6159
- this.#stopKeyboardAnimation();
6160
- this.#isDragging = true;
6161
- this.#dragPointerId = event.pointerId;
6162
- this.#dragStartX = event.clientX;
6163
- this.#dragStartValue = this.#numericValue();
6164
- this.#startElasticPull();
6165
- this.setAttribute("data-active", "");
6166
- document.body.classList.add(PropskitWheel.#DRAGGING_BODY_CLASS);
6167
- this.#wheel?.focus();
5931
+ if (!this.beginScrub(event)) return;
6168
5932
  this.#surface?.setPointerCapture?.(event.pointerId);
6169
5933
  window.addEventListener("pointermove", this.#boundPointerMove);
6170
5934
  window.addEventListener("pointerup", this.#boundPointerUp);
6171
5935
  window.addEventListener("pointercancel", this.#boundPointerUp);
6172
5936
  }
6173
5937
 
5938
+ beginScrub(start = {}) {
5939
+ if (figLabBooleanAttribute(this, "disabled")) return false;
5940
+ const clientX =
5941
+ typeof start === "number" ? start : Number(start?.clientX ?? start?.x);
5942
+ if (!Number.isFinite(clientX)) return false;
5943
+ this.#stopKeyboardAnimation();
5944
+ this.#isDragging = true;
5945
+ this.#dragPointerId =
5946
+ start && typeof start === "object" && start.pointerId !== undefined
5947
+ ? start.pointerId
5948
+ : null;
5949
+ this.#dragStartX = clientX;
5950
+ const requestedStart =
5951
+ start && typeof start === "object"
5952
+ ? Number(start.startValue)
5953
+ : Number.NaN;
5954
+ this.#dragStartValue = Number.isFinite(requestedStart)
5955
+ ? this.#clamp(requestedStart)
5956
+ : this.#numericValue();
5957
+ this.#startElasticPull();
5958
+ this.setAttribute("data-fig-input-wheel-active", "");
5959
+ document.body.classList.add(FigInputWheel.#DRAGGING_BODY_CLASS);
5960
+ this.focus();
5961
+ return true;
5962
+ }
5963
+
5964
+ updateScrub(position, speed) {
5965
+ if (!this.#isDragging) return this.#numericValue();
5966
+ const clientX =
5967
+ typeof position === "number"
5968
+ ? position
5969
+ : Number(position?.clientX ?? position?.x);
5970
+ if (!Number.isFinite(clientX)) return this.#numericValue();
5971
+ const multiplier =
5972
+ speed ??
5973
+ (typeof position === "object" && position?.shiftKey ? 10 : 1);
5974
+ this.#updateDrag(clientX, multiplier);
5975
+ return this.#numericValue();
5976
+ }
5977
+
5978
+ endScrub(commit = true) {
5979
+ if (!this.#isDragging) return this.#numericValue();
5980
+ if (commit && this.#numericValue() !== this.#dragStartValue) {
5981
+ this.#commitValue(this.#numericValue(), { snap: true, emit: "change" });
5982
+ }
5983
+ this.#stopDrag();
5984
+ return this.#numericValue();
5985
+ }
5986
+
6174
5987
  #handlePointerMove(event) {
6175
5988
  if (!this.#isDragging) return;
6176
5989
  if (this.#dragPointerId !== null && event.pointerId !== this.#dragPointerId) {
6177
5990
  return;
6178
5991
  }
6179
- this.#updateDrag(event.clientX, event.shiftKey ? 10 : 1);
5992
+ this.updateScrub(event);
6180
5993
  }
6181
5994
 
6182
5995
  #updateDrag(clientX, speed = 1) {
6183
5996
  const width = this.#wheel?.clientWidth || 1;
6184
5997
  const visibleSteps =
6185
- PropskitWheel.#TICK_COUNT *
6186
- ((PropskitWheel.#HALF_FOV_DEG * 2) / 360);
5998
+ FigInputWheel.#TICK_COUNT *
5999
+ ((FigInputWheel.#HALF_FOV_DEG * 2) / 360);
6187
6000
  const visibleUnits = this.#step() * visibleSteps;
6188
6001
  const delta =
6189
6002
  (clientX - this.#dragStartX) * (visibleUnits / width) * speed;
@@ -6201,18 +6014,15 @@ class PropskitWheel extends HTMLElement {
6201
6014
  if (this.#dragPointerId !== null && event.pointerId !== this.#dragPointerId) {
6202
6015
  return;
6203
6016
  }
6204
- if (this.#numericValue() !== this.#dragStartValue) {
6205
- this.#commitValue(this.#numericValue(), { snap: true, emit: "change" });
6206
- }
6207
- this.#stopDrag();
6017
+ this.endScrub();
6208
6018
  }
6209
6019
 
6210
6020
  #stopDrag() {
6211
6021
  this.#isDragging = false;
6212
6022
  this.#dragPointerId = null;
6213
6023
  this.#visualValue = null;
6214
- this.removeAttribute("data-active");
6215
- document.body.classList.remove(PropskitWheel.#DRAGGING_BODY_CLASS);
6024
+ this.removeAttribute("data-fig-input-wheel-active");
6025
+ document.body.classList.remove(FigInputWheel.#DRAGGING_BODY_CLASS);
6216
6026
  this.#resetElasticPull();
6217
6027
  window.removeEventListener("pointermove", this.#boundPointerMove);
6218
6028
  window.removeEventListener("pointerup", this.#boundPointerUp);
@@ -6220,107 +6030,14 @@ class PropskitWheel extends HTMLElement {
6220
6030
  this.#queueWheelLayout();
6221
6031
  }
6222
6032
 
6223
- #handleNumberPointerDown(event) {
6224
- if (
6225
- event.button !== 0 ||
6226
- event.altKey ||
6227
- figLabBooleanAttribute(this, "disabled") ||
6228
- !(event.target instanceof Element)
6229
- ) {
6230
- return;
6231
- }
6232
- const input = event.target.closest("fig-input-number input");
6233
- if (!input || input === document.activeElement) return;
6234
-
6235
- event.preventDefault();
6236
- event.stopImmediatePropagation();
6237
- this.#stopNumberTracking();
6238
- this.#numberPointerId = event.pointerId;
6239
- this.#numberPointerStartX = event.clientX;
6240
- this.#numberPointerStartY = event.clientY;
6241
- this.#numberPointerStartValue = this.#numericValue();
6242
- window.addEventListener("pointermove", this.#boundNumberPointerMove);
6243
- window.addEventListener("pointerup", this.#boundNumberPointerEnd, {
6244
- once: true,
6245
- });
6246
- window.addEventListener("pointercancel", this.#boundNumberPointerEnd, {
6247
- once: true,
6248
- });
6249
- window.addEventListener("blur", this.#boundNumberPointerEnd, {
6250
- once: true,
6251
- });
6252
- }
6253
-
6254
- #handleNumberPointerMove(event) {
6255
- if (event.pointerId !== this.#numberPointerId) return;
6256
- if (event.buttons === 0) {
6257
- this.#handleNumberPointerEnd(event);
6258
- return;
6259
- }
6260
- if (!this.#isNumberScrubbing) {
6261
- const distance = Math.hypot(
6262
- event.clientX - this.#numberPointerStartX,
6263
- event.clientY - this.#numberPointerStartY,
6264
- );
6265
- if (distance < 4) return;
6266
- this.#stopKeyboardAnimation();
6267
- this.#isNumberScrubbing = true;
6268
- this.setAttribute("data-number-scrubbing", "");
6269
- this.#isDragging = true;
6270
- this.#dragPointerId = event.pointerId;
6271
- this.#dragStartX = this.#numberPointerStartX;
6272
- this.#dragStartValue = this.#numberPointerStartValue;
6273
- this.#startElasticPull();
6274
- this.setAttribute("data-active", "");
6275
- document.body.classList.add(PropskitWheel.#DRAGGING_BODY_CLASS);
6276
- this.#wheel?.focus();
6277
- }
6278
- this.#updateDrag(event.clientX, event.shiftKey ? 10 : 1);
6279
- }
6280
-
6281
- #handleNumberPointerEnd(event) {
6282
- if (
6283
- event?.pointerId !== undefined &&
6284
- this.#numberPointerId !== null &&
6285
- event.pointerId !== this.#numberPointerId
6286
- ) {
6287
- return;
6288
- }
6289
- const wasScrubbing = this.#isNumberScrubbing;
6290
- this.#stopNumberTracking();
6291
- if (wasScrubbing) {
6292
- this.#commitValue(this.#numericValue(), { snap: true, emit: "change" });
6293
- this.#stopDrag();
6294
- this.#suppressNumberClick = true;
6295
- clearTimeout(this.#numberClickResetTimer);
6296
- this.#numberClickResetTimer = window.setTimeout(() => {
6297
- this.#suppressNumberClick = false;
6298
- this.#numberClickResetTimer = 0;
6299
- }, 0);
6300
- requestAnimationFrame(() => this.#wheel?.focus());
6301
- return;
6302
- }
6303
- this.#input?.querySelector("input")?.focus();
6304
- }
6305
-
6306
- #stopNumberTracking() {
6307
- window.removeEventListener("pointermove", this.#boundNumberPointerMove);
6308
- window.removeEventListener("pointerup", this.#boundNumberPointerEnd);
6309
- window.removeEventListener("pointercancel", this.#boundNumberPointerEnd);
6310
- window.removeEventListener("blur", this.#boundNumberPointerEnd);
6311
- this.#numberPointerId = null;
6312
- this.#isNumberScrubbing = false;
6313
- this.removeAttribute("data-number-scrubbing");
6314
- }
6315
-
6316
6033
  #startElasticPull() {
6317
6034
  this.#resetElasticPull();
6318
6035
  if (this.getAttribute("elastic") === "false") return;
6319
6036
  this.#handleDragMaxPx = this.#readCssLength(
6320
- "--propskit-wheel-handle-drag-max",
6037
+ "--fig-input-wheel-handle-drag-max",
6321
6038
  );
6322
6039
  this.#elasticMaxPx = this.#readCssLength(
6323
- "--propskit-wheel-elastic-distance",
6040
+ "--fig-input-wheel-elastic-distance",
6324
6041
  );
6325
6042
  const rect = this.#wheel?.getBoundingClientRect();
6326
6043
  if (rect) {
@@ -6367,9 +6084,9 @@ class PropskitWheel extends HTMLElement {
6367
6084
  Math.tanh(
6368
6085
  dragDelta / Math.max(1, this.#handleDragMaxPx * 3),
6369
6086
  );
6370
- this.dataset.elasticDragging = "true";
6087
+ this.setAttribute("data-fig-input-wheel-elastic-dragging", "");
6371
6088
  this.style.setProperty(
6372
- "--propskit-wheel-handle-drag-offset",
6089
+ "--fig-input-wheel-handle-drag-offset",
6373
6090
  `${offset}px`,
6374
6091
  );
6375
6092
 
@@ -6382,17 +6099,17 @@ class PropskitWheel extends HTMLElement {
6382
6099
  : 0
6383
6100
  : 0;
6384
6101
  if (!overshoot || !this.#elasticMaxPx) {
6385
- this.style.removeProperty("--propskit-wheel-elastic-scale");
6386
- this.style.removeProperty("--propskit-wheel-elastic-origin");
6102
+ this.style.removeProperty("--fig-input-wheel-elastic-scale");
6103
+ this.style.removeProperty("--fig-input-wheel-elastic-origin");
6387
6104
  return;
6388
6105
  }
6389
6106
  const stretch = Math.min(this.#elasticMaxPx, Math.abs(overshoot) * 0.5);
6390
6107
  const scale = this.#elasticHostWidth
6391
6108
  ? (this.#elasticHostWidth + stretch) / this.#elasticHostWidth
6392
6109
  : 1;
6393
- this.style.setProperty("--propskit-wheel-elastic-scale", `${scale}`);
6110
+ this.style.setProperty("--fig-input-wheel-elastic-scale", `${scale}`);
6394
6111
  this.style.setProperty(
6395
- "--propskit-wheel-elastic-origin",
6112
+ "--fig-input-wheel-elastic-origin",
6396
6113
  overshoot < 0 ? "right center" : "left center",
6397
6114
  );
6398
6115
  }
@@ -6406,10 +6123,10 @@ class PropskitWheel extends HTMLElement {
6406
6123
  }
6407
6124
 
6408
6125
  #clearElasticPull() {
6409
- this.removeAttribute("data-elastic-dragging");
6410
- this.style.removeProperty("--propskit-wheel-handle-drag-offset");
6411
- this.style.removeProperty("--propskit-wheel-elastic-scale");
6412
- this.style.removeProperty("--propskit-wheel-elastic-origin");
6126
+ this.removeAttribute("data-fig-input-wheel-elastic-dragging");
6127
+ this.style.removeProperty("--fig-input-wheel-handle-drag-offset");
6128
+ this.style.removeProperty("--fig-input-wheel-elastic-scale");
6129
+ this.style.removeProperty("--fig-input-wheel-elastic-origin");
6413
6130
  }
6414
6131
 
6415
6132
  #handleWheel(event) {
@@ -6484,16 +6201,16 @@ class PropskitWheel extends HTMLElement {
6484
6201
  }
6485
6202
  clearTimeout(this.#keyboardSettleTimer);
6486
6203
  this.#keyboardSettleTimer = 0;
6487
- this.removeAttribute("data-keyboard-settling");
6204
+ this.removeAttribute("data-fig-input-wheel-keyboard-settling");
6488
6205
  const visualFrom = this.#visualValue ?? from;
6489
6206
  const handleFrom =
6490
6207
  Number.parseFloat(
6491
- this.style.getPropertyValue("--propskit-wheel-handle-drag-offset"),
6208
+ this.style.getPropertyValue("--fig-input-wheel-handle-drag-offset"),
6492
6209
  ) || 0;
6493
6210
  const duration = multiplier > 1 ? 120 : 90;
6494
6211
  const startedAt = performance.now();
6495
6212
  const animateHandle = this.getAttribute("elastic") !== "false";
6496
- this.setAttribute("data-keyboard-moving", "");
6213
+ this.setAttribute("data-fig-input-wheel-keyboard-moving", "");
6497
6214
 
6498
6215
  const frame = (now) => {
6499
6216
  const progress = Math.min(1, (now - startedAt) / duration);
@@ -6502,7 +6219,7 @@ class PropskitWheel extends HTMLElement {
6502
6219
  const eased = Math.sin((progress * Math.PI) / 2);
6503
6220
  const offset = handleFrom + (direction * 4 - handleFrom) * eased;
6504
6221
  this.style.setProperty(
6505
- "--propskit-wheel-handle-drag-offset",
6222
+ "--fig-input-wheel-handle-drag-offset",
6506
6223
  `${offset}px`,
6507
6224
  );
6508
6225
  }
@@ -6515,11 +6232,11 @@ class PropskitWheel extends HTMLElement {
6515
6232
 
6516
6233
  this.#keyboardAnimationFrame = 0;
6517
6234
  this.#visualValue = null;
6518
- this.removeAttribute("data-keyboard-moving");
6519
- this.setAttribute("data-keyboard-settling", "");
6520
- this.style.removeProperty("--propskit-wheel-handle-drag-offset");
6235
+ this.removeAttribute("data-fig-input-wheel-keyboard-moving");
6236
+ this.setAttribute("data-fig-input-wheel-keyboard-settling", "");
6237
+ this.style.removeProperty("--fig-input-wheel-handle-drag-offset");
6521
6238
  this.#keyboardSettleTimer = window.setTimeout(() => {
6522
- this.removeAttribute("data-keyboard-settling");
6239
+ this.removeAttribute("data-fig-input-wheel-keyboard-settling");
6523
6240
  this.#keyboardSettleTimer = 0;
6524
6241
  }, 140);
6525
6242
  this.#queueWheelLayout();
@@ -6536,9 +6253,9 @@ class PropskitWheel extends HTMLElement {
6536
6253
  clearTimeout(this.#keyboardSettleTimer);
6537
6254
  this.#keyboardSettleTimer = 0;
6538
6255
  this.#visualValue = null;
6539
- this.removeAttribute("data-keyboard-moving");
6540
- this.removeAttribute("data-keyboard-settling");
6541
- this.style.removeProperty("--propskit-wheel-handle-drag-offset");
6256
+ this.removeAttribute("data-fig-input-wheel-keyboard-moving");
6257
+ this.removeAttribute("data-fig-input-wheel-keyboard-settling");
6258
+ this.style.removeProperty("--fig-input-wheel-handle-drag-offset");
6542
6259
  if (this.isConnected) this.#queueWheelLayout();
6543
6260
  }
6544
6261
 
@@ -6571,6 +6288,573 @@ class PropskitWheel extends HTMLElement {
6571
6288
  this.#writeBound("max", nextValue);
6572
6289
  }
6573
6290
 
6291
+ get step() {
6292
+ return this.#step();
6293
+ }
6294
+
6295
+ focus(options) {
6296
+ HTMLElement.prototype.focus.call(this, options);
6297
+ }
6298
+ }
6299
+ figLabDefineElement("fig-input-wheel", FigInputWheel);
6300
+
6301
+ /**
6302
+ * Labeled numeric wheel with an optional editable number field.
6303
+ *
6304
+ * @attr {string} label - Field label. Defaults to "Value".
6305
+ * @attr {string} default - Reset target.
6306
+ * @attr {number} precision - Number field display decimals.
6307
+ * @attr {boolean|string} text - Shows the number field. Defaults to true.
6308
+ * @attr {string} size - Set to "small" for compact sizing.
6309
+ * @attr {string} variant - Set to "minimal" for compact chrome.
6310
+ * @fires input - Retargeted numeric composed event.
6311
+ * @fires change - Retargeted numeric composed event.
6312
+ */
6313
+ class PropskitWheel extends HTMLElement {
6314
+ static #RESERVED_ATTRS = new Set([
6315
+ "label",
6316
+ "size",
6317
+ "disabled",
6318
+ "variant",
6319
+ "elastic",
6320
+ "text",
6321
+ "default",
6322
+ "class",
6323
+ "style",
6324
+ "id",
6325
+ "oninput",
6326
+ "onchange",
6327
+ ]);
6328
+
6329
+ static get observedAttributes() {
6330
+ return [
6331
+ "label",
6332
+ "units",
6333
+ "value",
6334
+ "disabled",
6335
+ "step",
6336
+ "precision",
6337
+ "min",
6338
+ "max",
6339
+ "elastic",
6340
+ "text",
6341
+ ];
6342
+ }
6343
+
6344
+ #surface = null;
6345
+ #wheel = null;
6346
+ #label = null;
6347
+ #input = null;
6348
+ #hasCustomLabel = false;
6349
+ #observer = null;
6350
+ #elasticObserver = null;
6351
+ #managedInputAttrs = new Set();
6352
+ #initialValue = null;
6353
+ #numberPointerId = null;
6354
+ #numberPointerStartX = 0;
6355
+ #numberPointerStartY = 0;
6356
+ #numberPointerStartValue = 0;
6357
+ #isNumberScrubbing = false;
6358
+ #suppressNumberClick = false;
6359
+ #numberClickResetTimer = 0;
6360
+ #boundPrimitiveInput = this.#handlePrimitiveEvent.bind(this, "input");
6361
+ #boundPrimitiveChange = this.#handlePrimitiveEvent.bind(this, "change");
6362
+ #boundNumberInput = this.#handleNumberEvent.bind(this, "input");
6363
+ #boundNumberChange = this.#handleNumberEvent.bind(this, "change");
6364
+ #boundNumberPointerDown = this.#handleNumberPointerDown.bind(this);
6365
+ #boundNumberPointerMove = this.#handleNumberPointerMove.bind(this);
6366
+ #boundNumberPointerEnd = this.#handleNumberPointerEnd.bind(this);
6367
+ #boundClick = this.#handleClick.bind(this);
6368
+
6369
+ connectedCallback() {
6370
+ if (!this.#surface) this.#initialize();
6371
+ this.#syncLabel();
6372
+ this.#syncText();
6373
+ this.#syncPrimitive();
6374
+ this.#syncInputAttributes();
6375
+ this.#bindEvents();
6376
+ figLabConnectPropskitResetMenu(this);
6377
+ this.#observer?.disconnect();
6378
+ this.#observer = new MutationObserver((mutations) => {
6379
+ if (
6380
+ mutations.some(
6381
+ ({ type, attributeName }) =>
6382
+ type === "attributes" &&
6383
+ attributeName &&
6384
+ !PropskitWheel.#RESERVED_ATTRS.has(attributeName) &&
6385
+ !PropskitWheel.observedAttributes.includes(attributeName) &&
6386
+ !attributeName.startsWith("data-"),
6387
+ )
6388
+ ) {
6389
+ this.#syncInputAttributes();
6390
+ }
6391
+ });
6392
+ this.#observer.observe(this, { attributes: true });
6393
+ this.#elasticObserver?.disconnect();
6394
+ this.#elasticObserver = new MutationObserver(() => {
6395
+ this.#syncElasticComposition();
6396
+ });
6397
+ if (this.#wheel) {
6398
+ this.#elasticObserver.observe(this.#wheel, {
6399
+ attributes: true,
6400
+ attributeFilter: ["style", "data-fig-input-wheel-elastic-dragging"],
6401
+ });
6402
+ }
6403
+ this.#syncElasticComposition();
6404
+ }
6405
+
6406
+ disconnectedCallback() {
6407
+ this.#observer?.disconnect();
6408
+ this.#elasticObserver?.disconnect();
6409
+ this.#elasticObserver = null;
6410
+ this.#clearElasticComposition();
6411
+ this.#unbindEvents();
6412
+ this.#stopNumberTracking();
6413
+ clearTimeout(this.#numberClickResetTimer);
6414
+ this.#numberClickResetTimer = 0;
6415
+ this.#wheel?.endScrub(false);
6416
+ figLabDisconnectPropskitResetMenu(this);
6417
+ }
6418
+
6419
+ attributeChangedCallback(name, oldValue, newValue) {
6420
+ if (oldValue === newValue || !this.#surface) return;
6421
+ if (name === "label") this.#syncLabel();
6422
+ if (name === "text") this.#syncText();
6423
+ if (
6424
+ ["units", "value", "disabled", "step", "min", "max", "elastic"].includes(
6425
+ name,
6426
+ )
6427
+ ) {
6428
+ this.#syncPrimitive();
6429
+ }
6430
+ if (
6431
+ ["units", "value", "disabled", "step", "precision", "min", "max"].includes(
6432
+ name,
6433
+ )
6434
+ ) {
6435
+ this.#syncInputAttributes();
6436
+ }
6437
+ }
6438
+
6439
+ #initialize() {
6440
+ this.#initialValue = this.getAttribute("value") ?? "0";
6441
+ const initialChildren = Array.from(this.childNodes).filter(
6442
+ (node) =>
6443
+ node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
6444
+ );
6445
+ const customLabel = initialChildren.find(
6446
+ (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
6447
+ );
6448
+ const surface = figLabCreateElement("div", {
6449
+ className: "propskit-wheel-surface",
6450
+ });
6451
+ const label = customLabel || document.createElement("label");
6452
+ const wheel = document.createElement("fig-input-wheel");
6453
+ const input = document.createElement("fig-input-number");
6454
+ const labelId = figLabUniqueId("propskit-wheel-label");
6455
+ label.id = labelId;
6456
+ wheel.setAttribute("aria-labelledby", labelId);
6457
+ input.setAttribute("aria-labelledby", labelId);
6458
+ surface.append(label, wheel, input);
6459
+ this.#surface = surface;
6460
+ this.#wheel = wheel;
6461
+ this.#label = label;
6462
+ this.#input = input;
6463
+ this.#hasCustomLabel = Boolean(customLabel);
6464
+ this.replaceChildren(surface);
6465
+ for (const node of initialChildren) {
6466
+ if (node !== customLabel) input.appendChild(node);
6467
+ }
6468
+ }
6469
+
6470
+ #syncLabel() {
6471
+ if (!this.#label) return;
6472
+ if (!this.#hasCustomLabel) {
6473
+ this.#label.textContent = this.hasAttribute("label")
6474
+ ? (this.getAttribute("label") ?? "")
6475
+ : "Value";
6476
+ }
6477
+ this.toggleAttribute(
6478
+ "data-label-empty",
6479
+ !(this.#label.textContent ?? "").trim(),
6480
+ );
6481
+ }
6482
+
6483
+ #syncText() {
6484
+ if (!this.#surface || !this.#input) return;
6485
+ const enabled = this.getAttribute("text") !== "false";
6486
+ const isInserted = this.#input.parentElement === this.#surface;
6487
+ if (enabled && !isInserted) this.#surface.append(this.#input);
6488
+ else if (!enabled && isInserted) this.#input.remove();
6489
+ }
6490
+
6491
+ #mirrorAttribute(name) {
6492
+ if (!this.#wheel) return;
6493
+ if (this.hasAttribute(name)) {
6494
+ this.#wheel.setAttribute(name, this.getAttribute(name) ?? "");
6495
+ } else {
6496
+ this.#wheel.removeAttribute(name);
6497
+ }
6498
+ }
6499
+
6500
+ #syncPrimitive() {
6501
+ if (!this.#wheel) return;
6502
+ this.#wheel.removeAttribute("units");
6503
+ for (const name of ["min", "max", "elastic"]) {
6504
+ this.#mirrorAttribute(name);
6505
+ }
6506
+ if (this.hasAttribute("step")) this.#mirrorAttribute("step");
6507
+ else this.#wheel.setAttribute("step", String(this.#defaultStep()));
6508
+ this.#wheel.toggleAttribute(
6509
+ "disabled",
6510
+ figLabBooleanAttribute(this, "disabled"),
6511
+ );
6512
+ this.#wheel.value = this.getAttribute("value") ?? "0";
6513
+ const normalized = this.#wheel.value;
6514
+ if (this.getAttribute("value") !== normalized) {
6515
+ this.setAttribute("value", normalized);
6516
+ }
6517
+ if (this.#input?.getAttribute("value") !== normalized) {
6518
+ this.#input?.setAttribute("value", normalized);
6519
+ }
6520
+ this.#syncPrimitiveAria();
6521
+ }
6522
+
6523
+ #syncElasticComposition() {
6524
+ if (!this.#wheel) return;
6525
+ const active = this.#wheel.hasAttribute(
6526
+ "data-fig-input-wheel-elastic-dragging",
6527
+ );
6528
+ if (!active) {
6529
+ this.#clearElasticComposition();
6530
+ return;
6531
+ }
6532
+ this.toggleAttribute("data-propskit-wheel-elastic-dragging", true);
6533
+ this.style.setProperty(
6534
+ "--propskit-wheel-elastic-scale",
6535
+ this.#wheel.style.getPropertyValue("--fig-input-wheel-elastic-scale") ||
6536
+ "1",
6537
+ );
6538
+ this.style.setProperty(
6539
+ "--propskit-wheel-elastic-origin",
6540
+ this.#wheel.style.getPropertyValue("--fig-input-wheel-elastic-origin") ||
6541
+ "left center",
6542
+ );
6543
+ }
6544
+
6545
+ #clearElasticComposition() {
6546
+ this.removeAttribute("data-propskit-wheel-elastic-dragging");
6547
+ this.style.removeProperty("--propskit-wheel-elastic-scale");
6548
+ this.style.removeProperty("--propskit-wheel-elastic-origin");
6549
+ }
6550
+
6551
+ #getForwardedInputAttrNames() {
6552
+ return this.getAttributeNames().filter(
6553
+ (name) =>
6554
+ !PropskitWheel.#RESERVED_ATTRS.has(name) &&
6555
+ !PropskitWheel.observedAttributes.includes(name) &&
6556
+ !name.startsWith("data-"),
6557
+ );
6558
+ }
6559
+
6560
+ #units() {
6561
+ const raw = (this.getAttribute("units") || "").trim();
6562
+ const normalized = raw.toLowerCase();
6563
+ if (
6564
+ normalized === "ms" ||
6565
+ normalized === "millisecond" ||
6566
+ normalized === "milliseconds"
6567
+ ) {
6568
+ return "ms";
6569
+ }
6570
+ if (
6571
+ normalized === "s" ||
6572
+ normalized === "second" ||
6573
+ normalized === "seconds"
6574
+ ) {
6575
+ return "s";
6576
+ }
6577
+ return raw;
6578
+ }
6579
+
6580
+ #defaultStep() {
6581
+ const units = this.#units();
6582
+ if (units === "s") return 0.1;
6583
+ if (units === "ms") return 100;
6584
+ return 1;
6585
+ }
6586
+
6587
+ #defaultPrecision() {
6588
+ return this.#units() === "s" ? 2 : 0;
6589
+ }
6590
+
6591
+ #syncPrimitiveAria() {
6592
+ if (!this.#wheel) return;
6593
+ const value = Number(this.#wheel.value);
6594
+ const units = this.#units();
6595
+ this.#wheel.setAttribute(
6596
+ "aria-valuetext",
6597
+ units
6598
+ ? `${value} ${
6599
+ units === "s" ? "seconds" : units === "ms" ? "milliseconds" : units
6600
+ }`
6601
+ : String(value),
6602
+ );
6603
+ }
6604
+
6605
+ #syncInputAttributes() {
6606
+ if (!this.#input || !this.#wheel) return;
6607
+ const forwarded = this.#getForwardedInputAttrNames();
6608
+ const nextManaged = new Set(forwarded);
6609
+ for (const name of this.#managedInputAttrs) {
6610
+ if (!nextManaged.has(name)) this.#input.removeAttribute(name);
6611
+ }
6612
+ for (const name of forwarded) {
6613
+ this.#input.setAttribute(name, this.getAttribute(name) ?? "");
6614
+ }
6615
+ const units = this.#units();
6616
+ if (units) this.#input.setAttribute("units", units);
6617
+ else this.#input.removeAttribute("units");
6618
+ this.#input.setAttribute("step", String(this.#wheel.step));
6619
+ if (this.hasAttribute("precision")) {
6620
+ this.#input.setAttribute(
6621
+ "precision",
6622
+ this.getAttribute("precision") ?? "",
6623
+ );
6624
+ } else {
6625
+ this.#input.setAttribute("precision", String(this.#defaultPrecision()));
6626
+ }
6627
+ for (const name of ["min", "max"]) {
6628
+ const value = this.#wheel[name];
6629
+ if (value === null) this.#input.removeAttribute(name);
6630
+ else this.#input.setAttribute(name, String(value));
6631
+ }
6632
+ this.#input.toggleAttribute(
6633
+ "disabled",
6634
+ figLabBooleanAttribute(this, "disabled"),
6635
+ );
6636
+ this.#input.setAttribute("value", this.#wheel.value);
6637
+ this.#managedInputAttrs = nextManaged;
6638
+ }
6639
+
6640
+ #bindEvents() {
6641
+ this.#unbindEvents();
6642
+ this.#wheel?.addEventListener("input", this.#boundPrimitiveInput);
6643
+ this.#wheel?.addEventListener("change", this.#boundPrimitiveChange);
6644
+ this.#input?.addEventListener("input", this.#boundNumberInput);
6645
+ this.#input?.addEventListener("change", this.#boundNumberChange);
6646
+ this.addEventListener("pointerdown", this.#boundNumberPointerDown, {
6647
+ capture: true,
6648
+ });
6649
+ this.addEventListener("click", this.#boundClick, true);
6650
+ }
6651
+
6652
+ #unbindEvents() {
6653
+ this.#wheel?.removeEventListener("input", this.#boundPrimitiveInput);
6654
+ this.#wheel?.removeEventListener("change", this.#boundPrimitiveChange);
6655
+ this.#input?.removeEventListener("input", this.#boundNumberInput);
6656
+ this.#input?.removeEventListener("change", this.#boundNumberChange);
6657
+ this.removeEventListener("pointerdown", this.#boundNumberPointerDown, {
6658
+ capture: true,
6659
+ });
6660
+ this.removeEventListener("click", this.#boundClick, true);
6661
+ }
6662
+
6663
+ #setSynchronizedValue(value) {
6664
+ const parsed = Number(value);
6665
+ if (!this.#wheel) {
6666
+ const normalized = Number.isFinite(parsed) ? parsed : 0;
6667
+ this.setAttribute("value", String(normalized));
6668
+ return normalized;
6669
+ }
6670
+ this.#wheel.value = Number.isFinite(parsed) ? parsed : 0;
6671
+ const normalized = this.#wheel.value;
6672
+ if (this.getAttribute("value") !== normalized) {
6673
+ this.setAttribute("value", normalized);
6674
+ }
6675
+ if (this.#input?.getAttribute("value") !== normalized) {
6676
+ this.#input?.setAttribute("value", normalized);
6677
+ }
6678
+ this.#syncPrimitiveAria();
6679
+ return Number(normalized);
6680
+ }
6681
+
6682
+ #emit(type, value) {
6683
+ this.dispatchEvent(
6684
+ new CustomEvent(type, {
6685
+ detail: value,
6686
+ bubbles: true,
6687
+ cancelable: true,
6688
+ composed: true,
6689
+ }),
6690
+ );
6691
+ }
6692
+
6693
+ #handlePrimitiveEvent(type, event) {
6694
+ event.stopImmediatePropagation();
6695
+ const value = this.#setSynchronizedValue(event.detail);
6696
+ this.#emit(type, value);
6697
+ }
6698
+
6699
+ #handleNumberEvent(type, event) {
6700
+ event.stopImmediatePropagation();
6701
+ if (figLabBooleanAttribute(this, "disabled")) return;
6702
+ const raw =
6703
+ event instanceof CustomEvent && event.detail !== undefined
6704
+ ? event.detail
6705
+ : this.#input?.value;
6706
+ const value = this.#setSynchronizedValue(raw);
6707
+ this.#emit(type, value);
6708
+ }
6709
+
6710
+ #handleNumberPointerDown(event) {
6711
+ if (
6712
+ event.button !== 0 ||
6713
+ event.altKey ||
6714
+ figLabBooleanAttribute(this, "disabled") ||
6715
+ !(event.target instanceof Element)
6716
+ ) {
6717
+ return;
6718
+ }
6719
+ const input = event.target.closest("fig-input-number input");
6720
+ if (!input || input === document.activeElement) return;
6721
+ event.preventDefault();
6722
+ event.stopImmediatePropagation();
6723
+ this.#stopNumberTracking();
6724
+ this.#numberPointerId = event.pointerId;
6725
+ this.#numberPointerStartX = event.clientX;
6726
+ this.#numberPointerStartY = event.clientY;
6727
+ this.#numberPointerStartValue = Number(this.#wheel?.value ?? 0);
6728
+ window.addEventListener("pointermove", this.#boundNumberPointerMove);
6729
+ window.addEventListener("pointerup", this.#boundNumberPointerEnd, {
6730
+ once: true,
6731
+ });
6732
+ window.addEventListener("pointercancel", this.#boundNumberPointerEnd, {
6733
+ once: true,
6734
+ });
6735
+ window.addEventListener("blur", this.#boundNumberPointerEnd, {
6736
+ once: true,
6737
+ });
6738
+ }
6739
+
6740
+ #handleNumberPointerMove(event) {
6741
+ if (event.pointerId !== this.#numberPointerId) return;
6742
+ if (event.buttons === 0) {
6743
+ this.#handleNumberPointerEnd(event);
6744
+ return;
6745
+ }
6746
+ if (!this.#isNumberScrubbing) {
6747
+ const distance = Math.hypot(
6748
+ event.clientX - this.#numberPointerStartX,
6749
+ event.clientY - this.#numberPointerStartY,
6750
+ );
6751
+ if (distance < 4) return;
6752
+ this.#isNumberScrubbing = true;
6753
+ this.setAttribute("data-number-scrubbing", "");
6754
+ this.#wheel?.beginScrub({
6755
+ clientX: this.#numberPointerStartX,
6756
+ pointerId: event.pointerId,
6757
+ startValue: this.#numberPointerStartValue,
6758
+ });
6759
+ }
6760
+ this.#wheel?.updateScrub(event);
6761
+ }
6762
+
6763
+ #handleNumberPointerEnd(event) {
6764
+ if (
6765
+ event?.pointerId !== undefined &&
6766
+ this.#numberPointerId !== null &&
6767
+ event.pointerId !== this.#numberPointerId
6768
+ ) {
6769
+ return;
6770
+ }
6771
+ const wasScrubbing = this.#isNumberScrubbing;
6772
+ this.#stopNumberTracking();
6773
+ if (wasScrubbing) {
6774
+ this.#wheel?.endScrub();
6775
+ this.#suppressNumberClick = true;
6776
+ clearTimeout(this.#numberClickResetTimer);
6777
+ this.#numberClickResetTimer = window.setTimeout(() => {
6778
+ this.#suppressNumberClick = false;
6779
+ this.#numberClickResetTimer = 0;
6780
+ }, 0);
6781
+ requestAnimationFrame(() => this.#wheel?.focus());
6782
+ return;
6783
+ }
6784
+ this.#input?.querySelector("input")?.focus();
6785
+ }
6786
+
6787
+ #stopNumberTracking() {
6788
+ window.removeEventListener("pointermove", this.#boundNumberPointerMove);
6789
+ window.removeEventListener("pointerup", this.#boundNumberPointerEnd);
6790
+ window.removeEventListener("pointercancel", this.#boundNumberPointerEnd);
6791
+ window.removeEventListener("blur", this.#boundNumberPointerEnd);
6792
+ this.#numberPointerId = null;
6793
+ this.#isNumberScrubbing = false;
6794
+ this.removeAttribute("data-number-scrubbing");
6795
+ }
6796
+
6797
+ #handleClick(event) {
6798
+ if (figLabBooleanAttribute(this, "disabled")) return;
6799
+ if (
6800
+ event.target instanceof Element &&
6801
+ event.target.closest("fig-input-number, fig-menu")
6802
+ ) {
6803
+ if (
6804
+ this.#suppressNumberClick &&
6805
+ event.target.closest("fig-input-number")
6806
+ ) {
6807
+ event.preventDefault();
6808
+ event.stopImmediatePropagation();
6809
+ this.#suppressNumberClick = false;
6810
+ }
6811
+ return;
6812
+ }
6813
+ if (
6814
+ !(event.target instanceof Element) ||
6815
+ !event.target.closest("fig-input-wheel")
6816
+ ) {
6817
+ this.#wheel?.focus();
6818
+ }
6819
+ }
6820
+
6821
+ get value() {
6822
+ return this.getAttribute("value") ?? this.#wheel?.value ?? "0";
6823
+ }
6824
+
6825
+ set value(nextValue) {
6826
+ const parsed = Number(nextValue);
6827
+ this.#setSynchronizedValue(Number.isFinite(parsed) ? parsed : 0);
6828
+ }
6829
+
6830
+ get min() {
6831
+ return this.#wheel?.min ?? null;
6832
+ }
6833
+
6834
+ set min(nextValue) {
6835
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
6836
+ this.removeAttribute("min");
6837
+ } else {
6838
+ const parsed = Number(nextValue);
6839
+ if (Number.isFinite(parsed)) this.setAttribute("min", String(parsed));
6840
+ else this.removeAttribute("min");
6841
+ }
6842
+ }
6843
+
6844
+ get max() {
6845
+ return this.#wheel?.max ?? null;
6846
+ }
6847
+
6848
+ set max(nextValue) {
6849
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
6850
+ this.removeAttribute("max");
6851
+ } else {
6852
+ const parsed = Number(nextValue);
6853
+ if (Number.isFinite(parsed)) this.setAttribute("max", String(parsed));
6854
+ else this.removeAttribute("max");
6855
+ }
6856
+ }
6857
+
6574
6858
  get defaultValue() {
6575
6859
  return this.getAttribute("default") ?? this.#initialValue ?? "0";
6576
6860
  }
@@ -6581,8 +6865,9 @@ class PropskitWheel extends HTMLElement {
6581
6865
 
6582
6866
  resetToDefault() {
6583
6867
  const parsed = Number(this.defaultValue);
6584
- const value = Number.isFinite(parsed) ? parsed : 0;
6585
- this.#commitValue(value);
6868
+ const value = this.#setSynchronizedValue(
6869
+ Number.isFinite(parsed) ? parsed : 0,
6870
+ );
6586
6871
  figLabEmitPropskitReset(this, value);
6587
6872
  }
6588
6873
 
@@ -9756,6 +10041,7 @@ class FigReorder extends HTMLElement {
9756
10041
  "fig-input-gradient",
9757
10042
  "fig-easing-curve",
9758
10043
  "fig-input-angle",
10044
+ "fig-input-wheel",
9759
10045
  "fig-input-joystick",
9760
10046
  "fig-canvas-control",
9761
10047
  "propskit-color-point",