@rogieking/figui3 6.8.5 → 6.8.7

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
@@ -35,6 +35,65 @@ function figNextFrame(host, callback) {
35
35
  });
36
36
  }
37
37
 
38
+ function figNormalizeTextOnlyInputSlots(host) {
39
+ host
40
+ .querySelectorAll(':scope > [slot="prepend"], :scope > [slot="append"]')
41
+ .forEach((node) => {
42
+ if (node.children.length > 0) {
43
+ figClearInputSlotTooltip(node);
44
+ return;
45
+ }
46
+ const text = node.textContent.trim();
47
+ if (!text) {
48
+ figClearInputSlotTooltip(node);
49
+ return;
50
+ }
51
+ const previousFullText = node.dataset.figInputSlotFullText || "";
52
+ const previousShortText = previousFullText
53
+ ? Array.from(previousFullText)[0]?.toUpperCase()
54
+ : "";
55
+ const fullText = text === previousShortText ? previousFullText : text;
56
+ const first = Array.from(text)[0];
57
+ const normalized = first.toUpperCase();
58
+ if (node.textContent !== normalized) node.textContent = normalized;
59
+ if (fullText && fullText !== normalized) {
60
+ node.dataset.figInputSlotFullText = fullText;
61
+ node.setAttribute("aria-label", fullText);
62
+ figBindInputSlotTooltip(node);
63
+ } else {
64
+ figClearInputSlotTooltip(node);
65
+ }
66
+ });
67
+ }
68
+
69
+ function figBindInputSlotTooltip(node) {
70
+ if (node.dataset.figInputSlotTooltipBound === "true") return;
71
+ node.dataset.figInputSlotTooltipBound = "true";
72
+ node.addEventListener("pointerenter", figShowInputSlotTooltip);
73
+ node.addEventListener("pointerleave", figHideInputSlotTooltip);
74
+ node.addEventListener("focus", figShowInputSlotTooltip);
75
+ node.addEventListener("blur", figHideInputSlotTooltip);
76
+ }
77
+
78
+ function figClearInputSlotTooltip(node) {
79
+ figHideInputSlotTooltip({ currentTarget: node });
80
+ delete node.dataset.figInputSlotFullText;
81
+ node.removeAttribute("aria-label");
82
+ }
83
+
84
+ function figShowInputSlotTooltip(event) {
85
+ const node = event.currentTarget;
86
+ const text = node?.dataset?.figInputSlotFullText;
87
+ if (!node || !text) return;
88
+ FigTooltip.show(node, text);
89
+ }
90
+
91
+ function figHideInputSlotTooltip(event) {
92
+ const node = event.currentTarget;
93
+ if (!node) return;
94
+ FigTooltip.hide(node);
95
+ }
96
+
38
97
  function createFigOverflowButtons({
39
98
  owner,
40
99
  onStart,
@@ -751,6 +810,7 @@ class FigTooltip extends HTMLElement {
751
810
  static #warmupWindow = 1000;
752
811
  static #hoverOpen = null;
753
812
  static #documentExitListenersReady = false;
813
+ static #programmaticAnchors = new Set();
754
814
 
755
815
  #boundHideOnChromeOpen;
756
816
  #boundHidePopupOutsideClick;
@@ -958,6 +1018,7 @@ class FigTooltip extends HTMLElement {
958
1018
 
959
1019
  destroy() {
960
1020
  if (this.popup) {
1021
+ this.#removeDescribedBy(this.popup.id);
961
1022
  this.popup.hidePopup?.();
962
1023
  this.popup.remove();
963
1024
  this.popup = null;
@@ -970,6 +1031,18 @@ class FigTooltip extends HTMLElement {
970
1031
  );
971
1032
  }
972
1033
  }
1034
+ #removeDescribedBy(id) {
1035
+ const trigger = this.firstElementChild;
1036
+ if (!trigger || !id) return;
1037
+ const value = trigger.getAttribute("aria-describedby");
1038
+ if (!value) return;
1039
+ const next = value
1040
+ .split(/\s+/)
1041
+ .filter((token) => token && token !== id)
1042
+ .join(" ");
1043
+ if (next) trigger.setAttribute("aria-describedby", next);
1044
+ else trigger.removeAttribute("aria-describedby");
1045
+ }
973
1046
  isTouchDevice() {
974
1047
  return (
975
1048
  "ontouchstart" in window ||
@@ -1206,6 +1279,9 @@ class FigTooltip extends HTMLElement {
1206
1279
  const handlePointerLeftDocument = () => {
1207
1280
  FigTooltip.#dismissHoverTooltipsOnDocumentExit();
1208
1281
  };
1282
+ const handleViewportChange = () => {
1283
+ FigTooltip.#dismissTooltipsOnViewportChange();
1284
+ };
1209
1285
 
1210
1286
  document.documentElement.addEventListener(
1211
1287
  "mouseleave",
@@ -1215,6 +1291,17 @@ class FigTooltip extends HTMLElement {
1215
1291
  if (event.relatedTarget) return;
1216
1292
  handlePointerLeftDocument();
1217
1293
  });
1294
+ document.addEventListener("scroll", handleViewportChange, {
1295
+ capture: true,
1296
+ passive: true,
1297
+ });
1298
+ window.addEventListener("scroll", handleViewportChange, { passive: true });
1299
+ window.visualViewport?.addEventListener("scroll", handleViewportChange, {
1300
+ passive: true,
1301
+ });
1302
+ window.visualViewport?.addEventListener("resize", handleViewportChange, {
1303
+ passive: true,
1304
+ });
1218
1305
 
1219
1306
  // Same-origin embed: leaving the iframe element (e.g. into a parent dialog).
1220
1307
  try {
@@ -1235,15 +1322,26 @@ class FigTooltip extends HTMLElement {
1235
1322
  for (const node of document.querySelectorAll("fig-tooltip")) {
1236
1323
  if (!(node instanceof FigTooltip)) continue;
1237
1324
  if (node.action !== "hover") continue;
1238
- if (node.hasAttribute("show") && node.getAttribute("show") !== "false")
1239
- continue;
1325
+ if (node.#showPersisted) continue;
1240
1326
  if (node.isOpen || node.timeout) node.hidePopup();
1241
1327
  }
1242
1328
  }
1243
1329
 
1244
1330
  static #programmatic = new WeakMap();
1245
1331
 
1332
+ static #dismissTooltipsOnViewportChange() {
1333
+ for (const node of document.querySelectorAll("fig-tooltip")) {
1334
+ if (!(node instanceof FigTooltip)) continue;
1335
+ if (node.#showPersisted) continue;
1336
+ if (node.isOpen || node.timeout) node.hidePopup();
1337
+ }
1338
+ for (const anchor of Array.from(FigTooltip.#programmaticAnchors)) {
1339
+ FigTooltip.hide(anchor);
1340
+ }
1341
+ }
1342
+
1246
1343
  static show(anchor, text, options = {}) {
1344
+ FigTooltip.#ensureDocumentExitListeners();
1247
1345
  FigTooltip.hide(anchor);
1248
1346
  const delay = options.delay ?? 500;
1249
1347
  const warm =
@@ -1252,6 +1350,7 @@ class FigTooltip extends HTMLElement {
1252
1350
 
1253
1351
  const state = { timeout: null, popup: null };
1254
1352
  FigTooltip.#programmatic.set(anchor, state);
1353
+ FigTooltip.#programmaticAnchors.add(anchor);
1255
1354
 
1256
1355
  state.timeout = setTimeout(() => {
1257
1356
  const supportsPopover =
@@ -1292,8 +1391,12 @@ class FigTooltip extends HTMLElement {
1292
1391
  const state = FigTooltip.#programmatic.get(anchor);
1293
1392
  if (!state) return;
1294
1393
  clearTimeout(state.timeout);
1295
- if (state.popup) state.popup.remove();
1394
+ if (state.popup) {
1395
+ state.popup.remove();
1396
+ FigTooltip.#lastHiddenAt = Date.now();
1397
+ }
1296
1398
  FigTooltip.#programmatic.delete(anchor);
1399
+ FigTooltip.#programmaticAnchors.delete(anchor);
1297
1400
  }
1298
1401
  }
1299
1402
 
@@ -5424,6 +5527,9 @@ class FigInputText extends HTMLElement {
5424
5527
  #boundInputChange;
5425
5528
  #boundNativeInput;
5426
5529
  #boundFocusControl;
5530
+ #boundAdornmentClick;
5531
+ #mutationObserver = null;
5532
+ #syncRaf = 0;
5427
5533
  #a11yAttributes = [
5428
5534
  "aria-label",
5429
5535
  "aria-labelledby",
@@ -5447,6 +5553,7 @@ class FigInputText extends HTMLElement {
5447
5553
  this.#syncSearchClearVisibility();
5448
5554
  };
5449
5555
  this.#boundFocusControl = this.focus.bind(this);
5556
+ this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
5450
5557
  }
5451
5558
 
5452
5559
  connectedCallback() {
@@ -5482,6 +5589,8 @@ class FigInputText extends HTMLElement {
5482
5589
  this.#syncSearchClear();
5483
5590
  this.#syncSearchClearVisibility();
5484
5591
  this.#syncPasswordToggle();
5592
+ figNormalizeTextOnlyInputSlots(this);
5593
+ this.#startObserver();
5485
5594
 
5486
5595
  if (this.type === "number") {
5487
5596
  if (this.getAttribute("min")) {
@@ -5495,6 +5604,8 @@ class FigInputText extends HTMLElement {
5495
5604
  }
5496
5605
  this.addEventListener("pointerdown", this.#boundMouseDown);
5497
5606
  }
5607
+ this.removeEventListener("click", this.#boundAdornmentClick);
5608
+ this.addEventListener("click", this.#boundAdornmentClick);
5498
5609
  this.input.removeEventListener("change", this.#boundInputChange);
5499
5610
  this.input.addEventListener("change", this.#boundInputChange);
5500
5611
  this.input.removeEventListener("input", this.#boundNativeInput);
@@ -5506,10 +5617,17 @@ class FigInputText extends HTMLElement {
5506
5617
  this.input.removeEventListener("change", this.#boundInputChange);
5507
5618
  this.input.removeEventListener("input", this.#boundNativeInput);
5508
5619
  }
5620
+ this.removeEventListener("click", this.#boundAdornmentClick);
5509
5621
  this.removeEventListener("pointerdown", this.#boundMouseDown);
5510
5622
  window.removeEventListener("pointermove", this.#boundMouseMove);
5511
5623
  window.removeEventListener("pointerup", this.#boundMouseUp);
5512
5624
  window.removeEventListener("blur", this.#boundWindowBlur);
5625
+ this.#mutationObserver?.disconnect();
5626
+ this.#mutationObserver = null;
5627
+ if (this.#syncRaf) {
5628
+ cancelAnimationFrame(this.#syncRaf);
5629
+ this.#syncRaf = 0;
5630
+ }
5513
5631
  }
5514
5632
 
5515
5633
  focus() {
@@ -5523,37 +5641,61 @@ class FigInputText extends HTMLElement {
5523
5641
  ? existing.tagName === "TEXTAREA"
5524
5642
  : existing.tagName === "INPUT";
5525
5643
  if (matches) return existing;
5644
+ existing.remove();
5526
5645
  }
5527
5646
 
5528
- let html = `<input
5529
- type="${this.type}"
5530
- ${this.name ? `name="${this.name}"` : ""}
5531
- placeholder="${this.placeholder}"
5532
- value="${
5533
- this.type === "number" ? this.#transformNumber(this.value) : this.value
5534
- }" />`;
5647
+ let control;
5535
5648
  if (wantsTextarea) {
5536
- html = `<textarea
5537
- placeholder="${this.placeholder}">${this.value}</textarea>`;
5649
+ control = document.createElement("textarea");
5650
+ control.value = this.value;
5651
+ } else {
5652
+ control = document.createElement("input");
5653
+ control.type = this.type;
5654
+ control.value =
5655
+ this.type === "number" ? this.#transformNumber(this.value) : this.value;
5538
5656
  }
5657
+ control.setAttribute("data-generated", "input-control");
5658
+ if (this.name) control.name = this.name;
5659
+ control.placeholder = this.placeholder;
5660
+ this.insertBefore(control, this.querySelector('[slot="append"]'));
5539
5661
 
5540
- const append = this.querySelector("[slot=append]");
5541
- const prepend = this.querySelector("[slot=prepend]");
5542
-
5543
- this.innerHTML = html;
5544
-
5545
- if (prepend) {
5546
- prepend.removeEventListener("click", this.#boundFocusControl);
5547
- prepend.addEventListener("click", this.#boundFocusControl);
5548
- this.prepend(prepend);
5549
- }
5550
- if (append) {
5551
- append.removeEventListener("click", this.#boundFocusControl);
5552
- append.addEventListener("click", this.#boundFocusControl);
5553
- this.append(append);
5662
+ return control;
5663
+ }
5664
+ #handleAdornmentClick(event) {
5665
+ const adornment = event.target?.closest?.("[slot]");
5666
+ if (!adornment || adornment.parentElement !== this) return;
5667
+ this.focus();
5668
+ }
5669
+ #startObserver() {
5670
+ this.#mutationObserver?.disconnect();
5671
+ this.#mutationObserver = new MutationObserver(() => this.#scheduleLightDomSync());
5672
+ this.#mutationObserver.observe(this, {
5673
+ childList: true,
5674
+ characterData: true,
5675
+ subtree: true,
5676
+ });
5677
+ }
5678
+ #scheduleLightDomSync() {
5679
+ if (this.#syncRaf) return;
5680
+ this.#syncRaf = requestAnimationFrame(() => {
5681
+ this.#syncRaf = 0;
5682
+ if (!this.isConnected) return;
5683
+ this.#syncLightDom();
5684
+ });
5685
+ }
5686
+ #syncLightDom() {
5687
+ if (!this.input || !this.contains(this.input)) {
5688
+ this.input = this.#ensureInputControl();
5689
+ this.input.readOnly = this.readonly;
5690
+ this.input.addEventListener("change", this.#boundInputChange);
5691
+ this.input.addEventListener("input", this.#boundNativeInput);
5554
5692
  }
5555
-
5556
- return this.querySelector("input,textarea");
5693
+ this.#syncInputA11yAttributes();
5694
+ this.#syncSearchPrefix();
5695
+ this.#syncSearchClear();
5696
+ this.#syncSearchClearVisibility();
5697
+ this.#syncPasswordToggle();
5698
+ figNormalizeTextOnlyInputSlots(this);
5557
5699
  }
5558
5700
  #syncInputA11yAttributes() {
5559
5701
  if (!this.input) return;
@@ -5574,8 +5716,11 @@ class FigInputText extends HTMLElement {
5574
5716
  generated?.remove();
5575
5717
  return;
5576
5718
  }
5577
- const prepend = this.querySelector('[slot="prepend"]');
5578
- if (prepend && prepend !== generated) return;
5719
+ const prepend = this.querySelector('[slot="prepend"]:not([data-generated])');
5720
+ if (prepend) {
5721
+ generated?.remove();
5722
+ return;
5723
+ }
5579
5724
  if (generated) {
5580
5725
  const icon = generated.querySelector("fig-icon");
5581
5726
  if (icon && icon.getAttribute("name") !== "search") {
@@ -5599,8 +5744,11 @@ class FigInputText extends HTMLElement {
5599
5744
  generated?.remove();
5600
5745
  return;
5601
5746
  }
5602
- const append = this.querySelector('[slot="append"]');
5603
- if (append && append !== generated) return;
5747
+ const append = this.querySelector('[slot="append"]:not([data-generated])');
5748
+ if (append) {
5749
+ generated?.remove();
5750
+ return;
5751
+ }
5604
5752
  if (generated) {
5605
5753
  const icon = generated.querySelector("fig-icon");
5606
5754
  if (icon && icon.getAttribute("name") !== "close") {
@@ -5659,8 +5807,11 @@ class FigInputText extends HTMLElement {
5659
5807
  this.#passwordVisible = false;
5660
5808
  return;
5661
5809
  }
5662
- const append = this.querySelector('[slot="append"]');
5663
- if (append && append !== generated) return;
5810
+ const append = this.querySelector('[slot="append"]:not([data-generated])');
5811
+ if (append) {
5812
+ generated?.remove();
5813
+ return;
5814
+ }
5664
5815
  if (generated) {
5665
5816
  this.#updatePasswordToggle(generated);
5666
5817
  return;
@@ -5930,6 +6081,7 @@ class FigInputNumber extends HTMLElement {
5930
6081
  #boundBlur;
5931
6082
  #boundKeyDown;
5932
6083
  #boundFocusControl;
6084
+ #boundAdornmentClick;
5933
6085
  #units;
5934
6086
  #rawUnits;
5935
6087
  #unitsDisallow;
@@ -5937,6 +6089,8 @@ class FigInputNumber extends HTMLElement {
5937
6089
  #precision;
5938
6090
  #isInteracting = false;
5939
6091
  #stepperEl = null;
6092
+ #mutationObserver = null;
6093
+ #syncRaf = 0;
5940
6094
  #a11yAttributes = [
5941
6095
  "aria-label",
5942
6096
  "aria-labelledby",
@@ -6056,6 +6210,7 @@ class FigInputNumber extends HTMLElement {
6056
6210
  this.#handleKeyDown(e);
6057
6211
  };
6058
6212
  this.#boundFocusControl = this.focus.bind(this);
6213
+ this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
6059
6214
  }
6060
6215
 
6061
6216
  connectedCallback() {
@@ -6114,8 +6269,12 @@ class FigInputNumber extends HTMLElement {
6114
6269
  }
6115
6270
  this.#syncStepperState();
6116
6271
  this.#syncSpinbuttonAria();
6272
+ figNormalizeTextOnlyInputSlots(this);
6117
6273
 
6118
6274
  this.addEventListener("pointerdown", this.#boundMouseDown);
6275
+ this.removeEventListener("click", this.#boundAdornmentClick);
6276
+ this.addEventListener("click", this.#boundAdornmentClick);
6277
+ this.#startObserver();
6119
6278
  this.input.removeEventListener("change", this.#boundInputChange);
6120
6279
  this.input.addEventListener("change", this.#boundInputChange);
6121
6280
  this.input.removeEventListener("input", this.#boundInput);
@@ -6136,10 +6295,17 @@ class FigInputNumber extends HTMLElement {
6136
6295
  this.input.removeEventListener("blur", this.#boundBlur);
6137
6296
  this.input.removeEventListener("keydown", this.#boundKeyDown);
6138
6297
  }
6298
+ this.removeEventListener("click", this.#boundAdornmentClick);
6139
6299
  this.removeEventListener("pointerdown", this.#boundMouseDown);
6140
6300
  window.removeEventListener("pointermove", this.#boundMouseMove);
6141
6301
  window.removeEventListener("pointerup", this.#boundMouseUp);
6142
6302
  window.removeEventListener("blur", this.#boundWindowBlur);
6303
+ this.#mutationObserver?.disconnect();
6304
+ this.#mutationObserver = null;
6305
+ if (this.#syncRaf) {
6306
+ cancelAnimationFrame(this.#syncRaf);
6307
+ this.#syncRaf = 0;
6308
+ }
6143
6309
  }
6144
6310
 
6145
6311
  focus() {
@@ -6150,30 +6316,62 @@ class FigInputNumber extends HTMLElement {
6150
6316
  const existing = this.querySelector("input");
6151
6317
  if (existing) return existing;
6152
6318
 
6153
- const html = `<input
6154
- type="text"
6155
- inputmode="decimal"
6156
- ${this.name ? `name="${this.name}"` : ""}
6157
- placeholder="${this.placeholder}"
6158
- value="${this.#formatWithUnit(this.value)}" />`;
6319
+ const input = document.createElement("input");
6320
+ input.type = "text";
6321
+ input.inputMode = "decimal";
6322
+ input.setAttribute("data-generated", "input-control");
6323
+ if (this.name) input.name = this.name;
6324
+ input.placeholder = this.placeholder;
6325
+ input.value = this.#formatWithUnit(this.value);
6326
+ this.insertBefore(input, this.querySelector('[slot="append"]'));
6159
6327
 
6160
- const append = this.querySelector("[slot=append]");
6161
- const prepend = this.querySelector("[slot=prepend]");
6328
+ return input;
6329
+ }
6162
6330
 
6163
- this.innerHTML = html;
6331
+ #handleAdornmentClick(event) {
6332
+ const adornment = event.target?.closest?.("[slot]");
6333
+ if (!adornment || adornment.parentElement !== this) return;
6334
+ this.focus();
6335
+ }
6164
6336
 
6165
- if (prepend) {
6166
- prepend.removeEventListener("click", this.#boundFocusControl);
6167
- prepend.addEventListener("click", this.#boundFocusControl);
6168
- this.prepend(prepend);
6337
+ #startObserver() {
6338
+ this.#mutationObserver?.disconnect();
6339
+ this.#mutationObserver = new MutationObserver(() => this.#scheduleLightDomSync());
6340
+ this.#mutationObserver.observe(this, {
6341
+ childList: true,
6342
+ characterData: true,
6343
+ subtree: true,
6344
+ });
6345
+ }
6346
+
6347
+ #scheduleLightDomSync() {
6348
+ if (this.#syncRaf) return;
6349
+ this.#syncRaf = requestAnimationFrame(() => {
6350
+ this.#syncRaf = 0;
6351
+ if (!this.isConnected) return;
6352
+ this.#syncLightDom();
6353
+ });
6354
+ }
6355
+
6356
+ #syncLightDom() {
6357
+ if (!this.input || !this.contains(this.input)) {
6358
+ this.input = this.#ensureInputControl();
6359
+ this.input.addEventListener("change", this.#boundInputChange);
6360
+ this.input.addEventListener("input", this.#boundInput);
6361
+ this.input.addEventListener("focus", this.#boundFocus);
6362
+ this.input.addEventListener("blur", this.#boundBlur);
6363
+ this.input.addEventListener("keydown", this.#boundKeyDown);
6169
6364
  }
6170
- if (append) {
6171
- append.removeEventListener("click", this.#boundFocusControl);
6172
- append.addEventListener("click", this.#boundFocusControl);
6173
- this.append(append);
6365
+ const hasSteppers =
6366
+ this.hasAttribute("steppers") &&
6367
+ this.getAttribute("steppers") !== "false";
6368
+ if (this.#stepperEl && !this.contains(this.#stepperEl)) {
6369
+ this.#stepperEl = null;
6174
6370
  }
6175
-
6176
- return this.querySelector("input");
6371
+ this.#syncSteppers(hasSteppers);
6372
+ this.#syncInputA11yAttributes();
6373
+ this.#syncSpinbuttonAria();
6374
+ figNormalizeTextOnlyInputSlots(this);
6177
6375
  }
6178
6376
 
6179
6377
  #syncInputA11yAttributes() {
@@ -13963,6 +14161,7 @@ class FigInputJoystick extends HTMLElement {
13963
14161
  const y = this.coordinates === "math" ? 1 - yScreen : yScreen;
13964
14162
  this.position = { x, y };
13965
14163
  if (syncHandle) this.#syncHandlePosition();
14164
+ this.#syncFields();
13966
14165
  this.#syncValueAttribute();
13967
14166
  }
13968
14167
 
@@ -14019,7 +14218,11 @@ class FigInputJoystick extends HTMLElement {
14019
14218
  if (this.cursor) {
14020
14219
  this.cursor.value = `${this.position.x * 100}% ${displayY * 100}%`;
14021
14220
  }
14022
- // Also sync text inputs if they exist (convert to percentage 0-100)
14221
+ this.#syncFields();
14222
+ }
14223
+
14224
+ // Sync the X/Y number fields to the current position (percentage 0-100).
14225
+ #syncFields() {
14023
14226
  if (this.#fieldsEnabled && this.xInput && this.yInput) {
14024
14227
  this.xInput.setAttribute("value", Math.round(this.position.x * 100));
14025
14228
  this.yInput.setAttribute("value", Math.round(this.position.y * 100));
@@ -15101,6 +15304,7 @@ class FigChooser extends HTMLElement {
15101
15304
  }
15102
15305
  if (name === "columns") {
15103
15306
  this.#syncGridColumns();
15307
+ this.#resettleSelectedChoice();
15104
15308
  }
15105
15309
  if (name === "drag") {
15106
15310
  if (this.#dragEnabled) {
@@ -15111,10 +15315,16 @@ class FigChooser extends HTMLElement {
15111
15315
  }
15112
15316
  if (name === "overflow") {
15113
15317
  this.#applyOverflowMode();
15318
+ requestAnimationFrame(() => this.#syncOverflow());
15114
15319
  }
15115
15320
  if (name === "layout") {
15116
15321
  this.#applyOverflowMode();
15117
- requestAnimationFrame(() => this.#syncOverflow());
15322
+ if (newValue === "horizontal") {
15323
+ this.scrollTop = 0;
15324
+ } else {
15325
+ this.scrollLeft = 0;
15326
+ }
15327
+ this.#resettleSelectedChoice();
15118
15328
  }
15119
15329
  }
15120
15330
 
@@ -15260,6 +15470,7 @@ class FigChooser extends HTMLElement {
15260
15470
  this.#resizeObserver?.disconnect();
15261
15471
  this.#resizeObserver = new ResizeObserver(() => {
15262
15472
  this.#syncOverflow();
15473
+ this.#resettleSelectedChoice();
15263
15474
  });
15264
15475
  this.#resizeObserver.observe(this);
15265
15476
  }
@@ -15447,6 +15658,11 @@ class FigChooser extends HTMLElement {
15447
15658
  figScrollOverflowPage(this, isHorizontal ? "x" : "y", direction);
15448
15659
  }
15449
15660
 
15661
+ #resettleSelectedChoice() {
15662
+ if (!this.#selectedChoice) return;
15663
+ this.#scrollToChoice(this.#selectedChoice, "auto");
15664
+ }
15665
+
15450
15666
  #scrollToChoice(el, behavior = "smooth") {
15451
15667
  if (!el) return;
15452
15668
  requestAnimationFrame(() => {
@@ -15459,58 +15675,25 @@ class FigChooser extends HTMLElement {
15459
15675
  const hostRect = this.getBoundingClientRect();
15460
15676
  const options = { behavior };
15461
15677
  let shouldScroll = false;
15462
- const threshold = 2;
15463
15678
 
15464
15679
  if (overflowY) {
15465
- const fullyVisible =
15466
- choiceRect.top >= hostRect.top - 1 &&
15467
- choiceRect.bottom <= hostRect.bottom + 1;
15468
- const topVisible = choiceRect.top >= hostRect.top - 1;
15469
- const bottomVisible = choiceRect.bottom <= hostRect.bottom + 1;
15470
- const atScrollStart = this.scrollTop <= threshold;
15471
- const needsScroll =
15472
- !fullyVisible &&
15473
- (!topVisible ||
15474
- (!bottomVisible && !atScrollStart) ||
15475
- choiceRect.top >= hostRect.bottom - 1);
15476
- if (needsScroll) {
15477
- const choiceTop = choiceRect.top - hostRect.top + this.scrollTop;
15478
- const maxScroll = this.scrollHeight - this.clientHeight;
15479
- options.top = Math.max(
15480
- 0,
15481
- Math.min(
15482
- choiceTop + choiceRect.height / 2 - this.clientHeight / 2,
15483
- maxScroll,
15484
- ),
15485
- );
15486
- shouldScroll = true;
15487
- }
15680
+ const choiceTop = choiceRect.top - hostRect.top + this.scrollTop;
15681
+ const maxScroll = this.scrollHeight - this.clientHeight;
15682
+ options.top = Math.max(
15683
+ 0,
15684
+ Math.min(choiceTop + choiceRect.height / 2 - this.clientHeight / 2, maxScroll),
15685
+ );
15686
+ shouldScroll = true;
15488
15687
  }
15489
15688
 
15490
15689
  if (overflowX) {
15491
- const fullyVisible =
15492
- choiceRect.left >= hostRect.left - 1 &&
15493
- choiceRect.right <= hostRect.right + 1;
15494
- const startVisible = choiceRect.left >= hostRect.left - 1;
15495
- const endVisible = choiceRect.right <= hostRect.right + 1;
15496
- const atScrollStart = this.scrollLeft <= threshold;
15497
- const needsScroll =
15498
- !fullyVisible &&
15499
- (!startVisible ||
15500
- (!endVisible && !atScrollStart) ||
15501
- choiceRect.left >= hostRect.right - 1);
15502
- if (needsScroll) {
15503
- const choiceLeft = choiceRect.left - hostRect.left + this.scrollLeft;
15504
- const maxScroll = this.scrollWidth - this.clientWidth;
15505
- options.left = Math.max(
15506
- 0,
15507
- Math.min(
15508
- choiceLeft + choiceRect.width / 2 - this.clientWidth / 2,
15509
- maxScroll,
15510
- ),
15511
- );
15512
- shouldScroll = true;
15513
- }
15690
+ const choiceLeft = choiceRect.left - hostRect.left + this.scrollLeft;
15691
+ const maxScroll = this.scrollWidth - this.clientWidth;
15692
+ options.left = Math.max(
15693
+ 0,
15694
+ Math.min(choiceLeft + choiceRect.width / 2 - this.clientWidth / 2, maxScroll),
15695
+ );
15696
+ shouldScroll = true;
15514
15697
  }
15515
15698
 
15516
15699
  if (shouldScroll) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.8.5",
3
+ "version": "6.8.7",
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",