@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/README.md +50 -1
- package/components.css +63 -2
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +115 -98
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +16 -16
- package/dist/fig.css +1 -1
- package/dist/fig.js +60 -43
- package/dist/propskit.css +1 -0
- package/dist/propskit.js +1 -0
- package/fig-editor.css +157 -17
- package/fig-editor.js +464 -188
- package/fig-lab.css +51 -42
- package/fig-lab.js +145 -23
- package/fig.js +577 -68
- package/package.json +33 -4
- package/propskit-core.js +556 -0
- package/propskit.d.ts +98 -0
- package/propskit.js +15 -0
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
|
-
/*
|
|
141
|
-
propskit-select applies left inset on label + trigger
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1599
|
-
|
|
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:
|
|
1631
|
+
gap: 0;
|
|
1641
1632
|
position: relative;
|
|
1642
|
-
height: var(--spacer-4);
|
|
1643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 /
|
|
5935
|
-
//
|
|
5936
|
-
if (this.#
|
|
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
|
-
|
|
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
|
-
|
|
6536
|
+
figLabDefineElement("fig-select", FigSelect);
|