@rogieking/figui3 6.20.2 → 6.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fig-lab.css CHANGED
@@ -137,27 +137,33 @@ fig-content:has(> propskit-group[name]:last-child) {
137
137
  padding-bottom: 0;
138
138
  }
139
139
 
140
- /* Large size: more left field padding; nudge trailing controls in from the right.
141
- propskit-select applies left inset on label + trigger button instead (below). */
140
+ /* Default/large size: more left field padding; nudge trailing controls in from
141
+ the right. propskit-select applies left inset on label + trigger instead. */
142
142
  :is(
143
143
  propskit-switch,
144
144
  propskit-color,
145
145
  propskit-text,
146
146
  propskit-number,
147
147
  propskit-slider
148
- )[size="large"] {
148
+ ):is(:not([size]), [size="large"]) {
149
149
  fig-field:not([direction="vertical"]) {
150
150
  padding-left: var(--spacer-2-5);
151
151
  }
152
152
  }
153
153
 
154
154
  /* Flex-positioned numbers — margin works. Absolute numbers use right: calc(...) below. */
155
- :is(propskit-color, propskit-number)[size="large"] {
155
+ :is(propskit-color, propskit-number):is(:not([size]), [size="large"]) {
156
156
  fig-field fig-input-number {
157
157
  margin-right: var(--spacer-1);
158
158
  }
159
159
  }
160
160
 
161
+ propskit-text:is(:not([size]), [size="large"]) {
162
+ fig-field > fig-input-text {
163
+ margin-right: var(--spacer-1);
164
+ }
165
+ }
166
+
161
167
  propskit-switch {
162
168
  --propskit-switch-height: 2rem;
163
169
  --propskit-switch-vertical-padding: var(--spacer-1);
@@ -169,6 +175,7 @@ propskit-switch {
169
175
  padding: var(--propskit-switch-vertical-padding)
170
176
  var(--propskit-switch-horizontal-padding);
171
177
 
178
+ &:not([size]),
172
179
  &[size="large"] {
173
180
  --propskit-switch-vertical-padding: 0px;
174
181
 
@@ -245,6 +252,7 @@ propskit-color {
245
252
  padding: var(--propskit-color-vertical-padding)
246
253
  var(--propskit-color-horizontal-padding);
247
254
 
255
+ &:not([size]),
248
256
  &[size="large"] {
249
257
  --propskit-color-vertical-padding: 0px;
250
258
  }
@@ -368,6 +376,7 @@ propskit-select {
368
376
  padding: var(--propskit-select-vertical-padding)
369
377
  var(--propskit-select-horizontal-padding);
370
378
 
379
+ &:not([size]),
371
380
  &[size="large"] {
372
381
  --propskit-select-vertical-padding: 0px;
373
382
 
@@ -477,6 +486,7 @@ propskit-text {
477
486
  padding: var(--propskit-text-vertical-padding)
478
487
  var(--propskit-text-horizontal-padding);
479
488
 
489
+ &:not([size]),
480
490
  &[size="large"] {
481
491
  --propskit-text-vertical-padding: 0px;
482
492
  }
@@ -586,6 +596,7 @@ propskit-number {
586
596
  padding: var(--propskit-number-vertical-padding)
587
597
  var(--propskit-number-horizontal-padding);
588
598
 
599
+ &:not([size]),
589
600
  &[size="large"] {
590
601
  --propskit-number-vertical-padding: 0px;
591
602
 
@@ -714,6 +725,7 @@ propskit-slider {
714
725
  padding-left: var(--propskit-slider-horizontal-padding);
715
726
  padding-right: var(--propskit-slider-horizontal-padding);
716
727
 
728
+ &:not([size]),
717
729
  &[size="large"] {
718
730
  --propskit-slider-vertical-padding: 0px;
719
731
  --propskit-slider-fig-slider-thumb-height: calc(
@@ -1496,9 +1508,9 @@ fig-select::part(listbox) {
1496
1508
  background-color: var(--fig-popup-bg-color);
1497
1509
  color: var(--figma-color-text-menu);
1498
1510
  color-scheme: dark;
1499
- box-shadow: var(--figma-elevation-100);
1511
+ box-shadow: var(--figma-elevation-400-menu-panel);
1500
1512
  /* width/min/max set in JS — ::part width rules override element.style. */
1501
- max-height: min(20rem, calc(100vh - 1rem));
1513
+ max-height: calc(100vh - 1rem);
1502
1514
  height: max-content;
1503
1515
  }
1504
1516
 
@@ -1512,7 +1524,7 @@ fig-select-options {
1512
1524
  gap: 0;
1513
1525
  width: 100%;
1514
1526
  max-width: 100%;
1515
- max-height: min(20rem, calc(100vh - 1rem));
1527
+ max-height: calc(100vh - 1rem);
1516
1528
  /* No block padding — sticky overflow buttons must sit flush to the popup edges. */
1517
1529
  padding: 0;
1518
1530
  overflow: hidden auto;
@@ -1595,56 +1607,35 @@ fig-select-options {
1595
1607
  & > fig-menu-separator {
1596
1608
  --fig-menu-separator-color: var(--figma-color-border-menu);
1597
1609
  --fig-menu-separator-label-color: var(--figma-color-text-menu-secondary);
1598
- /* Align label with option text (padding + checkmark column). */
1599
- --fig-menu-item-padding: calc(var(--spacer-1) * 2 + var(--spacer-1) + 1rem);
1610
+
1611
+ /* Align label with option text (padding + checkmark column).
1612
+ Negative margins on ::before must match this padding exactly so the
1613
+ rule bleeds to the listbox edges (base rule uses --fig-menu-item-padding). */
1600
1614
  &[label]:not([label=""]) {
1601
1615
  padding-left: var(--spacer-5);
1602
1616
  padding-right: var(--spacer-2);
1617
+
1618
+ &::before {
1619
+ margin-inline: unset;
1620
+ margin-left: calc(-1 * var(--spacer-5));
1621
+ margin-right: calc(-1 * var(--spacer-2));
1622
+ }
1603
1623
  }
1604
1624
  }
1605
1625
  }
1606
1626
 
1607
- /*
1608
- [label]:not([label=""]) {
1609
- display: flex;
1610
- flex-direction: column;
1611
- gap: var(--spacer-2);
1612
- height: auto;
1613
- margin: var(--spacer-2) 0;
1614
- margin-bottom: var(--spacer-1);
1615
- padding: 0 var(--spacer-2) 0 var(--fig-menu-item-padding, var(--spacer-4));
1616
- background: transparent;
1617
-
1618
- &::before {
1619
- content: "";
1620
- display: block;
1621
- height: 1px;
1622
- margin-left: calc(-1 * var(--fig-menu-item-padding, var(--spacer-4)));
1623
- margin-right: calc(-1 * var(--spacer-2));
1624
- background: var(--fig-menu-separator-color);
1625
- }
1626
-
1627
- &::after {
1628
- content: attr(label);
1629
- display: block;
1630
- color: var(--fig-menu-separator-label-color);
1631
- line-height: var(--spacer-4);
1632
- min-height: var(--spacer-4);
1633
- }
1634
- }*/
1635
-
1636
1627
  fig-select-option {
1637
1628
  /* Match fig-dropdown[experimental~="modern"] option + option::checkmark */
1638
1629
  display: flex;
1639
1630
  align-items: center;
1640
- gap: var(--spacer-1);
1631
+ gap: 0;
1641
1632
  position: relative;
1642
- height: var(--spacer-4);
1643
- padding: 0 var(--spacer-2);
1633
+ min-height: var(--spacer-4);
1634
+ height: auto;
1635
+ padding: var(--spacer-1) var(--spacer-2);
1644
1636
  padding-right: var(--spacer-3);
1645
1637
  border-radius: var(--radius-medium);
1646
1638
  color: var(--figma-color-text-menu);
1647
- font-weight: var(--body-medium-fontWeight);
1648
1639
  cursor: default;
1649
1640
  user-select: none;
1650
1641
 
@@ -1678,6 +1669,24 @@ fig-select-option {
1678
1669
  visibility: hidden;
1679
1670
  }
1680
1671
 
1672
+ & > [slot="prepend"] {
1673
+ margin-top: calc(var(--spacer-1) * -1);
1674
+ margin-bottom: calc(var(--spacer-1) * -1);
1675
+ margin-right: var(--spacer-1);
1676
+ }
1677
+
1678
+
1679
+ & > :not([slot="prepend"]):not([slot="append"]) {
1680
+ flex: 1 1 auto;
1681
+ min-width: 0;
1682
+ }
1683
+
1684
+ & > [slot="append"] {
1685
+ flex: 0 0 auto;
1686
+ margin-inline-start: auto;
1687
+ color: var(--figma-color-text-tertiary);
1688
+ }
1689
+
1681
1690
  &:hover,
1682
1691
  &:focus-visible {
1683
1692
  outline: none;
package/fig-lab.js CHANGED
@@ -8,6 +8,12 @@
8
8
  * <script src="fig-lab.js"></script>
9
9
  */
10
10
 
11
+ function figLabDefineElement(name, constructor) {
12
+ if (!customElements.get(name)) {
13
+ customElements.define(name, constructor);
14
+ }
15
+ }
16
+
11
17
  function figLabBooleanAttribute(element, name) {
12
18
  return element.hasAttribute(name) && element.getAttribute(name) !== "false";
13
19
  }
@@ -128,12 +134,12 @@ class PropskitSwitch extends HTMLElement {
128
134
  const reserved = new Set([
129
135
  "label",
130
136
  "direction",
137
+ "size",
131
138
  "oninput",
132
139
  "onchange",
133
140
  "class",
134
141
  "style",
135
142
  "id",
136
- "size",
137
143
  "checked",
138
144
  "value",
139
145
  ]);
@@ -230,7 +236,7 @@ class PropskitSwitch extends HTMLElement {
230
236
  selected?.focus(options);
231
237
  }
232
238
  }
233
- customElements.define("propskit-switch", PropskitSwitch);
239
+ figLabDefineElement("propskit-switch", PropskitSwitch);
234
240
 
235
241
  /* Field + Color wrapper */
236
242
  class PropskitColor extends HTMLElement {
@@ -516,7 +522,7 @@ class PropskitColor extends HTMLElement {
516
522
  this.#input?.querySelector("input:not([tabindex='-1'])")?.focus(options);
517
523
  }
518
524
  }
519
- customElements.define("propskit-color", PropskitColor);
525
+ figLabDefineElement("propskit-color", PropskitColor);
520
526
 
521
527
  /* Field + Select wrapper */
522
528
  class PropskitSelect extends HTMLElement {
@@ -771,7 +777,7 @@ class PropskitSelect extends HTMLElement {
771
777
  this.#select?.focus(options);
772
778
  }
773
779
  }
774
- customElements.define("propskit-select", PropskitSelect);
780
+ figLabDefineElement("propskit-select", PropskitSelect);
775
781
 
776
782
  /* Field + Text wrapper */
777
783
  class PropskitText extends HTMLElement {
@@ -981,7 +987,7 @@ class PropskitText extends HTMLElement {
981
987
  this.#input?.focus(options);
982
988
  }
983
989
  }
984
- customElements.define("propskit-text", PropskitText);
990
+ figLabDefineElement("propskit-text", PropskitText);
985
991
 
986
992
  /* Field + Number wrapper */
987
993
  class PropskitNumber extends HTMLElement {
@@ -1094,6 +1100,7 @@ class PropskitNumber extends HTMLElement {
1094
1100
  const reserved = new Set([
1095
1101
  "label",
1096
1102
  "direction",
1103
+ "size",
1097
1104
  "oninput",
1098
1105
  "onchange",
1099
1106
  "class",
@@ -1180,7 +1187,7 @@ class PropskitNumber extends HTMLElement {
1180
1187
  this.#input?.focus(options);
1181
1188
  }
1182
1189
  }
1183
- customElements.define("propskit-number", PropskitNumber);
1190
+ figLabDefineElement("propskit-number", PropskitNumber);
1184
1191
 
1185
1192
  /* Collapsible property group — always collapsible (no collapsible attr). */
1186
1193
  class PropskitGroup extends HTMLElement {
@@ -1571,7 +1578,7 @@ class PropskitGroup extends HTMLElement {
1571
1578
  }
1572
1579
  }
1573
1580
  }
1574
- customElements.define("propskit-group", PropskitGroup);
1581
+ figLabDefineElement("propskit-group", PropskitGroup);
1575
1582
 
1576
1583
  /* Field + Slider wrapper */
1577
1584
  class PropskitSlider extends HTMLElement {
@@ -2191,7 +2198,7 @@ class PropskitSlider extends HTMLElement {
2191
2198
  this.#resetToDefault();
2192
2199
  }
2193
2200
  }
2194
- customElements.define("propskit-slider", PropskitSlider);
2201
+ figLabDefineElement("propskit-slider", PropskitSlider);
2195
2202
 
2196
2203
  /* Canvas Control */
2197
2204
  class FigCanvasControl extends HTMLElement {
@@ -3488,7 +3495,7 @@ class FigCanvasControl extends HTMLElement {
3488
3495
  }
3489
3496
  }
3490
3497
  }
3491
- customElements.define("fig-canvas-control", FigCanvasControl);
3498
+ figLabDefineElement("fig-canvas-control", FigCanvasControl);
3492
3499
 
3493
3500
  /* Oscillator Input */
3494
3501
  /**
@@ -4390,7 +4397,7 @@ class PropskitOscillator extends HTMLElement {
4390
4397
  );
4391
4398
  }
4392
4399
  }
4393
- customElements.define("propskit-oscillator", PropskitOscillator);
4400
+ figLabDefineElement("propskit-oscillator", PropskitOscillator);
4394
4401
 
4395
4402
  /* Angle Input */
4396
4403
  /**
@@ -4908,7 +4915,7 @@ class FigInputAngle extends HTMLElement {
4908
4915
  }
4909
4916
  }
4910
4917
  }
4911
- customElements.define("fig-input-angle", FigInputAngle);
4918
+ figLabDefineElement("fig-input-angle", FigInputAngle);
4912
4919
 
4913
4920
  /* Reorder wrapper */
4914
4921
  class FigReorder extends HTMLElement {
@@ -5343,7 +5350,7 @@ class FigReorder extends HTMLElement {
5343
5350
  }
5344
5351
  }
5345
5352
 
5346
- customElements.define("fig-reorder", FigReorder);
5353
+ figLabDefineElement("fig-reorder", FigReorder);
5347
5354
 
5348
5355
  /* Select — dropdown-styled trigger + fig-popup listbox */
5349
5356
  let figLabSelectId = 0;
@@ -5384,9 +5391,19 @@ function figLabOptionEntryLabel(opt) {
5384
5391
  return String(opt ?? "");
5385
5392
  }
5386
5393
 
5394
+ /**
5395
+ * A selectable option for fig-select.
5396
+ * Supports light-DOM slots: `slot="prepend"` (leading) and `slot="append"` (trailing).
5397
+ * Use the `label` attribute for the closed-trigger label when option content is rich.
5398
+ *
5399
+ * @attr {string} value - Option value
5400
+ * @attr {string} label - Optional display label for the select trigger
5401
+ * @attr {boolean} disabled - Whether the option is disabled
5402
+ * @attr {boolean} selected - Whether the option is selected
5403
+ */
5387
5404
  class FigSelectOption extends HTMLElement {
5388
5405
  static get observedAttributes() {
5389
- return ["value", "disabled", "selected"];
5406
+ return ["value", "disabled", "selected", "label"];
5390
5407
  }
5391
5408
 
5392
5409
  get value() {
@@ -5443,7 +5460,7 @@ class FigSelectOption extends HTMLElement {
5443
5460
  }
5444
5461
  }
5445
5462
  }
5446
- customElements.define("fig-select-option", FigSelectOption);
5463
+ figLabDefineElement("fig-select-option", FigSelectOption);
5447
5464
 
5448
5465
  function figLabSyncOverflowState(host, scrollEl, threshold = 2) {
5449
5466
  if (!host || !scrollEl) return false;
@@ -5586,7 +5603,7 @@ class FigSelectOptions extends HTMLElement {
5586
5603
  this.classList.remove("overflow-start", "overflow-end");
5587
5604
  }
5588
5605
  }
5589
- customElements.define("fig-select-options", FigSelectOptions);
5606
+ figLabDefineElement("fig-select-options", FigSelectOptions);
5590
5607
 
5591
5608
  class FigSelect extends HTMLElement {
5592
5609
  #button = null;
@@ -5599,8 +5616,14 @@ class FigSelect extends HTMLElement {
5599
5616
  #syncingValue = false;
5600
5617
  #popupPositionPatched = false;
5601
5618
  #originalPositionPopup = null;
5602
- /** After open align, stop repositioning so overflow scroll isn't yanked back. */
5619
+ /**
5620
+ * After open align, ignore content/scroll-driven positionPopup passes so
5621
+ * overflow paging isn't yanked back. Still realign when the trigger moves
5622
+ * or the viewport size changes (window resize, layout shift, page scroll).
5623
+ */
5603
5624
  #freezeMenuPosition = false;
5625
+ #frozenLabelRect = null;
5626
+ #frozenViewport = null;
5604
5627
  #syncingOptions = false;
5605
5628
  #boundTriggerClick = this.#handleTriggerClick.bind(this);
5606
5629
  #boundOptionClick = this.#handleOptionClick.bind(this);
@@ -5871,6 +5894,12 @@ class FigSelect extends HTMLElement {
5871
5894
  popup.setAttribute("part", "listbox");
5872
5895
  popup.setAttribute("theme", "menu");
5873
5896
  popup.setAttribute("role", "listbox");
5897
+ // Top-layer via popover so the menu escapes ancestor contain/overflow
5898
+ // (e.g. fig-fill-picker-dialog). Stays in shadow so option slots still work —
5899
+ // unlike tooltips, we cannot portal this popup to the overlay root.
5900
+ if ("popover" in HTMLElement.prototype) {
5901
+ popup.setAttribute("popover", "manual");
5902
+ }
5874
5903
  popup.id = figLabUniqueId("fig-select-list");
5875
5904
  button.setAttribute("aria-controls", popup.id);
5876
5905
 
@@ -5930,10 +5959,73 @@ class FigSelect extends HTMLElement {
5930
5959
  return { top: 8, right: 8, bottom: 8, left: 8 };
5931
5960
  }
5932
5961
 
5962
+ #readLabelRectSnapshot() {
5963
+ const rect = this.#labelEl?.getBoundingClientRect();
5964
+ if (!rect) return null;
5965
+ return {
5966
+ x: rect.x,
5967
+ y: rect.y,
5968
+ width: rect.width,
5969
+ height: rect.height,
5970
+ };
5971
+ }
5972
+
5973
+ #readViewportSnapshot() {
5974
+ const vv = window.visualViewport;
5975
+ return {
5976
+ width: vv?.width ?? window.innerWidth,
5977
+ height: vv?.height ?? window.innerHeight,
5978
+ offsetLeft: vv?.offsetLeft ?? 0,
5979
+ offsetTop: vv?.offsetTop ?? 0,
5980
+ };
5981
+ }
5982
+
5983
+ #rectSnapshotChanged(prev, next, epsilon = 0.25) {
5984
+ if (!prev && !next) return false;
5985
+ if (!prev || !next) return true;
5986
+ return (
5987
+ Math.abs(prev.x - next.x) > epsilon ||
5988
+ Math.abs(prev.y - next.y) > epsilon ||
5989
+ Math.abs(prev.width - next.width) > epsilon ||
5990
+ Math.abs(prev.height - next.height) > epsilon
5991
+ );
5992
+ }
5993
+
5994
+ #viewportSnapshotChanged(prev, next, epsilon = 0.25) {
5995
+ if (!prev && !next) return false;
5996
+ if (!prev || !next) return true;
5997
+ return (
5998
+ Math.abs(prev.width - next.width) > epsilon ||
5999
+ Math.abs(prev.height - next.height) > epsilon ||
6000
+ Math.abs(prev.offsetLeft - next.offsetLeft) > epsilon ||
6001
+ Math.abs(prev.offsetTop - next.offsetTop) > epsilon
6002
+ );
6003
+ }
6004
+
6005
+ #shouldSkipFrozenPositionPass() {
6006
+ if (!this.#freezeMenuPosition) return false;
6007
+ const labelMoved = this.#rectSnapshotChanged(
6008
+ this.#frozenLabelRect,
6009
+ this.#readLabelRectSnapshot(),
6010
+ );
6011
+ const viewportChanged = this.#viewportSnapshotChanged(
6012
+ this.#frozenViewport,
6013
+ this.#readViewportSnapshot(),
6014
+ );
6015
+ // Skip only when neither the trigger nor the viewport moved — typical of
6016
+ // overflow scroll / content sync fighting the open-time alignment.
6017
+ return !labelMoved && !viewportChanged;
6018
+ }
6019
+
6020
+ #rememberFrozenGeometry() {
6021
+ this.#frozenLabelRect = this.#readLabelRectSnapshot();
6022
+ this.#frozenViewport = this.#readViewportSnapshot();
6023
+ }
6024
+
5933
6025
  #positionPopupOverSelected() {
5934
- // Content ResizeObserver / anchor tracking re-enter here after overflow
5935
- // scroll; keep the open-time alignment instead of fighting the scroller.
5936
- if (this.#freezeMenuPosition) return;
6026
+ // Content ResizeObserver / overflow scroll re-enter here; keep the
6027
+ // open-time alignment unless the trigger or viewport actually changed.
6028
+ if (this.#shouldSkipFrozenPositionPass()) return;
5937
6029
 
5938
6030
  const popup = this.#popup;
5939
6031
  const label = this.#labelEl;
@@ -6011,6 +6103,10 @@ class FigSelect extends HTMLElement {
6011
6103
  }
6012
6104
  panel.syncOverflow?.();
6013
6105
  }
6106
+
6107
+ if (this.#freezeMenuPosition || this.open) {
6108
+ this.#rememberFrozenGeometry();
6109
+ }
6014
6110
  }
6015
6111
 
6016
6112
  #setupListeners() {
@@ -6055,7 +6151,8 @@ class FigSelect extends HTMLElement {
6055
6151
  mutation.type === "attributes" &&
6056
6152
  mutation.target?.tagName === "FIG-SELECT-OPTION" &&
6057
6153
  (mutation.attributeName === "value" ||
6058
- mutation.attributeName === "disabled")
6154
+ mutation.attributeName === "disabled" ||
6155
+ mutation.attributeName === "label")
6059
6156
  ) {
6060
6157
  needsSync = true;
6061
6158
  }
@@ -6073,7 +6170,7 @@ class FigSelect extends HTMLElement {
6073
6170
  subtree: true,
6074
6171
  characterData: true,
6075
6172
  attributes: true,
6076
- attributeFilter: ["value", "disabled", "selected"],
6173
+ attributeFilter: ["value", "disabled", "selected", "label"],
6077
6174
  });
6078
6175
  }
6079
6176
 
@@ -6095,7 +6192,26 @@ class FigSelect extends HTMLElement {
6095
6192
  }
6096
6193
 
6097
6194
  #optionLabel(option) {
6098
- return (option?.textContent || "").trim();
6195
+ if (!option) return "";
6196
+ const labelAttr = option.getAttribute?.("label");
6197
+ if (labelAttr != null && labelAttr !== "") return labelAttr.trim();
6198
+
6199
+ // Ignore prepend/append slot content when deriving a label from children.
6200
+ const parts = [];
6201
+ for (const node of option.childNodes) {
6202
+ if (node.nodeType === Node.TEXT_NODE) {
6203
+ const text = node.textContent?.trim();
6204
+ if (text) parts.push(text);
6205
+ continue;
6206
+ }
6207
+ if (!(node instanceof Element)) continue;
6208
+ const slot = node.getAttribute("slot");
6209
+ if (slot === "prepend" || slot === "append") continue;
6210
+ const text = node.textContent?.trim();
6211
+ if (text) parts.push(text);
6212
+ }
6213
+ if (parts.length) return parts.join(" ").trim();
6214
+ return (option.textContent || "").trim();
6099
6215
  }
6100
6216
 
6101
6217
  #syncPopupAttrs() {
@@ -6375,6 +6491,8 @@ class FigSelect extends HTMLElement {
6375
6491
  if (this.#button) this.#popup.anchor = this.#button;
6376
6492
  this.#installPopupPositioning();
6377
6493
  this.#freezeMenuPosition = false;
6494
+ this.#frozenLabelRect = null;
6495
+ this.#frozenViewport = null;
6378
6496
  this.#syncValue();
6379
6497
  this.#syncPopupWidth();
6380
6498
  this.#popup.open = true;
@@ -6399,16 +6517,20 @@ class FigSelect extends HTMLElement {
6399
6517
  }
6400
6518
  panel?.syncOverflow?.();
6401
6519
  // Freeze after open align so later positionPopup passes don't undo scroll.
6520
+ // Window resize / trigger movement still realigns via geometry checks.
6402
6521
  this.#freezeMenuPosition = true;
6522
+ this.#rememberFrozenGeometry();
6403
6523
  });
6404
6524
  }
6405
6525
 
6406
6526
  #closeList() {
6407
6527
  if (!this.#popup) return;
6408
6528
  this.#freezeMenuPosition = false;
6529
+ this.#frozenLabelRect = null;
6530
+ this.#frozenViewport = null;
6409
6531
  document.removeEventListener("keydown", this.#boundKeydown, true);
6410
6532
  this.#popup.open = false;
6411
6533
  this.#button?.setAttribute("aria-expanded", "false");
6412
6534
  }
6413
6535
  }
6414
- customElements.define("fig-select", FigSelect);
6536
+ figLabDefineElement("fig-select", FigSelect);