@rogieking/figui3 8.9.29 → 8.9.31

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
@@ -4639,6 +4639,7 @@ class PropskitSlider extends HTMLElement {
4639
4639
  "data-wave-index",
4640
4640
  "data-active",
4641
4641
  "data-elastic-dragging",
4642
+ "default",
4642
4643
  "style",
4643
4644
  ]);
4644
4645
 
@@ -4761,8 +4762,11 @@ class PropskitSlider extends HTMLElement {
4761
4762
  }
4762
4763
 
4763
4764
  #initialize() {
4765
+ const sliderType = (this.getAttribute("type") || "range").toLowerCase();
4764
4766
  this.#initialValue =
4765
- this.getAttribute("value") ?? this.getAttribute("min") ?? "0";
4767
+ this.getAttribute("value") ??
4768
+ this.getAttribute("default") ??
4769
+ (sliderType === "delta" ? "0" : this.getAttribute("min") ?? "0");
4766
4770
  const initialChildren = Array.from(this.childNodes).filter((node) => {
4767
4771
  return (
4768
4772
  node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim())
@@ -4866,12 +4870,15 @@ class PropskitSlider extends HTMLElement {
4866
4870
  this.#slider.setAttribute("text", "true");
4867
4871
 
4868
4872
  const sliderType = (this.getAttribute("type") || "range").toLowerCase();
4869
- if (sliderType === "delta" || sliderType === "stepper") {
4870
- this.#slider.setAttribute(
4871
- "default",
4872
- this.getAttribute("default") ?? "50",
4873
- );
4874
- } else if (!this.hasAttribute("default")) {
4873
+ if (sliderType === "delta") {
4874
+ const min = Number(this.#slider.min);
4875
+ const max = Number(this.#slider.max);
4876
+ const midpoint =
4877
+ Number.isFinite(min) && Number.isFinite(max)
4878
+ ? min + (max - min) / 2
4879
+ : 0;
4880
+ this.#slider.setAttribute("default", String(midpoint));
4881
+ } else {
4875
4882
  this.#slider.removeAttribute("default");
4876
4883
  }
4877
4884
  if (sliderType === "stepper") {
@@ -4910,8 +4917,10 @@ class PropskitSlider extends HTMLElement {
4910
4917
  }
4911
4918
 
4912
4919
  #pushExternalValueToSlider() {
4913
- if (!this.#slider || !this.hasAttribute("value")) return;
4914
- const next = this.getAttribute("value") ?? "";
4920
+ if (!this.#slider) return;
4921
+ const next = this.hasAttribute("value")
4922
+ ? this.getAttribute("value") ?? ""
4923
+ : this.#initialValue ?? "";
4915
4924
  if (String(this.#slider.value) !== next) {
4916
4925
  this.#slider.value = next;
4917
4926
  }
@@ -5313,7 +5322,6 @@ class PropskitSlider extends HTMLElement {
5313
5322
  #defaultValue() {
5314
5323
  return (
5315
5324
  this.getAttribute("default") ??
5316
- this.#slider?.getAttribute("default") ??
5317
5325
  this.#initialValue ??
5318
5326
  this.#rangeInput?.min ??
5319
5327
  "0"
@@ -7163,6 +7171,7 @@ class PropskitOscillator extends HTMLElement {
7163
7171
  { text: label },
7164
7172
  figLabCreateElement("fig-handle", {
7165
7173
  size: "small",
7174
+ type: "minimal",
7166
7175
  "aria-label": `Oscillator ${type} handle`,
7167
7176
  }),
7168
7177
  ),
package/fig.js CHANGED
@@ -407,14 +407,19 @@ function figSyncOverflowState(host, scrollEl, axis = "x", threshold = 2) {
407
407
  return scrollable;
408
408
  }
409
409
 
410
- function figScrollOverflowPage(scrollEl, axis = "x", direction = 1) {
410
+ function figScrollOverflowPage(
411
+ scrollEl,
412
+ axis = "x",
413
+ direction = 1,
414
+ behavior = "smooth",
415
+ ) {
411
416
  if (!scrollEl) return;
412
417
  const isHorizontal = axis === "x";
413
418
  const pageSize = isHorizontal ? scrollEl.clientWidth : scrollEl.clientHeight;
414
419
  const scrollAmount = pageSize * 0.8 * direction;
415
420
  scrollEl.scrollBy({
416
421
  [isHorizontal ? "left" : "top"]: scrollAmount,
417
- behavior: "smooth",
422
+ behavior,
418
423
  });
419
424
  }
420
425
 
@@ -5700,11 +5705,7 @@ class FigSlider extends HTMLElement {
5700
5705
  this.step = readNumber("step", defaults.step);
5701
5706
  if (!(this.step > 0)) this.step = defaults.step > 0 ? defaults.step : 1;
5702
5707
  this.color = this.getAttribute("color") || defaults?.color;
5703
- this.default = this.hasAttribute("default")
5704
- ? this.getAttribute("default")
5705
- : this.type === "delta"
5706
- ? 0
5707
- : this.min;
5708
+ this.default = this.#resolveDefaultValue();
5708
5709
  this.value = this.#normalizeSliderValue(rawValue);
5709
5710
  }
5710
5711
 
@@ -5772,12 +5773,7 @@ class FigSlider extends HTMLElement {
5772
5773
  this.input.removeEventListener("pointercancel", this.#boundRangePointerUp);
5773
5774
  this.input.addEventListener("pointercancel", this.#boundRangePointerUp);
5774
5775
 
5775
- if (this.default) {
5776
- this.style.setProperty(
5777
- "--default",
5778
- this.#calculateNormal(this.default),
5779
- );
5780
- }
5776
+ this.style.setProperty("--default", this.#calculateNormal(this.default));
5781
5777
 
5782
5778
  if (this.figInputNumber) {
5783
5779
  this.#syncTextInputA11yAttributes();
@@ -5864,6 +5860,7 @@ class FigSlider extends HTMLElement {
5864
5860
  this.input.setAttribute("list", this.datalist.getAttribute("id"));
5865
5861
  } else if (this.type === "stepper") {
5866
5862
  this.datalist = document.createElement("datalist");
5863
+ this.datalist.setAttribute("data-generated", "slider-stepper");
5867
5864
  this.datalist.setAttribute("id", figUniqueId());
5868
5865
  const steps = Math.min(
5869
5866
  1001,
@@ -5878,6 +5875,7 @@ class FigSlider extends HTMLElement {
5878
5875
  this.input.setAttribute("list", this.datalist.getAttribute("id"));
5879
5876
  } else if (this.type === "delta") {
5880
5877
  this.datalist = document.createElement("datalist");
5878
+ this.datalist.setAttribute("data-generated", "slider-delta");
5881
5879
  this.datalist.setAttribute("id", figUniqueId());
5882
5880
  let option = document.createElement("option");
5883
5881
  option.setAttribute("value", this.default);
@@ -5885,14 +5883,7 @@ class FigSlider extends HTMLElement {
5885
5883
  this.inputContainer.append(this.datalist);
5886
5884
  this.input.setAttribute("list", this.datalist.getAttribute("id"));
5887
5885
  }
5888
- if (this.datalist) {
5889
- const defaultOption = [...this.datalist.querySelectorAll("option")].find(
5890
- (option) => option.value === String(this.default),
5891
- );
5892
- if (defaultOption) {
5893
- defaultOption.setAttribute("default", "true");
5894
- }
5895
- }
5886
+ this.#syncDefaultDatalist();
5896
5887
  this.#syncValue();
5897
5888
  }
5898
5889
 
@@ -5938,6 +5929,10 @@ class FigSlider extends HTMLElement {
5938
5929
  if (this.input) this.#syncProperties();
5939
5930
  }
5940
5931
 
5932
+ get defaultValue() {
5933
+ return this.#resolveDefaultValue();
5934
+ }
5935
+
5941
5936
  disconnectedCallback() {
5942
5937
  this.#pendingRegeneration = false;
5943
5938
  if (this.input) {
@@ -5998,6 +5993,28 @@ class FigSlider extends HTMLElement {
5998
5993
  const { min, max } = this.#getBounds();
5999
5994
  return Math.min(max, Math.max(min, value));
6000
5995
  }
5996
+ #isStepAligned(value) {
5997
+ const { min } = this.#getBounds();
5998
+ const step = this.#toFiniteNumber(this.step);
5999
+ if (!(step > 0)) return true;
6000
+ const steps = (value - min) / step;
6001
+ const nearest = Math.round(steps);
6002
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(steps)) * 16;
6003
+ return Math.abs(steps - nearest) <= tolerance;
6004
+ }
6005
+ #resolveDefaultValue(rawDefault = this.getAttribute("default")) {
6006
+ const { min, max } = this.#getBounds();
6007
+ const parsed = this.#toFiniteNumber(rawDefault);
6008
+ const isValid =
6009
+ parsed !== null &&
6010
+ parsed >= min &&
6011
+ parsed <= max &&
6012
+ (this.type !== "stepper" || this.#isStepAligned(parsed));
6013
+ if (isValid) return parsed;
6014
+ return this.type === "delta"
6015
+ ? Math.min(max, Math.max(min, 0))
6016
+ : min;
6017
+ }
6001
6018
  #getFallbackValue() {
6002
6019
  if (this.type === "delta") {
6003
6020
  const deltaDefault = this.#toFiniteNumber(this.default);
@@ -6163,10 +6180,13 @@ class FigSlider extends HTMLElement {
6163
6180
  }
6164
6181
  if (!this.hasAttribute("default") && this.type !== "delta") {
6165
6182
  this.default = this.min;
6183
+ } else {
6184
+ this.default = this.#resolveDefaultValue();
6166
6185
  }
6167
6186
  this.value = this.#normalizeSliderValue(this.value);
6168
6187
  this.#syncProperties();
6169
6188
  this.#syncStepperDatalist();
6189
+ this.#syncDefaultDatalist();
6170
6190
  }
6171
6191
 
6172
6192
  #syncStepperDatalist() {
@@ -6183,6 +6203,25 @@ class FigSlider extends HTMLElement {
6183
6203
  }
6184
6204
  this.datalist.replaceChildren(fragment);
6185
6205
  this.input.setAttribute("list", this.datalist.id);
6206
+ this.#syncDefaultDatalist();
6207
+ }
6208
+
6209
+ #syncDefaultDatalist() {
6210
+ if (!this.datalist) return;
6211
+ const defaultValue = String(this.default);
6212
+ const options = [...this.datalist.querySelectorAll("option")];
6213
+ options.forEach((option) => option.removeAttribute("default"));
6214
+ let defaultOption = options.find((option) => option.value === defaultValue);
6215
+ if (
6216
+ !defaultOption &&
6217
+ this.type === "delta" &&
6218
+ this.datalist.getAttribute("data-generated") === "slider-delta"
6219
+ ) {
6220
+ defaultOption = document.createElement("option");
6221
+ defaultOption.value = defaultValue;
6222
+ this.datalist.replaceChildren(defaultOption);
6223
+ }
6224
+ defaultOption?.setAttribute("default", "true");
6186
6225
  }
6187
6226
 
6188
6227
  #regenerateWhenIdle() {
@@ -6272,9 +6311,9 @@ class FigSlider extends HTMLElement {
6272
6311
  }
6273
6312
  break;
6274
6313
  case "default":
6275
- this.default =
6276
- newValue !== null ? newValue : this.type === "delta" ? 0 : this.min;
6314
+ this.default = this.#resolveDefaultValue(newValue);
6277
6315
  this.#syncProperties();
6316
+ this.#syncDefaultDatalist();
6278
6317
  break;
6279
6318
  case "min":
6280
6319
  case "max":
@@ -16850,6 +16889,7 @@ class FigGroup extends HTMLElement {
16850
16889
 
16851
16890
  attributeChangedCallback(name, oldValue, newValue) {
16852
16891
  if (oldValue === newValue) return;
16892
+ if (!this.isConnected) return;
16853
16893
  if (name === "open") {
16854
16894
  this.#header?.setAttribute("aria-expanded", String(this.open));
16855
16895
  return;
@@ -16898,7 +16938,9 @@ class FigGroup extends HTMLElement {
16898
16938
  const label = nameAttr || (isCollapsible ? "Group" : null);
16899
16939
 
16900
16940
  // Check if user supplied their own fig-header
16901
- const userHeader = this.querySelector(":scope > fig-header");
16941
+ const userHeader = this.querySelector(
16942
+ ":scope > fig-header:not([data-generated])",
16943
+ );
16902
16944
 
16903
16945
  if (!label && !isCollapsible && !userHeader) {
16904
16946
  if (this.#header && this.#header.dataset.generated) {
@@ -16910,6 +16952,9 @@ class FigGroup extends HTMLElement {
16910
16952
  }
16911
16953
 
16912
16954
  if (userHeader) {
16955
+ this.querySelectorAll(":scope > fig-header[data-generated]").forEach(
16956
+ (header) => header.remove(),
16957
+ );
16913
16958
  this.#header = userHeader;
16914
16959
  } else if (!this.#header || !this.#header.dataset.generated) {
16915
16960
  this.#header = document.createElement("fig-header");
@@ -17647,6 +17692,8 @@ figDefineElement("fig-choice", FigChoice);
17647
17692
  * @attr {number} columns - Number of columns when layout="grid" (default: 2)
17648
17693
  * @attr {string} value - Selected choice value. Omit to select the first choice; `value=""` means none.
17649
17694
  * @attr {boolean} disabled - Whether the chooser is disabled
17695
+ * @attr {boolean} auto-scroll - Whether selection changes automatically scroll into view (default: true)
17696
+ * @attr {"auto"|"smooth"} scroll-behavior - Scrolling animation behavior (default: "smooth")
17650
17697
  */
17651
17698
  class FigChooser extends HTMLElement {
17652
17699
  #selectedChoice = null;
@@ -17674,6 +17721,7 @@ class FigChooser extends HTMLElement {
17674
17721
  "layout",
17675
17722
  "overflow",
17676
17723
  "loop",
17724
+ "auto-scroll",
17677
17725
  ];
17678
17726
  }
17679
17727
 
@@ -17688,6 +17736,31 @@ class FigChooser extends HTMLElement {
17688
17736
  return attr === null || attr !== "false";
17689
17737
  }
17690
17738
 
17739
+ get autoScroll() {
17740
+ const attr = this.getAttribute("auto-scroll");
17741
+ return attr === null || attr !== "false";
17742
+ }
17743
+
17744
+ set autoScroll(value) {
17745
+ if (value === false || value === "false" || value === null) {
17746
+ this.setAttribute("auto-scroll", "false");
17747
+ } else {
17748
+ this.removeAttribute("auto-scroll");
17749
+ }
17750
+ }
17751
+
17752
+ get scrollBehavior() {
17753
+ return this.getAttribute("scroll-behavior") === "auto" ? "auto" : "smooth";
17754
+ }
17755
+
17756
+ set scrollBehavior(value) {
17757
+ if (value === "auto" || value === "smooth") {
17758
+ this.setAttribute("scroll-behavior", value);
17759
+ } else {
17760
+ this.removeAttribute("scroll-behavior");
17761
+ }
17762
+ }
17763
+
17691
17764
  get #choiceSelector() {
17692
17765
  return this.getAttribute("choice-element") || "fig-choice";
17693
17766
  }
@@ -17727,7 +17800,7 @@ class FigChooser extends HTMLElement {
17727
17800
  if (this.getAttribute("value") !== val) {
17728
17801
  this.setAttribute("value", val);
17729
17802
  }
17730
- this.#scrollToChoice(element);
17803
+ if (this.autoScroll) this.#scrollToChoice(element);
17731
17804
  }
17732
17805
 
17733
17806
  get value() {
@@ -17742,6 +17815,11 @@ class FigChooser extends HTMLElement {
17742
17815
  this.setAttribute("value", String(val));
17743
17816
  }
17744
17817
 
17818
+ scrollSelectionIntoView(options = {}) {
17819
+ if (!this.#selectedChoice) return;
17820
+ this.#scrollToChoice(this.#selectedChoice, options);
17821
+ }
17822
+
17745
17823
  connectedCallback() {
17746
17824
  this.setAttribute("role", "listbox");
17747
17825
  if (this.shadowRoot) this.shadowRoot.replaceChildren();
@@ -17765,9 +17843,9 @@ class FigChooser extends HTMLElement {
17765
17843
 
17766
17844
  #scheduleInitialScrollSettle() {
17767
17845
  const resettle = () => {
17768
- if (!this.isConnected) return;
17846
+ if (!this.isConnected || !this.autoScroll) return;
17769
17847
  if (this.#selectedChoice) {
17770
- this.#scrollToChoice(this.#selectedChoice, "auto");
17848
+ this.#scrollToChoice(this.#selectedChoice);
17771
17849
  }
17772
17850
  };
17773
17851
  const wireImages = () => {
@@ -17843,6 +17921,9 @@ class FigChooser extends HTMLElement {
17843
17921
  }
17844
17922
  this.#resettleSelectedChoice();
17845
17923
  }
17924
+ if (name === "auto-scroll" && this.autoScroll) {
17925
+ this.#resettleSelectedChoice();
17926
+ }
17846
17927
  }
17847
17928
 
17848
17929
  #syncSelection() {
@@ -17986,7 +18067,7 @@ class FigChooser extends HTMLElement {
17986
18067
 
17987
18068
  if (nextIndex !== currentIndex && choices[nextIndex]) {
17988
18069
  this.selectedChoice = choices[nextIndex];
17989
- choices[nextIndex].focus();
18070
+ choices[nextIndex].focus({ preventScroll: !this.autoScroll });
17990
18071
  this.#emitEvents();
17991
18072
  }
17992
18073
  }
@@ -18196,16 +18277,25 @@ class FigChooser extends HTMLElement {
18196
18277
 
18197
18278
  #scrollByPage(direction) {
18198
18279
  const isHorizontal = this.getAttribute("layout") === "horizontal";
18199
- figScrollOverflowPage(this, isHorizontal ? "x" : "y", direction);
18280
+ figScrollOverflowPage(this, isHorizontal ? "x" : "y", direction, "auto");
18200
18281
  }
18201
18282
 
18202
18283
  #resettleSelectedChoice() {
18203
- if (!this.#selectedChoice) return;
18204
- this.#scrollToChoice(this.#selectedChoice, "auto");
18284
+ if (!this.autoScroll || !this.#selectedChoice) return;
18285
+ this.#scrollToChoice(this.#selectedChoice);
18205
18286
  }
18206
18287
 
18207
- #scrollToChoice(el, behavior = "smooth") {
18288
+ #scrollToChoice(el, options = {}) {
18208
18289
  if (!el) return;
18290
+ const scrollOptions =
18291
+ typeof options === "object" && options !== null ? options : {};
18292
+ const behavior =
18293
+ scrollOptions.behavior === "smooth" ||
18294
+ scrollOptions.behavior === "instant"
18295
+ ? scrollOptions.behavior
18296
+ : "auto";
18297
+ const block = scrollOptions.block || "center";
18298
+ const inline = scrollOptions.inline || "center";
18209
18299
  requestAnimationFrame(() => {
18210
18300
  if (!el.isConnected) return;
18211
18301
  const overflowY = this.scrollHeight > this.clientHeight;
@@ -18220,9 +18310,13 @@ class FigChooser extends HTMLElement {
18220
18310
  if (overflowY) {
18221
18311
  const choiceTop = choiceRect.top - hostRect.top + this.scrollTop;
18222
18312
  const maxScroll = this.scrollHeight - this.clientHeight;
18223
- options.top = Math.max(
18224
- 0,
18225
- Math.min(choiceTop + choiceRect.height / 2 - this.clientHeight / 2, maxScroll),
18313
+ options.top = this.#alignedScrollPosition(
18314
+ choiceTop,
18315
+ choiceRect.height,
18316
+ this.clientHeight,
18317
+ this.scrollTop,
18318
+ maxScroll,
18319
+ block,
18226
18320
  );
18227
18321
  shouldScroll = true;
18228
18322
  }
@@ -18230,9 +18324,13 @@ class FigChooser extends HTMLElement {
18230
18324
  if (overflowX) {
18231
18325
  const choiceLeft = choiceRect.left - hostRect.left + this.scrollLeft;
18232
18326
  const maxScroll = this.scrollWidth - this.clientWidth;
18233
- options.left = Math.max(
18234
- 0,
18235
- Math.min(choiceLeft + choiceRect.width / 2 - this.clientWidth / 2, maxScroll),
18327
+ options.left = this.#alignedScrollPosition(
18328
+ choiceLeft,
18329
+ choiceRect.width,
18330
+ this.clientWidth,
18331
+ this.scrollLeft,
18332
+ maxScroll,
18333
+ inline,
18236
18334
  );
18237
18335
  shouldScroll = true;
18238
18336
  }
@@ -18244,6 +18342,36 @@ class FigChooser extends HTMLElement {
18244
18342
  });
18245
18343
  }
18246
18344
 
18345
+ #alignedScrollPosition(
18346
+ elementStart,
18347
+ elementSize,
18348
+ viewportSize,
18349
+ currentScroll,
18350
+ maxScroll,
18351
+ alignment,
18352
+ ) {
18353
+ let nextScroll;
18354
+ if (alignment === "start") {
18355
+ nextScroll = elementStart;
18356
+ } else if (alignment === "end") {
18357
+ nextScroll = elementStart + elementSize - viewportSize;
18358
+ } else if (alignment === "nearest") {
18359
+ const viewportStart = currentScroll;
18360
+ const viewportEnd = currentScroll + viewportSize;
18361
+ const elementEnd = elementStart + elementSize;
18362
+ if (elementStart >= viewportStart && elementEnd <= viewportEnd) {
18363
+ nextScroll = currentScroll;
18364
+ } else if (elementStart < viewportStart) {
18365
+ nextScroll = elementStart;
18366
+ } else {
18367
+ nextScroll = elementEnd - viewportSize;
18368
+ }
18369
+ } else {
18370
+ nextScroll = elementStart + elementSize / 2 - viewportSize / 2;
18371
+ }
18372
+ return Math.max(0, Math.min(nextScroll, maxScroll));
18373
+ }
18374
+
18247
18375
  #startObserver() {
18248
18376
  this.#mutationObserver?.disconnect();
18249
18377
  this.#mutationObserver = new MutationObserver(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "8.9.29",
3
+ "version": "8.9.31",
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",