@rogieking/figui3 2.10.1 → 2.10.3

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.
Files changed (3) hide show
  1. package/components.css +1 -11
  2. package/fig.js +48 -76
  3. package/package.json +1 -1
package/components.css CHANGED
@@ -581,10 +581,8 @@ input[type="text"][list] {
581
581
  }
582
582
 
583
583
  ::picker(select) {
584
- position-area: auto;
585
- align-self: auto;
586
- max-block-size: 100vh;
587
584
  appearance: base-select;
585
+
588
586
  scrollbar-width: thin;
589
587
  outline: 0;
590
588
  scrollbar-color: var(--figma-color-text-menu-tertiary)
@@ -595,14 +593,6 @@ input[type="text"][list] {
595
593
  padding: var(--spacer-2) 0;
596
594
  box-shadow: var(--figma-elevation-400-menu-panel);
597
595
  }
598
- select {
599
- &::picker(select) {
600
- /* --picker-top and --picker-max-height are calculated by JavaScript */
601
- top: var(--picker-top, 0px);
602
- max-height: var(--picker-max-height, 100vh);
603
- overflow-y: auto;
604
- }
605
- }
606
596
  }
607
597
  }
608
598
 
package/fig.js CHANGED
@@ -167,9 +167,6 @@ customElements.define("fig-button", FigButton);
167
167
  class FigDropdown extends HTMLElement {
168
168
  #label = "Menu";
169
169
  #selectedValue = null; // Stores last selected value for dropdown type
170
- #optionHeight = 28; // Default option height in pixels
171
- #pickerPadding = 8; // Padding inside picker (spacer-2)
172
- #viewportMargin = 8; // Minimum margin from viewport edges
173
170
 
174
171
  get label() {
175
172
  return this.#label;
@@ -187,12 +184,6 @@ class FigDropdown extends HTMLElement {
187
184
  #addEventListeners() {
188
185
  this.select.addEventListener("input", this.#handleSelectInput.bind(this));
189
186
  this.select.addEventListener("change", this.#handleSelectChange.bind(this));
190
- // Calculate picker position before it opens
191
- this.select.addEventListener(
192
- "mousedown",
193
- this.#calculatePickerPosition.bind(this),
194
- );
195
- this.select.addEventListener("keydown", this.#handleKeyDown.bind(this));
196
187
  }
197
188
 
198
189
  connectedCallback() {
@@ -230,7 +221,6 @@ class FigDropdown extends HTMLElement {
230
221
  if (this.type === "dropdown") {
231
222
  this.select.selectedIndex = -1;
232
223
  }
233
- this.#calculatePickerPosition();
234
224
  }
235
225
 
236
226
  #handleSelectInput(e) {
@@ -257,7 +247,6 @@ class FigDropdown extends HTMLElement {
257
247
  if (this.type === "dropdown") {
258
248
  this.select.selectedIndex = -1;
259
249
  }
260
- this.#calculatePickerPosition();
261
250
  this.dispatchEvent(
262
251
  new CustomEvent("change", {
263
252
  detail: selectedValue,
@@ -267,64 +256,6 @@ class FigDropdown extends HTMLElement {
267
256
  );
268
257
  }
269
258
 
270
- #handleKeyDown(e) {
271
- // Recalculate on keyboard navigation that might open the picker
272
- if (
273
- e.key === " " ||
274
- e.key === "Enter" ||
275
- e.key === "ArrowDown" ||
276
- e.key === "ArrowUp"
277
- ) {
278
- this.#calculatePickerPosition();
279
- }
280
- }
281
-
282
- #calculatePickerPosition() {
283
- // Only apply positioning for neue variant
284
- if (this.getAttribute("variant") !== "neue") {
285
- return;
286
- }
287
-
288
- // Get positions in viewport
289
- const dropdownRect = this.getBoundingClientRect();
290
- const selectRect = this.select.getBoundingClientRect();
291
- const viewportHeight = window.innerHeight;
292
-
293
- // Get selected index (use 0 for dropdown type since nothing is visually selected)
294
- let selectedIndex = this.select.selectedIndex;
295
- if (this.type === "dropdown" || selectedIndex < 0) {
296
- selectedIndex = 0;
297
- }
298
-
299
- // Calculate the top offset to align selected item with trigger
300
- // This is relative to the anchor point (bottom of select by default)
301
- const pickerTopOffset =
302
- -1 * (selectedIndex + 1) * this.#optionHeight - this.#pickerPadding;
303
-
304
- // The picker is anchored to the select, and top offset is relative to the anchor
305
- // Anchor is typically at the bottom of the select element
306
- // Actual picker top = select.bottom + topOffset (topOffset is negative)
307
- const pickerActualTop = selectRect.bottom + pickerTopOffset;
308
-
309
- // Ensure picker doesn't go above viewport
310
- let adjustedTopOffset = pickerTopOffset;
311
- if (pickerActualTop < this.#viewportMargin) {
312
- adjustedTopOffset = this.#viewportMargin - selectRect.bottom;
313
- }
314
-
315
- // Recalculate actual top with adjusted offset
316
- const adjustedActualTop = selectRect.bottom + adjustedTopOffset;
317
-
318
- // Calculate max-height: from picker's actual top to 1rem (16px) from bottom of viewport
319
- const bottomMargin = 16; // 1rem
320
- const maxHeight = viewportHeight - adjustedActualTop - bottomMargin;
321
-
322
- // Set CSS variables for the picker
323
- this.select.style.setProperty("--selected-index", selectedIndex);
324
- this.select.style.setProperty("--picker-top", `${adjustedTopOffset}px`);
325
- this.select.style.setProperty("--picker-max-height", `${Math.max(0, maxHeight)}px`);
326
- }
327
-
328
259
  focus() {
329
260
  this.select.focus();
330
261
  }
@@ -346,7 +277,7 @@ class FigDropdown extends HTMLElement {
346
277
  this.setAttribute("value", value);
347
278
  }
348
279
  static get observedAttributes() {
349
- return ["value", "type", "variant"];
280
+ return ["value", "type"];
350
281
  }
351
282
  #syncSelectedValue(value) {
352
283
  // For dropdown type, don't sync the visual selection - it should always show the hidden placeholder
@@ -359,7 +290,6 @@ class FigDropdown extends HTMLElement {
359
290
  this.select.selectedIndex = i;
360
291
  }
361
292
  });
362
- this.#calculatePickerPosition();
363
293
  }
364
294
  }
365
295
  attributeChangedCallback(name, oldValue, newValue) {
@@ -373,9 +303,6 @@ class FigDropdown extends HTMLElement {
373
303
  this.#label = newValue;
374
304
  this.select.setAttribute("aria-label", this.#label);
375
305
  }
376
- if (name === "variant") {
377
- this.#calculatePickerPosition();
378
- }
379
306
  }
380
307
  }
381
308
 
@@ -2008,6 +1935,7 @@ class FigInputNumber extends HTMLElement {
2008
1935
  #boundInput;
2009
1936
  #boundFocus;
2010
1937
  #boundBlur;
1938
+ #boundKeyDown;
2011
1939
  #units;
2012
1940
  #unitPosition;
2013
1941
 
@@ -2031,6 +1959,9 @@ class FigInputNumber extends HTMLElement {
2031
1959
  this.#boundBlur = (e) => {
2032
1960
  this.#handleBlur(e);
2033
1961
  };
1962
+ this.#boundKeyDown = (e) => {
1963
+ this.#handleKeyDown(e);
1964
+ };
2034
1965
  }
2035
1966
 
2036
1967
  connectedCallback() {
@@ -2053,9 +1984,17 @@ class FigInputNumber extends HTMLElement {
2053
1984
  }
2054
1985
  this.transform = Number(this.getAttribute("transform") || 1);
2055
1986
 
1987
+ const hasSteppers =
1988
+ this.hasAttribute("steppers") &&
1989
+ this.getAttribute("steppers") !== "false";
1990
+
1991
+ // Use type="number" when steppers are enabled (for native spin buttons)
1992
+ const inputType = hasSteppers ? "number" : "text";
1993
+ const inputMode = hasSteppers ? "" : 'inputmode="decimal"';
1994
+
2056
1995
  let html = `<input
2057
- type="text"
2058
- inputmode="decimal"
1996
+ type="${inputType}"
1997
+ ${inputMode}
2059
1998
  ${this.name ? `name="${this.name}"` : ""}
2060
1999
  placeholder="${this.placeholder}"
2061
2000
  value="${this.#formatWithUnit(this.value)}" />`;
@@ -2103,6 +2042,8 @@ class FigInputNumber extends HTMLElement {
2103
2042
  this.input.addEventListener("focus", this.#boundFocus);
2104
2043
  this.input.removeEventListener("blur", this.#boundBlur);
2105
2044
  this.input.addEventListener("blur", this.#boundBlur);
2045
+ this.input.removeEventListener("keydown", this.#boundKeyDown);
2046
+ this.input.addEventListener("keydown", this.#boundKeyDown);
2106
2047
  });
2107
2048
  }
2108
2049
 
@@ -2112,6 +2053,7 @@ class FigInputNumber extends HTMLElement {
2112
2053
  this.input.removeEventListener("input", this.#boundInput);
2113
2054
  this.input.removeEventListener("focus", this.#boundFocus);
2114
2055
  this.input.removeEventListener("blur", this.#boundBlur);
2056
+ this.input.removeEventListener("keydown", this.#boundKeyDown);
2115
2057
  }
2116
2058
  this.removeEventListener("pointerdown", this.#boundMouseDown);
2117
2059
  window.removeEventListener("pointermove", this.#boundMouseMove);
@@ -2203,6 +2145,35 @@ class FigInputNumber extends HTMLElement {
2203
2145
  );
2204
2146
  }
2205
2147
 
2148
+ #handleKeyDown(e) {
2149
+ if (this.disabled) return;
2150
+
2151
+ // Only handle arrow up/down
2152
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
2153
+
2154
+ e.preventDefault();
2155
+
2156
+ const step = this.step || 1;
2157
+ // Shift multiplies step by 10
2158
+ const multiplier = e.shiftKey ? 10 : 1;
2159
+ const delta = step * multiplier * (e.key === "ArrowUp" ? 1 : -1);
2160
+
2161
+ let numericValue = this.#getNumericValue(this.input.value);
2162
+ let value =
2163
+ (numericValue !== "" ? Number(numericValue) / (this.transform || 1) : 0) +
2164
+ delta;
2165
+ value = this.#sanitizeInput(value, false);
2166
+ this.value = value;
2167
+ this.input.value = this.#formatWithUnit(this.value);
2168
+
2169
+ this.dispatchEvent(
2170
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
2171
+ );
2172
+ this.dispatchEvent(
2173
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
2174
+ );
2175
+ }
2176
+
2206
2177
  #handleInput(e) {
2207
2178
  let numericValue = this.#getNumericValue(e.target.value);
2208
2179
  if (numericValue !== "") {
@@ -2305,6 +2276,7 @@ class FigInputNumber extends HTMLElement {
2305
2276
  "name",
2306
2277
  "units",
2307
2278
  "unit-position",
2279
+ "steppers",
2308
2280
  ];
2309
2281
  }
2310
2282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.10.1",
3
+ "version": "2.10.3",
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",