@rogieking/figui3 6.21.1 → 6.23.0
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/.cursor/skills/figui3/SKILL.md +1 -15
- package/.cursor/skills/propkit/SKILL.md +2 -2
- package/README.md +6 -8
- package/components.css +344 -201
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +30 -33
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +32 -103
- package/dist/fig.css +1 -1
- package/dist/fig.js +130 -41
- package/dist/propskit.css +1 -1
- package/fig-editor.js +186 -67
- package/fig-lab.css +32 -307
- package/fig-lab.js +727 -1278
- package/fig.js +1645 -288
- package/package.json +1 -1
package/fig-lab.js
CHANGED
|
@@ -18,6 +18,13 @@ function figLabBooleanAttribute(element, name) {
|
|
|
18
18
|
return element.hasAttribute(name) && element.getAttribute(name) !== "false";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/* Unique IDs for lab components such as propskit-group. */
|
|
22
|
+
let figLabUniqueIdCounter = 0;
|
|
23
|
+
function figLabUniqueId(prefix = "fig-lab") {
|
|
24
|
+
figLabUniqueIdCounter += 1;
|
|
25
|
+
return `${prefix}-${figLabUniqueIdCounter}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
/* Field + Switch wrapper */
|
|
22
29
|
class PropskitSwitch extends HTMLElement {
|
|
23
30
|
#field = null;
|
|
@@ -218,7 +225,9 @@ class PropskitSwitch extends HTMLElement {
|
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
set checked(nextChecked) {
|
|
221
|
-
|
|
228
|
+
const checked = Boolean(nextChecked);
|
|
229
|
+
this.toggleAttribute("checked", checked);
|
|
230
|
+
if (this.#switch) this.#switch.value = checked ? "on" : "off";
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
get value() {
|
|
@@ -978,8 +987,11 @@ class PropskitText extends HTMLElement {
|
|
|
978
987
|
set value(nextValue) {
|
|
979
988
|
if (nextValue === null || nextValue === undefined) {
|
|
980
989
|
this.removeAttribute("value");
|
|
990
|
+
if (this.#input) this.#input.value = "";
|
|
981
991
|
} else {
|
|
982
|
-
|
|
992
|
+
const next = String(nextValue);
|
|
993
|
+
this.setAttribute("value", next);
|
|
994
|
+
if (this.#input) this.#input.value = next;
|
|
983
995
|
}
|
|
984
996
|
}
|
|
985
997
|
|
|
@@ -1178,8 +1190,11 @@ class PropskitNumber extends HTMLElement {
|
|
|
1178
1190
|
set value(nextValue) {
|
|
1179
1191
|
if (nextValue === null || nextValue === undefined || nextValue === "") {
|
|
1180
1192
|
this.removeAttribute("value");
|
|
1193
|
+
if (this.#input) this.#input.value = "";
|
|
1181
1194
|
} else {
|
|
1182
|
-
|
|
1195
|
+
const next = String(nextValue);
|
|
1196
|
+
this.setAttribute("value", next);
|
|
1197
|
+
if (this.#input) this.#input.value = next;
|
|
1183
1198
|
}
|
|
1184
1199
|
}
|
|
1185
1200
|
|
|
@@ -1203,6 +1218,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1203
1218
|
].join(",");
|
|
1204
1219
|
|
|
1205
1220
|
#header = null;
|
|
1221
|
+
#disclosure = null;
|
|
1206
1222
|
#chevron = null;
|
|
1207
1223
|
#resetTooltip = null;
|
|
1208
1224
|
#defaults = new WeakMap();
|
|
@@ -1225,10 +1241,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1225
1241
|
cancelAnimationFrame(this.#dirtyFrame);
|
|
1226
1242
|
this.#dirtyFrame = 0;
|
|
1227
1243
|
}
|
|
1228
|
-
|
|
1229
|
-
this.#header.removeEventListener("click", this.#handleToggle);
|
|
1230
|
-
this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
|
|
1231
|
-
}
|
|
1244
|
+
this.#disclosure?.removeEventListener("click", this.#handleToggle);
|
|
1232
1245
|
const resetBtn = this.#resetTooltip?.querySelector("fig-button");
|
|
1233
1246
|
resetBtn?.removeEventListener("click", this.#handleReset);
|
|
1234
1247
|
}
|
|
@@ -1236,7 +1249,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1236
1249
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
1237
1250
|
if (oldValue === newValue) return;
|
|
1238
1251
|
if (name === "open") {
|
|
1239
|
-
this.#
|
|
1252
|
+
this.#disclosure?.setAttribute("aria-expanded", String(this.open));
|
|
1240
1253
|
return;
|
|
1241
1254
|
}
|
|
1242
1255
|
if (name === "show-reset") {
|
|
@@ -1259,7 +1272,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1259
1272
|
} else {
|
|
1260
1273
|
this.setAttribute("open", "false");
|
|
1261
1274
|
}
|
|
1262
|
-
this.#
|
|
1275
|
+
this.#disclosure?.setAttribute("aria-expanded", String(!!value));
|
|
1263
1276
|
if (was !== !!value) {
|
|
1264
1277
|
this.dispatchEvent(
|
|
1265
1278
|
new CustomEvent("openchange", {
|
|
@@ -1339,14 +1352,6 @@ class PropskitGroup extends HTMLElement {
|
|
|
1339
1352
|
this.open = !this.open;
|
|
1340
1353
|
};
|
|
1341
1354
|
|
|
1342
|
-
#handleHeaderKeyDown = (e) => {
|
|
1343
|
-
if (this.#isResetTarget(e.target)) return;
|
|
1344
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
1345
|
-
e.preventDefault();
|
|
1346
|
-
e.stopPropagation();
|
|
1347
|
-
this.open = !this.open;
|
|
1348
|
-
};
|
|
1349
|
-
|
|
1350
1355
|
#handleReset = (e) => {
|
|
1351
1356
|
e.preventDefault();
|
|
1352
1357
|
e.stopPropagation();
|
|
@@ -1528,17 +1533,34 @@ class PropskitGroup extends HTMLElement {
|
|
|
1528
1533
|
|
|
1529
1534
|
if (userHeader) {
|
|
1530
1535
|
this.#header = userHeader;
|
|
1531
|
-
} else if (
|
|
1536
|
+
} else if (
|
|
1537
|
+
!this.#header ||
|
|
1538
|
+
!this.#header.dataset.generated ||
|
|
1539
|
+
this.#header.parentElement !== this
|
|
1540
|
+
) {
|
|
1532
1541
|
this.#header = document.createElement("fig-header");
|
|
1533
1542
|
this.#header.setAttribute("borderless", "");
|
|
1534
1543
|
this.#header.dataset.generated = "true";
|
|
1535
1544
|
this.prepend(this.#header);
|
|
1536
1545
|
}
|
|
1537
1546
|
|
|
1538
|
-
let
|
|
1547
|
+
let disclosure = this.#header.querySelector(
|
|
1548
|
+
":scope > .propskit-group-disclosure",
|
|
1549
|
+
);
|
|
1550
|
+
if (!disclosure) {
|
|
1551
|
+
disclosure = document.createElement("fig-button");
|
|
1552
|
+
disclosure.className = "propskit-group-disclosure";
|
|
1553
|
+
disclosure.setAttribute("variant", "ghost");
|
|
1554
|
+
const existingHeading = this.#header.querySelector(":scope > h3");
|
|
1555
|
+
if (existingHeading) disclosure.appendChild(existingHeading);
|
|
1556
|
+
this.#header.prepend(disclosure);
|
|
1557
|
+
}
|
|
1558
|
+
this.#disclosure = disclosure;
|
|
1559
|
+
|
|
1560
|
+
let h3 = disclosure.querySelector("h3");
|
|
1539
1561
|
if (!h3) {
|
|
1540
1562
|
h3 = document.createElement("h3");
|
|
1541
|
-
|
|
1563
|
+
disclosure.appendChild(h3);
|
|
1542
1564
|
}
|
|
1543
1565
|
if (!h3.id) h3.id = figLabUniqueId("propskit-group");
|
|
1544
1566
|
if (this.#header.dataset.generated) {
|
|
@@ -1564,17 +1586,17 @@ class PropskitGroup extends HTMLElement {
|
|
|
1564
1586
|
}
|
|
1565
1587
|
this.#chevron = h3.querySelector(".propskit-group-chevron");
|
|
1566
1588
|
this.#syncResetButton();
|
|
1567
|
-
this.#header.
|
|
1568
|
-
this.#header.
|
|
1569
|
-
this.#header.
|
|
1570
|
-
this.#
|
|
1571
|
-
this.#
|
|
1572
|
-
this.#
|
|
1573
|
-
this.#
|
|
1589
|
+
this.#header.removeAttribute("role");
|
|
1590
|
+
this.#header.removeAttribute("tabindex");
|
|
1591
|
+
this.#header.removeAttribute("aria-expanded");
|
|
1592
|
+
this.#disclosure.setAttribute("aria-labelledby", h3.id);
|
|
1593
|
+
this.#disclosure.setAttribute("aria-expanded", String(this.open));
|
|
1594
|
+
this.#disclosure.removeEventListener("click", this.#handleToggle);
|
|
1595
|
+
this.#disclosure.addEventListener("click", this.#handleToggle);
|
|
1574
1596
|
|
|
1575
1597
|
if (!this.hasAttribute("open")) {
|
|
1576
1598
|
this.setAttribute("open", "false");
|
|
1577
|
-
this.#
|
|
1599
|
+
this.#disclosure.setAttribute("aria-expanded", "false");
|
|
1578
1600
|
}
|
|
1579
1601
|
}
|
|
1580
1602
|
}
|
|
@@ -1607,6 +1629,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
1607
1629
|
#boundHandleRangeDoubleClick = this.#handleRangeDoubleClick.bind(this);
|
|
1608
1630
|
#boundHandleContextMenu = this.#handleContextMenu.bind(this);
|
|
1609
1631
|
#boundHandleContextMenuChange = this.#handleContextMenuChange.bind(this);
|
|
1632
|
+
#boundHandleClick = this.#handleClick.bind(this);
|
|
1610
1633
|
#ignoredSliderAttrs = new Set([
|
|
1611
1634
|
"variant",
|
|
1612
1635
|
"color",
|
|
@@ -1644,6 +1667,16 @@ class PropskitSlider extends HTMLElement {
|
|
|
1644
1667
|
});
|
|
1645
1668
|
this.removeEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
1646
1669
|
this.addEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
1670
|
+
this.removeEventListener("click", this.#boundHandleClick, true);
|
|
1671
|
+
this.addEventListener("click", this.#boundHandleClick, true);
|
|
1672
|
+
this.#contextMenu?.removeEventListener(
|
|
1673
|
+
"change",
|
|
1674
|
+
this.#boundHandleContextMenuChange,
|
|
1675
|
+
);
|
|
1676
|
+
this.#contextMenu?.addEventListener(
|
|
1677
|
+
"change",
|
|
1678
|
+
this.#boundHandleContextMenuChange,
|
|
1679
|
+
);
|
|
1647
1680
|
|
|
1648
1681
|
if (!this.#observer) {
|
|
1649
1682
|
this.#observer = new MutationObserver((mutations) => {
|
|
@@ -1699,6 +1732,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
1699
1732
|
capture: true,
|
|
1700
1733
|
});
|
|
1701
1734
|
this.removeEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
1735
|
+
this.removeEventListener("click", this.#boundHandleClick, true);
|
|
1702
1736
|
this.#contextMenu?.removeEventListener("change", this.#boundHandleContextMenuChange);
|
|
1703
1737
|
}
|
|
1704
1738
|
|
|
@@ -1798,8 +1832,10 @@ class PropskitSlider extends HTMLElement {
|
|
|
1798
1832
|
|
|
1799
1833
|
for (const attrName of hostAttrs) {
|
|
1800
1834
|
if (attrName === "text") continue;
|
|
1801
|
-
const value = this.getAttribute(attrName);
|
|
1802
|
-
this.#slider.
|
|
1835
|
+
const value = this.getAttribute(attrName) ?? "";
|
|
1836
|
+
if (this.#slider.getAttribute(attrName) !== value) {
|
|
1837
|
+
this.#slider.setAttribute(attrName, value);
|
|
1838
|
+
}
|
|
1803
1839
|
}
|
|
1804
1840
|
|
|
1805
1841
|
this.#slider.removeAttribute("variant");
|
|
@@ -1875,8 +1911,45 @@ class PropskitSlider extends HTMLElement {
|
|
|
1875
1911
|
if (rangeInput !== this.#rangeInput) {
|
|
1876
1912
|
this.#bindRangeInput(rangeInput);
|
|
1877
1913
|
}
|
|
1878
|
-
rangeInput
|
|
1879
|
-
|
|
1914
|
+
if (rangeInput) {
|
|
1915
|
+
rangeInput.removeAttribute("tabindex");
|
|
1916
|
+
rangeInput.removeAttribute("aria-hidden");
|
|
1917
|
+
const label =
|
|
1918
|
+
this.getAttribute("aria-label") ||
|
|
1919
|
+
this.#label?.textContent?.trim() ||
|
|
1920
|
+
"Slider";
|
|
1921
|
+
if (
|
|
1922
|
+
!rangeInput.hasAttribute("aria-label") &&
|
|
1923
|
+
!rangeInput.hasAttribute("aria-labelledby")
|
|
1924
|
+
) {
|
|
1925
|
+
rangeInput.setAttribute("aria-label", label);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (numberInput) {
|
|
1929
|
+
numberInput.setAttribute("tabindex", "-1");
|
|
1930
|
+
numberInput.setAttribute("aria-hidden", "true");
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
#handleClick(event) {
|
|
1935
|
+
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
1936
|
+
if (
|
|
1937
|
+
event.target instanceof Element &&
|
|
1938
|
+
event.target.closest("fig-input-number, fig-menu")
|
|
1939
|
+
) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
this.#queueRangeFocus();
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
#queueRangeFocus() {
|
|
1946
|
+
requestAnimationFrame(() => {
|
|
1947
|
+
requestAnimationFrame(() => {
|
|
1948
|
+
if (this.isConnected && !figLabBooleanAttribute(this, "disabled")) {
|
|
1949
|
+
this.focus();
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1880
1953
|
}
|
|
1881
1954
|
|
|
1882
1955
|
#bindRangeInput(rangeInput) {
|
|
@@ -1904,6 +1977,9 @@ class PropskitSlider extends HTMLElement {
|
|
|
1904
1977
|
|
|
1905
1978
|
#handleElasticPointerDown(event) {
|
|
1906
1979
|
if (event.button !== 0 || figLabBooleanAttribute(this, "disabled")) return;
|
|
1980
|
+
if (!event.target?.closest?.("fig-input-number, fig-menu")) {
|
|
1981
|
+
this.#queueRangeFocus();
|
|
1982
|
+
}
|
|
1907
1983
|
if (this.getAttribute("elastic") === "false") return;
|
|
1908
1984
|
if (event.target?.closest?.("fig-input-number")) return;
|
|
1909
1985
|
const rangeInput =
|
|
@@ -2116,6 +2192,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
2116
2192
|
#setSliderValue(value, eventType) {
|
|
2117
2193
|
if (!this.#slider || value === null || value === undefined) return;
|
|
2118
2194
|
this.#slider.value = value;
|
|
2195
|
+
this.setAttribute("value", String(this.#slider.value));
|
|
2119
2196
|
this.dispatchEvent(
|
|
2120
2197
|
new CustomEvent(eventType, {
|
|
2121
2198
|
detail: this.#slider.value,
|
|
@@ -2180,6 +2257,9 @@ class PropskitSlider extends HTMLElement {
|
|
|
2180
2257
|
event instanceof CustomEvent && event.detail !== undefined
|
|
2181
2258
|
? event.detail
|
|
2182
2259
|
: this.#slider?.value;
|
|
2260
|
+
if (this.#slider?.value !== undefined) {
|
|
2261
|
+
this.setAttribute("value", String(this.#slider.value));
|
|
2262
|
+
}
|
|
2183
2263
|
this.dispatchEvent(
|
|
2184
2264
|
new CustomEvent(type, {
|
|
2185
2265
|
detail,
|
|
@@ -2191,7 +2271,25 @@ class PropskitSlider extends HTMLElement {
|
|
|
2191
2271
|
}
|
|
2192
2272
|
|
|
2193
2273
|
focus(options) {
|
|
2194
|
-
this.#
|
|
2274
|
+
this.#syncFocusDelegation();
|
|
2275
|
+
const range = this.#slider?.querySelector('input[type="range"]');
|
|
2276
|
+
range?.setAttribute("data-propskit-focus-called", "");
|
|
2277
|
+
range?.focus(options);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
get value() {
|
|
2281
|
+
return this.getAttribute("value") ?? this.#slider?.value ?? "";
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
set value(nextValue) {
|
|
2285
|
+
if (nextValue === null || nextValue === undefined || nextValue === "") {
|
|
2286
|
+
this.removeAttribute("value");
|
|
2287
|
+
if (this.#slider) this.#slider.value = "";
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const next = String(nextValue);
|
|
2291
|
+
this.setAttribute("value", next);
|
|
2292
|
+
if (this.#slider) this.#slider.value = next;
|
|
2195
2293
|
}
|
|
2196
2294
|
|
|
2197
2295
|
resetToDefault() {
|
|
@@ -2242,6 +2340,8 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2242
2340
|
#rotateCursorPrevBodyCursor = "";
|
|
2243
2341
|
#rotateCursorPrevBodyCursorPriority = "";
|
|
2244
2342
|
#boundRotateCursorEnd = null;
|
|
2343
|
+
#activeGestureController = null;
|
|
2344
|
+
#activeGestureFinish = null;
|
|
2245
2345
|
|
|
2246
2346
|
get #type() {
|
|
2247
2347
|
return this.getAttribute("type") || "point";
|
|
@@ -2340,6 +2440,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2340
2440
|
}
|
|
2341
2441
|
|
|
2342
2442
|
disconnectedCallback() {
|
|
2443
|
+
this.#cancelActiveGesture();
|
|
2343
2444
|
this.#teardownRadiusDrag();
|
|
2344
2445
|
this.#deactivateMoveCursor();
|
|
2345
2446
|
this.#deactivateRotateCursor();
|
|
@@ -2361,6 +2462,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2361
2462
|
else this.#render();
|
|
2362
2463
|
}
|
|
2363
2464
|
if (name === "type") {
|
|
2465
|
+
this.#cancelActiveGesture();
|
|
2364
2466
|
this.#parseValue();
|
|
2365
2467
|
this.#render();
|
|
2366
2468
|
}
|
|
@@ -2369,6 +2471,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2369
2471
|
else this.#pointHandle.removeAttribute("color");
|
|
2370
2472
|
}
|
|
2371
2473
|
if (name === "disabled") {
|
|
2474
|
+
this.#cancelActiveGesture();
|
|
2372
2475
|
this.#render();
|
|
2373
2476
|
}
|
|
2374
2477
|
if (name === "tooltips") {
|
|
@@ -2440,6 +2543,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2440
2543
|
}
|
|
2441
2544
|
|
|
2442
2545
|
#render() {
|
|
2546
|
+
this.#cancelActiveGesture();
|
|
2443
2547
|
this.innerHTML = "";
|
|
2444
2548
|
this.#pointHandle = null;
|
|
2445
2549
|
this.#secondHandle = null;
|
|
@@ -2862,7 +2966,6 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2862
2966
|
e.stopPropagation();
|
|
2863
2967
|
const container = this.#container;
|
|
2864
2968
|
if (!container) return;
|
|
2865
|
-
const rect0 = container.getBoundingClientRect();
|
|
2866
2969
|
const startX = e.clientX;
|
|
2867
2970
|
const startY = e.clientY;
|
|
2868
2971
|
const x0 = this.#x;
|
|
@@ -2894,21 +2997,24 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2894
2997
|
this.#emitInput();
|
|
2895
2998
|
};
|
|
2896
2999
|
|
|
2897
|
-
const
|
|
3000
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
2898
3001
|
document.body.classList.remove("fig-lab-move-active");
|
|
2899
3002
|
hitLine.style.pointerEvents = "stroke";
|
|
2900
|
-
this.#
|
|
2901
|
-
this.#
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
});
|
|
2908
|
-
};
|
|
3003
|
+
this.#isDragging = false;
|
|
3004
|
+
this.#isSecondDragging = false;
|
|
3005
|
+
if (commit) {
|
|
3006
|
+
this.#syncValueAttribute();
|
|
3007
|
+
this.#emitChange();
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
2909
3010
|
|
|
2910
|
-
window.addEventListener("pointermove", onMove);
|
|
2911
|
-
window.addEventListener("pointerup",
|
|
3011
|
+
window.addEventListener("pointermove", onMove, { signal: gesture.signal });
|
|
3012
|
+
window.addEventListener("pointerup", () => gesture.finish(true), {
|
|
3013
|
+
signal: gesture.signal,
|
|
3014
|
+
});
|
|
3015
|
+
window.addEventListener("pointercancel", () => gesture.finish(false), {
|
|
3016
|
+
signal: gesture.signal,
|
|
3017
|
+
});
|
|
2912
3018
|
});
|
|
2913
3019
|
}
|
|
2914
3020
|
|
|
@@ -3066,7 +3172,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3066
3172
|
svg.style.top = "0";
|
|
3067
3173
|
svg.setAttribute("viewBox", `0 0 ${rect.width} ${rect.height}`);
|
|
3068
3174
|
const lines = svg.querySelectorAll(
|
|
3069
|
-
".fig-canvas-control-angle-line, .fig-canvas-control-angle-line-halo",
|
|
3175
|
+
".fig-canvas-control-angle-line, .fig-canvas-control-angle-line-halo, .fig-canvas-control-angle-line-hit",
|
|
3070
3176
|
);
|
|
3071
3177
|
for (const line of lines) {
|
|
3072
3178
|
line.setAttribute("x1", String(cx));
|
|
@@ -3254,19 +3360,24 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3254
3360
|
this.#emitInput();
|
|
3255
3361
|
};
|
|
3256
3362
|
|
|
3257
|
-
const
|
|
3363
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
3258
3364
|
this.#isAngleDragging = false;
|
|
3259
3365
|
this.classList.remove("fig-canvas-control-ring-active");
|
|
3260
3366
|
this.#angleHandle.removeAttribute("selected");
|
|
3261
3367
|
if (this.#angleTooltip) this.#angleTooltip.removeAttribute("show");
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
};
|
|
3368
|
+
if (commit) {
|
|
3369
|
+
this.#syncValueAttribute();
|
|
3370
|
+
this.#emitChange();
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3267
3373
|
|
|
3268
|
-
window.addEventListener("pointermove", onMove);
|
|
3269
|
-
window.addEventListener("pointerup",
|
|
3374
|
+
window.addEventListener("pointermove", onMove, { signal: gesture.signal });
|
|
3375
|
+
window.addEventListener("pointerup", () => gesture.finish(true), {
|
|
3376
|
+
signal: gesture.signal,
|
|
3377
|
+
});
|
|
3378
|
+
window.addEventListener("pointercancel", () => gesture.finish(false), {
|
|
3379
|
+
signal: gesture.signal,
|
|
3380
|
+
});
|
|
3270
3381
|
});
|
|
3271
3382
|
}
|
|
3272
3383
|
|
|
@@ -3356,17 +3467,22 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3356
3467
|
this.#emitInput();
|
|
3357
3468
|
};
|
|
3358
3469
|
|
|
3359
|
-
const
|
|
3470
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
3360
3471
|
this.#isDragging = false;
|
|
3361
3472
|
if (tooltip) tooltip.removeAttribute("show");
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
};
|
|
3473
|
+
if (commit) {
|
|
3474
|
+
this.#syncValueAttribute();
|
|
3475
|
+
this.#emitChange();
|
|
3476
|
+
}
|
|
3477
|
+
});
|
|
3367
3478
|
|
|
3368
|
-
window.addEventListener("pointermove", onMove);
|
|
3369
|
-
window.addEventListener("pointerup",
|
|
3479
|
+
window.addEventListener("pointermove", onMove, { signal: gesture.signal });
|
|
3480
|
+
window.addEventListener("pointerup", () => gesture.finish(true), {
|
|
3481
|
+
signal: gesture.signal,
|
|
3482
|
+
});
|
|
3483
|
+
window.addEventListener("pointercancel", () => gesture.finish(false), {
|
|
3484
|
+
signal: gesture.signal,
|
|
3485
|
+
});
|
|
3370
3486
|
});
|
|
3371
3487
|
}
|
|
3372
3488
|
|
|
@@ -3462,7 +3578,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3462
3578
|
this.#emitInput();
|
|
3463
3579
|
};
|
|
3464
3580
|
|
|
3465
|
-
const
|
|
3581
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
3466
3582
|
this.#isRadiusDragging = false;
|
|
3467
3583
|
this.classList.remove("fig-canvas-control-ring-active");
|
|
3468
3584
|
circle.style.pointerEvents = "";
|
|
@@ -3474,14 +3590,19 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3474
3590
|
}
|
|
3475
3591
|
document.body.style.cursor = prevBodyCursor;
|
|
3476
3592
|
if (this.#radiusTooltip) this.#radiusTooltip.removeAttribute("show");
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
};
|
|
3593
|
+
if (commit) {
|
|
3594
|
+
this.#syncValueAttribute();
|
|
3595
|
+
this.#emitChange();
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3482
3598
|
|
|
3483
|
-
window.addEventListener("pointermove", onMove);
|
|
3484
|
-
window.addEventListener("pointerup",
|
|
3599
|
+
window.addEventListener("pointermove", onMove, { signal: gesture.signal });
|
|
3600
|
+
window.addEventListener("pointerup", () => gesture.finish(true), {
|
|
3601
|
+
signal: gesture.signal,
|
|
3602
|
+
});
|
|
3603
|
+
window.addEventListener("pointercancel", () => gesture.finish(false), {
|
|
3604
|
+
signal: gesture.signal,
|
|
3605
|
+
});
|
|
3485
3606
|
};
|
|
3486
3607
|
circle.addEventListener("pointerdown", onDown);
|
|
3487
3608
|
this._radiusDragCleanup = () =>
|
|
@@ -3494,6 +3615,32 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3494
3615
|
this._radiusDragCleanup = null;
|
|
3495
3616
|
}
|
|
3496
3617
|
}
|
|
3618
|
+
|
|
3619
|
+
#beginActiveGesture(onFinish) {
|
|
3620
|
+
this.#cancelActiveGesture();
|
|
3621
|
+
const controller = new AbortController();
|
|
3622
|
+
let finished = false;
|
|
3623
|
+
const finish = (commit = false) => {
|
|
3624
|
+
if (finished) return;
|
|
3625
|
+
finished = true;
|
|
3626
|
+
controller.abort();
|
|
3627
|
+
if (this.#activeGestureController === controller) {
|
|
3628
|
+
this.#activeGestureController = null;
|
|
3629
|
+
this.#activeGestureFinish = null;
|
|
3630
|
+
}
|
|
3631
|
+
onFinish(commit);
|
|
3632
|
+
};
|
|
3633
|
+
this.#activeGestureController = controller;
|
|
3634
|
+
this.#activeGestureFinish = finish;
|
|
3635
|
+
return { signal: controller.signal, finish };
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
#cancelActiveGesture() {
|
|
3639
|
+
this.#activeGestureFinish?.(false);
|
|
3640
|
+
this.#activeGestureController?.abort();
|
|
3641
|
+
this.#activeGestureController = null;
|
|
3642
|
+
this.#activeGestureFinish = null;
|
|
3643
|
+
}
|
|
3497
3644
|
}
|
|
3498
3645
|
figLabDefineElement("fig-canvas-control", FigCanvasControl);
|
|
3499
3646
|
|
|
@@ -3528,6 +3675,8 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3528
3675
|
#expandedWaveIndices = new Set();
|
|
3529
3676
|
#resizeObserver = null;
|
|
3530
3677
|
#activeFieldInput = null;
|
|
3678
|
+
#dragController = null;
|
|
3679
|
+
#dragCleanup = null;
|
|
3531
3680
|
|
|
3532
3681
|
static TYPES = [
|
|
3533
3682
|
{ name: "Wave", value: "sine" },
|
|
@@ -3559,7 +3708,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3559
3708
|
}
|
|
3560
3709
|
|
|
3561
3710
|
disconnectedCallback() {
|
|
3562
|
-
this.#
|
|
3711
|
+
this.#cancelDrag();
|
|
3563
3712
|
this.#stopPlayhead();
|
|
3564
3713
|
if (this.#resizeObserver) {
|
|
3565
3714
|
this.#resizeObserver.disconnect();
|
|
@@ -3764,6 +3913,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3764
3913
|
}
|
|
3765
3914
|
|
|
3766
3915
|
#render() {
|
|
3916
|
+
this.#cancelDrag();
|
|
3767
3917
|
this.#stopPlayhead();
|
|
3768
3918
|
this.innerHTML = this.#getInnerHTML();
|
|
3769
3919
|
this.#cacheRefs();
|
|
@@ -3774,7 +3924,8 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3774
3924
|
}
|
|
3775
3925
|
|
|
3776
3926
|
#getInnerHTML() {
|
|
3777
|
-
const
|
|
3927
|
+
const interactive = this.#isEditEnabled() && !this.#isDisabled();
|
|
3928
|
+
const disabled = interactive ? "" : " disabled";
|
|
3778
3929
|
|
|
3779
3930
|
return `<div class="propskit-oscillator-svg-container">
|
|
3780
3931
|
<svg viewBox="0 0 ${this.#drawWidth} ${this.#drawHeight}" class="propskit-oscillator-svg">
|
|
@@ -3782,8 +3933,8 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3782
3933
|
<line class="propskit-oscillator-baseline"></line>
|
|
3783
3934
|
<path class="propskit-oscillator-path"></path>
|
|
3784
3935
|
<circle class="propskit-oscillator-playhead"></circle>
|
|
3785
|
-
|
|
3786
|
-
<foreignObject class="propskit-oscillator-handle propskit-oscillator-frequency-handle" data-handle="frequency" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Frequency"><fig-handle size="small" aria-label="Oscillator frequency handle"
|
|
3936
|
+
${interactive ? `<foreignObject class="propskit-oscillator-handle propskit-oscillator-amplitude-handle" data-handle="amplitude" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Amplitude"><fig-handle size="small" aria-label="Oscillator amplitude handle"></fig-handle></fig-tooltip></div></foreignObject>
|
|
3937
|
+
<foreignObject class="propskit-oscillator-handle propskit-oscillator-frequency-handle" data-handle="frequency" width="20" height="20"><div class="propskit-oscillator-handle-inner"><fig-tooltip text="Frequency"><fig-handle size="small" aria-label="Oscillator frequency handle"></fig-handle></fig-tooltip></div></foreignObject>` : ""}
|
|
3787
3938
|
</svg>
|
|
3788
3939
|
</div>
|
|
3789
3940
|
${this.#isEditEnabled() ? this.#getWaveControlsHTML(disabled) : ""}`;
|
|
@@ -3807,10 +3958,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3807
3958
|
<fig-button class="propskit-oscillator-remove-button" variant="ghost" icon data-wave-index="${index}" aria-label="Remove form"${removeDisabled}><fig-icon name="minus"></fig-icon></fig-button>
|
|
3808
3959
|
</fig-tooltip>
|
|
3809
3960
|
<fig-tooltip text="Add form">
|
|
3810
|
-
|
|
3811
|
-
<fig-icon name="add"></fig-icon>
|
|
3812
|
-
${this.#getWaveTypeDropdownHTML("propskit-oscillator-add-type", "sine", disabled, index)}
|
|
3813
|
-
</fig-button>
|
|
3961
|
+
${this.#getWaveTypeSelectHTML("propskit-oscillator-add-type propskit-oscillator-add-type-button", "sine", disabled, index)}
|
|
3814
3962
|
</fig-tooltip>
|
|
3815
3963
|
</fig-header>
|
|
3816
3964
|
<div class="propskit-oscillator-fields" data-wave-index="${index}"${active}>
|
|
@@ -3822,17 +3970,17 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3822
3970
|
</fig-group>`;
|
|
3823
3971
|
}
|
|
3824
3972
|
|
|
3825
|
-
#
|
|
3973
|
+
#getWaveTypeSelectHTML(className, value, disabled, index = null) {
|
|
3826
3974
|
const options = PropskitOscillator.TYPES.map((type) => {
|
|
3827
3975
|
const selected = type.value === value ? " selected" : "";
|
|
3828
|
-
return `<option value="${type.value}"${selected}>
|
|
3829
|
-
|
|
3830
|
-
<
|
|
3831
|
-
</option>`;
|
|
3976
|
+
return `<fig-select-option value="${type.value}" label="${type.name}"${selected}>
|
|
3977
|
+
<span slot="prepend">${PropskitOscillator.waveIcon(type.value, 24)}</span>
|
|
3978
|
+
<span>${type.name}</span>
|
|
3979
|
+
</fig-select-option>`;
|
|
3832
3980
|
}).join("");
|
|
3833
3981
|
const indexAttr =
|
|
3834
3982
|
index === null ? "" : ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
|
|
3835
|
-
return `<fig-
|
|
3983
|
+
return `<fig-select class="${className}" value="${value}" label="Add form"${indexAttr}${disabled}><fig-select-options>${options}</fig-select-options></fig-select>`;
|
|
3836
3984
|
}
|
|
3837
3985
|
|
|
3838
3986
|
#getNumberFieldHTML(index, name, label, min, max, step, units) {
|
|
@@ -4039,6 +4187,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4039
4187
|
}
|
|
4040
4188
|
|
|
4041
4189
|
#setupEvents() {
|
|
4190
|
+
if (!this.#isEditEnabled() || this.#isDisabled()) return;
|
|
4042
4191
|
for (const typeControl of this.#typeControls) {
|
|
4043
4192
|
typeControl.addEventListener("change", (event) => {
|
|
4044
4193
|
if (this.#isDisabled()) return;
|
|
@@ -4273,6 +4422,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4273
4422
|
}
|
|
4274
4423
|
|
|
4275
4424
|
#startDrag(event, type) {
|
|
4425
|
+
this.#cancelDrag();
|
|
4276
4426
|
this.#isDragging = type;
|
|
4277
4427
|
this.#svg?.classList.add("dragging");
|
|
4278
4428
|
const dragCursor =
|
|
@@ -4313,19 +4463,42 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4313
4463
|
this.#emit("input");
|
|
4314
4464
|
};
|
|
4315
4465
|
|
|
4316
|
-
const
|
|
4466
|
+
const controller = new AbortController();
|
|
4467
|
+
const finish = (commit = false) => {
|
|
4317
4468
|
this.#isDragging = null;
|
|
4318
4469
|
this.#svg?.classList.remove("dragging");
|
|
4319
4470
|
if (dragCursor) {
|
|
4320
4471
|
document.body.style.cursor = prevBodyCursor;
|
|
4321
4472
|
}
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4473
|
+
controller.abort();
|
|
4474
|
+
if (this.#dragController === controller) {
|
|
4475
|
+
this.#dragController = null;
|
|
4476
|
+
this.#dragCleanup = null;
|
|
4477
|
+
}
|
|
4478
|
+
if (commit) this.#emit("change");
|
|
4325
4479
|
};
|
|
4480
|
+
this.#dragController = controller;
|
|
4481
|
+
this.#dragCleanup = finish;
|
|
4482
|
+
|
|
4483
|
+
document.addEventListener("pointermove", onMove, { signal: controller.signal });
|
|
4484
|
+
document.addEventListener("pointerup", () => finish(true), {
|
|
4485
|
+
signal: controller.signal,
|
|
4486
|
+
});
|
|
4487
|
+
document.addEventListener("pointercancel", () => finish(false), {
|
|
4488
|
+
signal: controller.signal,
|
|
4489
|
+
});
|
|
4490
|
+
window.addEventListener("blur", () => finish(false), {
|
|
4491
|
+
signal: controller.signal,
|
|
4492
|
+
});
|
|
4493
|
+
}
|
|
4326
4494
|
|
|
4327
|
-
|
|
4328
|
-
|
|
4495
|
+
#cancelDrag() {
|
|
4496
|
+
this.#dragCleanup?.(false);
|
|
4497
|
+
this.#dragController?.abort();
|
|
4498
|
+
this.#dragController = null;
|
|
4499
|
+
this.#dragCleanup = null;
|
|
4500
|
+
this.#isDragging = null;
|
|
4501
|
+
this.#svg?.classList.remove("dragging");
|
|
4329
4502
|
}
|
|
4330
4503
|
|
|
4331
4504
|
#sampleAtWithoutWave(excludedIndex, t) {
|
|
@@ -4421,6 +4594,9 @@ class FigInputAngle extends HTMLElement {
|
|
|
4421
4594
|
#boundHandleKeyDown;
|
|
4422
4595
|
#boundHandleKeyUp;
|
|
4423
4596
|
#boundHandleAngleInput;
|
|
4597
|
+
#boundHandleDialKeyDown;
|
|
4598
|
+
#gestureController = null;
|
|
4599
|
+
#gestureCleanup = null;
|
|
4424
4600
|
|
|
4425
4601
|
constructor() {
|
|
4426
4602
|
super();
|
|
@@ -4446,10 +4622,12 @@ class FigInputAngle extends HTMLElement {
|
|
|
4446
4622
|
this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
|
|
4447
4623
|
this.#boundHandleKeyUp = this.#handleKeyUp.bind(this);
|
|
4448
4624
|
this.#boundHandleAngleInput = this.#handleAngleInput.bind(this);
|
|
4625
|
+
this.#boundHandleDialKeyDown = this.#handleDialKeyDown.bind(this);
|
|
4449
4626
|
}
|
|
4450
4627
|
|
|
4451
4628
|
connectedCallback() {
|
|
4452
4629
|
requestAnimationFrame(() => {
|
|
4630
|
+
if (!this.isConnected) return;
|
|
4453
4631
|
this.precision = this.getAttribute("precision") || 1;
|
|
4454
4632
|
this.precision = parseInt(this.precision);
|
|
4455
4633
|
this.text = this.getAttribute("text") === "true";
|
|
@@ -4481,10 +4659,13 @@ class FigInputAngle extends HTMLElement {
|
|
|
4481
4659
|
}
|
|
4482
4660
|
|
|
4483
4661
|
disconnectedCallback() {
|
|
4662
|
+
this.#cancelGesture();
|
|
4484
4663
|
this.#cleanupListeners();
|
|
4485
4664
|
}
|
|
4486
4665
|
|
|
4487
4666
|
#render() {
|
|
4667
|
+
this.#cancelGesture();
|
|
4668
|
+
this.#cleanupListeners();
|
|
4488
4669
|
this.innerHTML = this.#getInnerHTML();
|
|
4489
4670
|
}
|
|
4490
4671
|
|
|
@@ -4511,10 +4692,23 @@ class FigInputAngle extends HTMLElement {
|
|
|
4511
4692
|
const step = this.#getStepForUnit();
|
|
4512
4693
|
const minAttr = this.min !== null ? `min="${this.min}"` : "";
|
|
4513
4694
|
const maxAttr = this.max !== null ? `max="${this.max}"` : "";
|
|
4695
|
+
const disabled = this.#isDisabled();
|
|
4696
|
+
const name =
|
|
4697
|
+
this.getAttribute("aria-label") || this.getAttribute("name") || "Angle";
|
|
4698
|
+
const ariaMin = this.min ?? this.#fromDegrees(0);
|
|
4699
|
+
const ariaMax = this.max ?? this.#fromDegrees(360);
|
|
4514
4700
|
return `
|
|
4515
4701
|
${
|
|
4516
4702
|
this.dial
|
|
4517
|
-
? `<div class="fig-input-angle-plane"
|
|
4703
|
+
? `<div class="fig-input-angle-plane"
|
|
4704
|
+
role="slider"
|
|
4705
|
+
tabindex="${disabled ? -1 : 0}"
|
|
4706
|
+
aria-label="${FigInputAngle.#escapeAttribute(name)}"
|
|
4707
|
+
aria-valuemin="${ariaMin}"
|
|
4708
|
+
aria-valuemax="${ariaMax}"
|
|
4709
|
+
aria-valuenow="${this.angle}"
|
|
4710
|
+
aria-valuetext="${FigInputAngle.#escapeAttribute(`${this.angle.toFixed(this.precision)}${this.units}`)}"
|
|
4711
|
+
${disabled ? 'aria-disabled="true"' : ""}>
|
|
4518
4712
|
<div class="fig-input-angle-handle"></div>
|
|
4519
4713
|
</div>`
|
|
4520
4714
|
: ""
|
|
@@ -4527,7 +4721,9 @@ class FigInputAngle extends HTMLElement {
|
|
|
4527
4721
|
value="${this.angle}"
|
|
4528
4722
|
${minAttr}
|
|
4529
4723
|
${maxAttr}
|
|
4530
|
-
units="${this.units}"
|
|
4724
|
+
units="${this.units}"
|
|
4725
|
+
aria-label="${FigInputAngle.#escapeAttribute(name)}"
|
|
4726
|
+
${disabled ? "disabled" : ""}>
|
|
4531
4727
|
${this.showRotations ? `<span slot="append" class="fig-input-angle-rotations"></span>` : ""}
|
|
4532
4728
|
</fig-input-number>`
|
|
4533
4729
|
: ""
|
|
@@ -4540,6 +4736,57 @@ class FigInputAngle extends HTMLElement {
|
|
|
4540
4736
|
return Math.floor(degrees / 360);
|
|
4541
4737
|
}
|
|
4542
4738
|
|
|
4739
|
+
static #escapeAttribute(value) {
|
|
4740
|
+
return String(value)
|
|
4741
|
+
.replace(/&/g, "&")
|
|
4742
|
+
.replace(/"/g, """)
|
|
4743
|
+
.replace(/</g, "<")
|
|
4744
|
+
.replace(/>/g, ">");
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
#isDisabled() {
|
|
4748
|
+
return figLabBooleanAttribute(this, "disabled");
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
#clampValue(value) {
|
|
4752
|
+
let next = Number(value);
|
|
4753
|
+
if (!Number.isFinite(next)) return this.angle;
|
|
4754
|
+
if (this.min !== null && Number.isFinite(this.min)) {
|
|
4755
|
+
next = Math.max(this.min, next);
|
|
4756
|
+
}
|
|
4757
|
+
if (this.max !== null && Number.isFinite(this.max)) {
|
|
4758
|
+
next = Math.min(this.max, next);
|
|
4759
|
+
}
|
|
4760
|
+
return next;
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
#setValue(value, { reflect = true } = {}) {
|
|
4764
|
+
const next = this.#clampValue(value);
|
|
4765
|
+
this.angle = next;
|
|
4766
|
+
this.#calculateAdjacentAndOpposite();
|
|
4767
|
+
if (reflect) {
|
|
4768
|
+
const serialized = String(next);
|
|
4769
|
+
if (this.getAttribute("value") !== serialized) {
|
|
4770
|
+
this.setAttribute("value", serialized);
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
this.#syncHandlePosition();
|
|
4774
|
+
if (this.angleInput) {
|
|
4775
|
+
this.angleInput.value = next.toFixed(this.precision);
|
|
4776
|
+
}
|
|
4777
|
+
this.#syncDialState();
|
|
4778
|
+
this.#updateRotationDisplay();
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
#syncDialState() {
|
|
4782
|
+
if (!this.plane) return;
|
|
4783
|
+
this.plane.setAttribute("aria-valuenow", String(this.angle));
|
|
4784
|
+
this.plane.setAttribute(
|
|
4785
|
+
"aria-valuetext",
|
|
4786
|
+
`${this.angle.toFixed(this.precision)}${this.units}`,
|
|
4787
|
+
);
|
|
4788
|
+
}
|
|
4789
|
+
|
|
4543
4790
|
#updateRotationDisplay() {
|
|
4544
4791
|
if (!this.rotationSpan) return;
|
|
4545
4792
|
const rotations = this.#getRotationCount();
|
|
@@ -4615,10 +4862,12 @@ class FigInputAngle extends HTMLElement {
|
|
|
4615
4862
|
this.#updateRotationDisplay();
|
|
4616
4863
|
this.plane?.addEventListener("mousedown", this.#boundHandleMouseDown);
|
|
4617
4864
|
this.plane?.addEventListener("touchstart", this.#boundHandleTouchStart);
|
|
4865
|
+
this.plane?.addEventListener("keydown", this.#boundHandleDialKeyDown);
|
|
4618
4866
|
window.addEventListener("keydown", this.#boundHandleKeyDown);
|
|
4619
4867
|
window.addEventListener("keyup", this.#boundHandleKeyUp);
|
|
4620
4868
|
if (this.text && this.angleInput) {
|
|
4621
4869
|
this.angleInput.addEventListener("input", this.#boundHandleAngleInput);
|
|
4870
|
+
this.angleInput.addEventListener("change", this.#boundHandleAngleInput);
|
|
4622
4871
|
}
|
|
4623
4872
|
this.addEventListener("change", this.#boundHandleRawChange, true);
|
|
4624
4873
|
}
|
|
@@ -4626,10 +4875,12 @@ class FigInputAngle extends HTMLElement {
|
|
|
4626
4875
|
#cleanupListeners() {
|
|
4627
4876
|
this.plane?.removeEventListener("mousedown", this.#boundHandleMouseDown);
|
|
4628
4877
|
this.plane?.removeEventListener("touchstart", this.#boundHandleTouchStart);
|
|
4878
|
+
this.plane?.removeEventListener("keydown", this.#boundHandleDialKeyDown);
|
|
4629
4879
|
window.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
4630
4880
|
window.removeEventListener("keyup", this.#boundHandleKeyUp);
|
|
4631
4881
|
if (this.text && this.angleInput) {
|
|
4632
4882
|
this.angleInput.removeEventListener("input", this.#boundHandleAngleInput);
|
|
4883
|
+
this.angleInput.removeEventListener("change", this.#boundHandleAngleInput);
|
|
4633
4884
|
}
|
|
4634
4885
|
this.removeEventListener("change", this.#boundHandleRawChange, true);
|
|
4635
4886
|
}
|
|
@@ -4651,12 +4902,10 @@ class FigInputAngle extends HTMLElement {
|
|
|
4651
4902
|
|
|
4652
4903
|
#handleAngleInput(e) {
|
|
4653
4904
|
e.stopPropagation();
|
|
4654
|
-
|
|
4655
|
-
this.#
|
|
4656
|
-
this.#
|
|
4657
|
-
this.#
|
|
4658
|
-
this.#emitInputEvent();
|
|
4659
|
-
this.#emitChangeEvent();
|
|
4905
|
+
if (this.#isDisabled()) return;
|
|
4906
|
+
this.#setValue(Number(e.target.value));
|
|
4907
|
+
if (e.type === "change") this.#emitChangeEvent();
|
|
4908
|
+
else this.#emitInputEvent();
|
|
4660
4909
|
}
|
|
4661
4910
|
|
|
4662
4911
|
#calculateAdjacentAndOpposite() {
|
|
@@ -4689,7 +4938,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
4689
4938
|
const isBounded = this.min !== null || this.max !== null;
|
|
4690
4939
|
|
|
4691
4940
|
if (isBounded) {
|
|
4692
|
-
this.angle = this.#fromDegrees(normalizedAngle);
|
|
4941
|
+
this.angle = this.#clampValue(this.#fromDegrees(normalizedAngle));
|
|
4693
4942
|
} else {
|
|
4694
4943
|
if (this.#prevRawAngle === null) {
|
|
4695
4944
|
this.#prevRawAngle = normalizedAngle;
|
|
@@ -4698,23 +4947,25 @@ class FigInputAngle extends HTMLElement {
|
|
|
4698
4947
|
let delta = normalizedAngle - currentMod;
|
|
4699
4948
|
if (delta > 180) delta -= 360;
|
|
4700
4949
|
if (delta < -180) delta += 360;
|
|
4701
|
-
this.angle
|
|
4950
|
+
this.angle = this.#clampValue(this.angle + this.#fromDegrees(delta));
|
|
4702
4951
|
} else {
|
|
4703
4952
|
let delta = normalizedAngle - this.#prevRawAngle;
|
|
4704
4953
|
if (delta > 180) delta -= 360;
|
|
4705
4954
|
if (delta < -180) delta += 360;
|
|
4706
|
-
this.angle
|
|
4955
|
+
this.angle = this.#clampValue(this.angle + this.#fromDegrees(delta));
|
|
4707
4956
|
this.#prevRawAngle = normalizedAngle;
|
|
4708
4957
|
}
|
|
4709
4958
|
}
|
|
4710
4959
|
|
|
4711
4960
|
this.#calculateAdjacentAndOpposite();
|
|
4961
|
+
this.setAttribute("value", String(this.angle));
|
|
4712
4962
|
|
|
4713
4963
|
this.#syncHandlePosition();
|
|
4714
4964
|
if (this.text && this.angleInput) {
|
|
4715
4965
|
this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
|
|
4716
4966
|
}
|
|
4717
4967
|
this.#updateRotationDisplay();
|
|
4968
|
+
this.#syncDialState();
|
|
4718
4969
|
|
|
4719
4970
|
this.#emitInputEvent();
|
|
4720
4971
|
}
|
|
@@ -4751,6 +5002,8 @@ class FigInputAngle extends HTMLElement {
|
|
|
4751
5002
|
}
|
|
4752
5003
|
|
|
4753
5004
|
#handleMouseDown(e) {
|
|
5005
|
+
if (this.#isDisabled() || e.button !== 0) return;
|
|
5006
|
+
this.#cancelGesture();
|
|
4754
5007
|
this.isDragging = true;
|
|
4755
5008
|
this.#prevRawAngle = null;
|
|
4756
5009
|
this.#updateAngle(e);
|
|
@@ -4760,21 +5013,36 @@ class FigInputAngle extends HTMLElement {
|
|
|
4760
5013
|
if (this.isDragging) this.#updateAngle(e);
|
|
4761
5014
|
};
|
|
4762
5015
|
|
|
4763
|
-
const
|
|
5016
|
+
const controller = new AbortController();
|
|
5017
|
+
const finish = (commit = false) => {
|
|
4764
5018
|
this.isDragging = false;
|
|
4765
5019
|
this.#prevRawAngle = null;
|
|
4766
5020
|
this.plane.classList.remove("dragging");
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
5021
|
+
controller.abort();
|
|
5022
|
+
if (this.#gestureController === controller) {
|
|
5023
|
+
this.#gestureController = null;
|
|
5024
|
+
this.#gestureCleanup = null;
|
|
5025
|
+
}
|
|
5026
|
+
if (commit) this.#emitChangeEvent();
|
|
4770
5027
|
};
|
|
5028
|
+
this.#gestureController = controller;
|
|
5029
|
+
this.#gestureCleanup = finish;
|
|
4771
5030
|
|
|
4772
|
-
window.addEventListener("mousemove", handleMouseMove
|
|
4773
|
-
|
|
5031
|
+
window.addEventListener("mousemove", handleMouseMove, {
|
|
5032
|
+
signal: controller.signal,
|
|
5033
|
+
});
|
|
5034
|
+
window.addEventListener("mouseup", () => finish(true), {
|
|
5035
|
+
signal: controller.signal,
|
|
5036
|
+
});
|
|
5037
|
+
window.addEventListener("blur", () => finish(false), {
|
|
5038
|
+
signal: controller.signal,
|
|
5039
|
+
});
|
|
4774
5040
|
}
|
|
4775
5041
|
|
|
4776
5042
|
#handleTouchStart(e) {
|
|
5043
|
+
if (this.#isDisabled()) return;
|
|
4777
5044
|
e.preventDefault();
|
|
5045
|
+
this.#cancelGesture();
|
|
4778
5046
|
this.isDragging = true;
|
|
4779
5047
|
this.#prevRawAngle = null;
|
|
4780
5048
|
this.#updateAngle(e.touches[0]);
|
|
@@ -4784,17 +5052,56 @@ class FigInputAngle extends HTMLElement {
|
|
|
4784
5052
|
if (this.isDragging) this.#updateAngle(e.touches[0]);
|
|
4785
5053
|
};
|
|
4786
5054
|
|
|
4787
|
-
const
|
|
5055
|
+
const controller = new AbortController();
|
|
5056
|
+
const finish = (commit = false) => {
|
|
4788
5057
|
this.isDragging = false;
|
|
4789
5058
|
this.#prevRawAngle = null;
|
|
4790
5059
|
this.plane.classList.remove("dragging");
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
5060
|
+
controller.abort();
|
|
5061
|
+
if (this.#gestureController === controller) {
|
|
5062
|
+
this.#gestureController = null;
|
|
5063
|
+
this.#gestureCleanup = null;
|
|
5064
|
+
}
|
|
5065
|
+
if (commit) this.#emitChangeEvent();
|
|
4794
5066
|
};
|
|
5067
|
+
this.#gestureController = controller;
|
|
5068
|
+
this.#gestureCleanup = finish;
|
|
5069
|
+
|
|
5070
|
+
window.addEventListener("touchmove", handleTouchMove, {
|
|
5071
|
+
signal: controller.signal,
|
|
5072
|
+
});
|
|
5073
|
+
window.addEventListener("touchend", () => finish(true), {
|
|
5074
|
+
signal: controller.signal,
|
|
5075
|
+
});
|
|
5076
|
+
window.addEventListener("touchcancel", () => finish(false), {
|
|
5077
|
+
signal: controller.signal,
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
4795
5080
|
|
|
4796
|
-
|
|
4797
|
-
|
|
5081
|
+
#cancelGesture() {
|
|
5082
|
+
this.#gestureCleanup?.(false);
|
|
5083
|
+
this.#gestureController?.abort();
|
|
5084
|
+
this.#gestureController = null;
|
|
5085
|
+
this.#gestureCleanup = null;
|
|
5086
|
+
this.isDragging = false;
|
|
5087
|
+
this.#prevRawAngle = null;
|
|
5088
|
+
this.plane?.classList.remove("dragging");
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
#handleDialKeyDown(e) {
|
|
5092
|
+
if (this.#isDisabled()) return;
|
|
5093
|
+
const step = this.#getStepForUnit() * (e.shiftKey ? 10 : 1);
|
|
5094
|
+
let next = this.angle;
|
|
5095
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowDown") next -= step;
|
|
5096
|
+
else if (e.key === "ArrowRight" || e.key === "ArrowUp") next += step;
|
|
5097
|
+
else if (e.key === "Home") next = this.min ?? this.#fromDegrees(0);
|
|
5098
|
+
else if (e.key === "End") next = this.max ?? this.#fromDegrees(360);
|
|
5099
|
+
else return;
|
|
5100
|
+
e.preventDefault();
|
|
5101
|
+
e.stopPropagation();
|
|
5102
|
+
this.#setValue(next);
|
|
5103
|
+
this.#emitInputEvent();
|
|
5104
|
+
this.#emitChangeEvent();
|
|
4798
5105
|
}
|
|
4799
5106
|
|
|
4800
5107
|
#handleKeyDown(e) {
|
|
@@ -4820,6 +5127,9 @@ class FigInputAngle extends HTMLElement {
|
|
|
4820
5127
|
"dial",
|
|
4821
5128
|
"rotations",
|
|
4822
5129
|
"show-rotations",
|
|
5130
|
+
"disabled",
|
|
5131
|
+
"aria-label",
|
|
5132
|
+
"name",
|
|
4823
5133
|
];
|
|
4824
5134
|
}
|
|
4825
5135
|
|
|
@@ -4840,20 +5150,14 @@ class FigInputAngle extends HTMLElement {
|
|
|
4840
5150
|
console.error("Invalid value: must be a number.");
|
|
4841
5151
|
return;
|
|
4842
5152
|
}
|
|
4843
|
-
this
|
|
4844
|
-
this.#calculateAdjacentAndOpposite();
|
|
4845
|
-
this.#syncHandlePosition();
|
|
4846
|
-
if (this.angleInput) {
|
|
4847
|
-
this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
|
|
4848
|
-
}
|
|
4849
|
-
this.#updateRotationDisplay();
|
|
5153
|
+
this.#setValue(Number(value));
|
|
4850
5154
|
}
|
|
4851
5155
|
|
|
4852
5156
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
4853
5157
|
switch (name) {
|
|
4854
5158
|
case "value":
|
|
4855
5159
|
if (this.isDragging) break;
|
|
4856
|
-
|
|
5160
|
+
if (newValue !== null) this.#setValue(Number(newValue), { reflect: false });
|
|
4857
5161
|
break;
|
|
4858
5162
|
case "precision":
|
|
4859
5163
|
this.precision = parseInt(newValue);
|
|
@@ -4889,6 +5193,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
4889
5193
|
}
|
|
4890
5194
|
case "min":
|
|
4891
5195
|
this.min = newValue !== null ? Number(newValue) : null;
|
|
5196
|
+
this.#setValue(this.angle);
|
|
4892
5197
|
if (this.isConnected) {
|
|
4893
5198
|
this.#render();
|
|
4894
5199
|
this.#setupListeners();
|
|
@@ -4897,6 +5202,23 @@ class FigInputAngle extends HTMLElement {
|
|
|
4897
5202
|
break;
|
|
4898
5203
|
case "max":
|
|
4899
5204
|
this.max = newValue !== null ? Number(newValue) : null;
|
|
5205
|
+
this.#setValue(this.angle);
|
|
5206
|
+
if (this.isConnected) {
|
|
5207
|
+
this.#render();
|
|
5208
|
+
this.#setupListeners();
|
|
5209
|
+
this.#syncHandlePosition();
|
|
5210
|
+
}
|
|
5211
|
+
break;
|
|
5212
|
+
case "disabled":
|
|
5213
|
+
this.#cancelGesture();
|
|
5214
|
+
if (this.isConnected) {
|
|
5215
|
+
this.#render();
|
|
5216
|
+
this.#setupListeners();
|
|
5217
|
+
this.#syncHandlePosition();
|
|
5218
|
+
}
|
|
5219
|
+
break;
|
|
5220
|
+
case "aria-label":
|
|
5221
|
+
case "name":
|
|
4900
5222
|
if (this.isConnected) {
|
|
4901
5223
|
this.#render();
|
|
4902
5224
|
this.#setupListeners();
|
|
@@ -4955,9 +5277,15 @@ class FigReorder extends HTMLElement {
|
|
|
4955
5277
|
#bindings = new Map();
|
|
4956
5278
|
#drag = null;
|
|
4957
5279
|
#indicator = null;
|
|
5280
|
+
#liveRegion = null;
|
|
4958
5281
|
|
|
4959
5282
|
connectedCallback() {
|
|
4960
5283
|
this.style.display = "contents";
|
|
5284
|
+
if (!this.hasAttribute("role")) this.setAttribute("role", "list");
|
|
5285
|
+
if (!this.hasAttribute("aria-label")) {
|
|
5286
|
+
this.setAttribute("aria-label", "Reorderable list");
|
|
5287
|
+
}
|
|
5288
|
+
this.#ensureLiveRegion();
|
|
4961
5289
|
this.#syncChildren();
|
|
4962
5290
|
this.#childObserver = new MutationObserver(() => this.#syncChildren());
|
|
4963
5291
|
this.#childObserver.observe(this, { childList: true });
|
|
@@ -4969,10 +5297,15 @@ class FigReorder extends HTMLElement {
|
|
|
4969
5297
|
this.#unbindAll();
|
|
4970
5298
|
this.#cancelDrag();
|
|
4971
5299
|
this.#removeIndicator();
|
|
5300
|
+
this.#liveRegion?.remove();
|
|
5301
|
+
this.#liveRegion = null;
|
|
4972
5302
|
}
|
|
4973
5303
|
|
|
4974
5304
|
attributeChangedCallback() {
|
|
4975
|
-
if (this.isConnected)
|
|
5305
|
+
if (this.isConnected) {
|
|
5306
|
+
if (this.#disabled) this.#cancelDrag();
|
|
5307
|
+
this.#syncChildren();
|
|
5308
|
+
}
|
|
4976
5309
|
}
|
|
4977
5310
|
|
|
4978
5311
|
get #disabled() {
|
|
@@ -4991,7 +5324,11 @@ class FigReorder extends HTMLElement {
|
|
|
4991
5324
|
}
|
|
4992
5325
|
|
|
4993
5326
|
#getElementChildren() {
|
|
4994
|
-
return [...this.children].filter(
|
|
5327
|
+
return [...this.children].filter(
|
|
5328
|
+
(node) =>
|
|
5329
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
5330
|
+
!node.hasAttribute("data-reorder-live"),
|
|
5331
|
+
);
|
|
4995
5332
|
}
|
|
4996
5333
|
|
|
4997
5334
|
#syncChildren() {
|
|
@@ -5005,6 +5342,8 @@ class FigReorder extends HTMLElement {
|
|
|
5005
5342
|
binding.onPointerDown,
|
|
5006
5343
|
true,
|
|
5007
5344
|
);
|
|
5345
|
+
binding.target.removeEventListener("keydown", binding.onKeyDown);
|
|
5346
|
+
this.#restoreKeyboardAttrs(binding);
|
|
5008
5347
|
this.#bindings.delete(child);
|
|
5009
5348
|
}
|
|
5010
5349
|
}
|
|
@@ -5040,12 +5379,20 @@ class FigReorder extends HTMLElement {
|
|
|
5040
5379
|
#clearReorderItemMarks(children) {
|
|
5041
5380
|
for (const child of children) {
|
|
5042
5381
|
child.removeAttribute("data-reorder-item");
|
|
5382
|
+
if (child.hasAttribute("data-reorder-generated-role")) {
|
|
5383
|
+
child.removeAttribute("role");
|
|
5384
|
+
child.removeAttribute("data-reorder-generated-role");
|
|
5385
|
+
}
|
|
5043
5386
|
}
|
|
5044
5387
|
}
|
|
5045
5388
|
|
|
5046
5389
|
#markReorderItems(children) {
|
|
5047
5390
|
for (const child of children) {
|
|
5048
5391
|
child.setAttribute("data-reorder-item", "");
|
|
5392
|
+
if (!child.hasAttribute("role")) {
|
|
5393
|
+
child.setAttribute("role", "listitem");
|
|
5394
|
+
child.setAttribute("data-reorder-generated-role", "");
|
|
5395
|
+
}
|
|
5049
5396
|
}
|
|
5050
5397
|
}
|
|
5051
5398
|
|
|
@@ -5053,7 +5400,10 @@ class FigReorder extends HTMLElement {
|
|
|
5053
5400
|
for (const child of children) {
|
|
5054
5401
|
child
|
|
5055
5402
|
.querySelectorAll("[data-reorder-handle]")
|
|
5056
|
-
.forEach((node) =>
|
|
5403
|
+
.forEach((node) => {
|
|
5404
|
+
node.removeAttribute("data-reorder-handle");
|
|
5405
|
+
node.removeAttribute("aria-roledescription");
|
|
5406
|
+
});
|
|
5057
5407
|
}
|
|
5058
5408
|
}
|
|
5059
5409
|
|
|
@@ -5061,7 +5411,10 @@ class FigReorder extends HTMLElement {
|
|
|
5061
5411
|
if (!this.#handleSelector) return;
|
|
5062
5412
|
for (const child of children) {
|
|
5063
5413
|
const handle = child.querySelector(this.#handleSelector);
|
|
5064
|
-
if (handle)
|
|
5414
|
+
if (handle) {
|
|
5415
|
+
handle.setAttribute("data-reorder-handle", "");
|
|
5416
|
+
handle.setAttribute("aria-roledescription", "reorder handle");
|
|
5417
|
+
}
|
|
5065
5418
|
}
|
|
5066
5419
|
}
|
|
5067
5420
|
|
|
@@ -5084,9 +5437,107 @@ class FigReorder extends HTMLElement {
|
|
|
5084
5437
|
}
|
|
5085
5438
|
this.#startPendingDrag(event, child, target);
|
|
5086
5439
|
};
|
|
5440
|
+
const originalTabIndex = target.getAttribute("tabindex");
|
|
5441
|
+
const originalAriaLabel = target.getAttribute("aria-label");
|
|
5442
|
+
const originalRole = target.getAttribute("role");
|
|
5443
|
+
const itemName =
|
|
5444
|
+
child.getAttribute("aria-label") ||
|
|
5445
|
+
child.textContent?.trim().replace(/\s+/g, " ").slice(0, 80) ||
|
|
5446
|
+
"item";
|
|
5447
|
+
target.setAttribute("tabindex", "0");
|
|
5448
|
+
target.setAttribute("aria-label", `Move ${itemName}`);
|
|
5449
|
+
if (target !== child && !target.hasAttribute("role")) {
|
|
5450
|
+
target.setAttribute("role", "button");
|
|
5451
|
+
}
|
|
5452
|
+
const onKeyDown = (event) => {
|
|
5453
|
+
if (this.#disabled) return;
|
|
5454
|
+
const horizontal = this.#axis === "horizontal";
|
|
5455
|
+
const previousKey = horizontal ? "ArrowLeft" : "ArrowUp";
|
|
5456
|
+
const nextKey = horizontal ? "ArrowRight" : "ArrowDown";
|
|
5457
|
+
if (
|
|
5458
|
+
event.key !== previousKey &&
|
|
5459
|
+
event.key !== nextKey &&
|
|
5460
|
+
event.key !== "Home" &&
|
|
5461
|
+
event.key !== "End"
|
|
5462
|
+
) {
|
|
5463
|
+
return;
|
|
5464
|
+
}
|
|
5465
|
+
event.preventDefault();
|
|
5466
|
+
event.stopPropagation();
|
|
5467
|
+
const items = this.#getElementChildren();
|
|
5468
|
+
const oldIndex = items.indexOf(child);
|
|
5469
|
+
let newIndex = oldIndex;
|
|
5470
|
+
if (event.key === previousKey) newIndex = Math.max(0, oldIndex - 1);
|
|
5471
|
+
else if (event.key === nextKey) {
|
|
5472
|
+
newIndex = Math.min(items.length - 1, oldIndex + 1);
|
|
5473
|
+
} else if (event.key === "Home") newIndex = 0;
|
|
5474
|
+
else if (event.key === "End") newIndex = items.length - 1;
|
|
5475
|
+
if (newIndex === oldIndex) {
|
|
5476
|
+
this.#announce(`${itemName}, position ${oldIndex + 1} of ${items.length}`);
|
|
5477
|
+
return;
|
|
5478
|
+
}
|
|
5479
|
+
this.#moveItemToFinalIndex(child, newIndex);
|
|
5480
|
+
this.#syncChildren();
|
|
5481
|
+
this.#getDragTarget(child)?.focus();
|
|
5482
|
+
this.dispatchEvent(
|
|
5483
|
+
new CustomEvent("reorder", {
|
|
5484
|
+
bubbles: true,
|
|
5485
|
+
detail: { oldIndex, newIndex, item: child },
|
|
5486
|
+
}),
|
|
5487
|
+
);
|
|
5488
|
+
this.#announce(`${itemName}, position ${newIndex + 1} of ${items.length}`);
|
|
5489
|
+
};
|
|
5087
5490
|
|
|
5088
5491
|
target.addEventListener("pointerdown", onPointerDown, true);
|
|
5089
|
-
|
|
5492
|
+
target.addEventListener("keydown", onKeyDown);
|
|
5493
|
+
this.#bindings.set(child, {
|
|
5494
|
+
target,
|
|
5495
|
+
onPointerDown,
|
|
5496
|
+
onKeyDown,
|
|
5497
|
+
originalTabIndex,
|
|
5498
|
+
originalAriaLabel,
|
|
5499
|
+
originalRole,
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
|
|
5503
|
+
#restoreKeyboardAttrs(binding) {
|
|
5504
|
+
const { target, originalTabIndex, originalAriaLabel, originalRole } = binding;
|
|
5505
|
+
if (originalTabIndex === null) target.removeAttribute("tabindex");
|
|
5506
|
+
else target.setAttribute("tabindex", originalTabIndex);
|
|
5507
|
+
if (originalAriaLabel === null) target.removeAttribute("aria-label");
|
|
5508
|
+
else target.setAttribute("aria-label", originalAriaLabel);
|
|
5509
|
+
if (originalRole === null) target.removeAttribute("role");
|
|
5510
|
+
else target.setAttribute("role", originalRole);
|
|
5511
|
+
}
|
|
5512
|
+
|
|
5513
|
+
#ensureLiveRegion() {
|
|
5514
|
+
if (this.#liveRegion?.isConnected) return;
|
|
5515
|
+
const region = document.createElement("span");
|
|
5516
|
+
region.setAttribute("data-reorder-live", "");
|
|
5517
|
+
region.setAttribute("role", "status");
|
|
5518
|
+
region.setAttribute("aria-live", "polite");
|
|
5519
|
+
region.setAttribute("aria-atomic", "true");
|
|
5520
|
+
Object.assign(region.style, {
|
|
5521
|
+
position: "absolute",
|
|
5522
|
+
width: "1px",
|
|
5523
|
+
height: "1px",
|
|
5524
|
+
padding: "0",
|
|
5525
|
+
margin: "-1px",
|
|
5526
|
+
overflow: "hidden",
|
|
5527
|
+
clip: "rect(0, 0, 0, 0)",
|
|
5528
|
+
whiteSpace: "nowrap",
|
|
5529
|
+
border: "0",
|
|
5530
|
+
});
|
|
5531
|
+
this.#liveRegion = region;
|
|
5532
|
+
document.body.appendChild(region);
|
|
5533
|
+
}
|
|
5534
|
+
|
|
5535
|
+
#announce(message) {
|
|
5536
|
+
if (!this.#liveRegion) return;
|
|
5537
|
+
this.#liveRegion.textContent = "";
|
|
5538
|
+
requestAnimationFrame(() => {
|
|
5539
|
+
if (this.#liveRegion) this.#liveRegion.textContent = message;
|
|
5540
|
+
});
|
|
5090
5541
|
}
|
|
5091
5542
|
|
|
5092
5543
|
#isInteractiveTarget(target, child) {
|
|
@@ -5304,6 +5755,13 @@ class FigReorder extends HTMLElement {
|
|
|
5304
5755
|
if (ref !== item) this.insertBefore(item, ref);
|
|
5305
5756
|
}
|
|
5306
5757
|
|
|
5758
|
+
#moveItemToFinalIndex(item, newIndex) {
|
|
5759
|
+
const items = this.#getElementChildren().filter((candidate) => candidate !== item);
|
|
5760
|
+
const ref = items[newIndex] ?? null;
|
|
5761
|
+
if (ref) this.insertBefore(item, ref);
|
|
5762
|
+
else this.appendChild(item);
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5307
5765
|
#finishDrag(state, revert) {
|
|
5308
5766
|
const { item, oldIndex, active, onMove, onUp, onKeyDown } = state;
|
|
5309
5767
|
|
|
@@ -5323,6 +5781,13 @@ class FigReorder extends HTMLElement {
|
|
|
5323
5781
|
detail: { oldIndex, newIndex, item },
|
|
5324
5782
|
}),
|
|
5325
5783
|
);
|
|
5784
|
+
const name =
|
|
5785
|
+
item.getAttribute("aria-label") ||
|
|
5786
|
+
item.textContent?.trim().replace(/\s+/g, " ").slice(0, 80) ||
|
|
5787
|
+
"item";
|
|
5788
|
+
this.#announce(
|
|
5789
|
+
`${name}, position ${newIndex + 1} of ${this.#getElementChildren().length}`,
|
|
5790
|
+
);
|
|
5326
5791
|
}
|
|
5327
5792
|
}
|
|
5328
5793
|
}
|
|
@@ -5340,6 +5805,8 @@ class FigReorder extends HTMLElement {
|
|
|
5340
5805
|
binding.onPointerDown,
|
|
5341
5806
|
true,
|
|
5342
5807
|
);
|
|
5808
|
+
binding.target.removeEventListener("keydown", binding.onKeyDown);
|
|
5809
|
+
this.#restoreKeyboardAttrs(binding);
|
|
5343
5810
|
}
|
|
5344
5811
|
this.#bindings.clear();
|
|
5345
5812
|
}
|
|
@@ -5352,1185 +5819,167 @@ class FigReorder extends HTMLElement {
|
|
|
5352
5819
|
|
|
5353
5820
|
figLabDefineElement("fig-reorder", FigReorder);
|
|
5354
5821
|
|
|
5355
|
-
/*
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
function figLabParseOptionsAttribute(raw) {
|
|
5364
|
-
const text = raw || "";
|
|
5365
|
-
if (text.startsWith("[")) {
|
|
5366
|
-
try {
|
|
5367
|
-
const parsed = JSON.parse(text);
|
|
5368
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
5369
|
-
} catch {
|
|
5370
|
-
/* fall through */
|
|
5371
|
-
}
|
|
5372
|
-
}
|
|
5373
|
-
const delimiter = text.includes("\n") ? "\n" : ",";
|
|
5374
|
-
return text
|
|
5375
|
-
.split(delimiter)
|
|
5376
|
-
.map((s) => s.trim())
|
|
5377
|
-
.filter(Boolean);
|
|
5378
|
-
}
|
|
5822
|
+
/*
|
|
5823
|
+
* fig-select currently lives in fig.js. Keep the lab consumers resilient to
|
|
5824
|
+
* reconnects and selected-option mutations until those core fixes can move
|
|
5825
|
+
* with the component.
|
|
5826
|
+
*/
|
|
5827
|
+
const figLabEnhancedSelects = new WeakSet();
|
|
5828
|
+
const figLabDisconnectedSelects = new WeakSet();
|
|
5829
|
+
const figLabReconnectFallbacks = new WeakSet();
|
|
5379
5830
|
|
|
5380
|
-
function
|
|
5381
|
-
|
|
5382
|
-
return String(opt.value ?? opt.label ?? "");
|
|
5383
|
-
}
|
|
5384
|
-
return String(opt ?? "");
|
|
5831
|
+
function figLabSelectOptions(select) {
|
|
5832
|
+
return Array.from(select.querySelectorAll("fig-select-option"));
|
|
5385
5833
|
}
|
|
5386
5834
|
|
|
5387
|
-
function
|
|
5388
|
-
|
|
5389
|
-
|
|
5835
|
+
function figLabSyncSelectedOption(select, requestedOption, emit = true) {
|
|
5836
|
+
const options = figLabSelectOptions(select);
|
|
5837
|
+
if (!options.length) return;
|
|
5838
|
+
let option =
|
|
5839
|
+
requestedOption && options.includes(requestedOption)
|
|
5840
|
+
? requestedOption
|
|
5841
|
+
: options.find((candidate) => figLabBooleanAttribute(candidate, "selected"));
|
|
5842
|
+
if (!option || figLabBooleanAttribute(option, "disabled")) {
|
|
5843
|
+
option =
|
|
5844
|
+
options.find((candidate) => !figLabBooleanAttribute(candidate, "disabled")) ||
|
|
5845
|
+
options[0];
|
|
5390
5846
|
}
|
|
5391
|
-
|
|
5392
|
-
}
|
|
5847
|
+
if (!option) return;
|
|
5393
5848
|
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
* @attr {boolean} disabled - Whether the option is disabled
|
|
5402
|
-
* @attr {boolean} selected - Whether the option is selected
|
|
5403
|
-
*/
|
|
5404
|
-
class FigSelectOption extends HTMLElement {
|
|
5405
|
-
static get observedAttributes() {
|
|
5406
|
-
return ["value", "disabled", "selected", "label"];
|
|
5849
|
+
const value = option.value ?? option.getAttribute("value") ?? "";
|
|
5850
|
+
const previousValue = select.getAttribute("value") ?? "";
|
|
5851
|
+
select.setAttribute("value", String(value));
|
|
5852
|
+
for (const candidate of options) {
|
|
5853
|
+
const selected = candidate === option;
|
|
5854
|
+
candidate.toggleAttribute("selected", selected);
|
|
5855
|
+
candidate.setAttribute("aria-selected", String(selected));
|
|
5407
5856
|
}
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5857
|
+
if (emit && previousValue !== String(value)) {
|
|
5858
|
+
select.dispatchEvent(
|
|
5859
|
+
new CustomEvent("input", {
|
|
5860
|
+
detail: String(value),
|
|
5861
|
+
bubbles: true,
|
|
5862
|
+
composed: true,
|
|
5863
|
+
}),
|
|
5864
|
+
);
|
|
5865
|
+
select.dispatchEvent(
|
|
5866
|
+
new CustomEvent("change", {
|
|
5867
|
+
detail: String(value),
|
|
5868
|
+
bubbles: true,
|
|
5869
|
+
composed: true,
|
|
5870
|
+
}),
|
|
5871
|
+
);
|
|
5413
5872
|
}
|
|
5873
|
+
}
|
|
5414
5874
|
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5875
|
+
function figLabEnhanceSelect(select) {
|
|
5876
|
+
if (figLabEnhancedSelects.has(select)) return;
|
|
5877
|
+
const trigger = select.shadowRoot?.querySelector(".fig-select-trigger");
|
|
5878
|
+
if (!trigger) {
|
|
5879
|
+
requestAnimationFrame(() => {
|
|
5880
|
+
if (select.isConnected) figLabEnhanceSelect(select);
|
|
5881
|
+
});
|
|
5882
|
+
return;
|
|
5421
5883
|
}
|
|
5884
|
+
figLabEnhancedSelects.add(select);
|
|
5422
5885
|
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
return figLabBooleanAttribute(this, "selected");
|
|
5434
|
-
}
|
|
5435
|
-
|
|
5436
|
-
set selected(val) {
|
|
5437
|
-
if (val) this.setAttribute("selected", "");
|
|
5438
|
-
else this.removeAttribute("selected");
|
|
5439
|
-
}
|
|
5440
|
-
|
|
5441
|
-
connectedCallback() {
|
|
5442
|
-
if (!this.hasAttribute("role")) this.setAttribute("role", "option");
|
|
5443
|
-
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5444
|
-
this.#syncDisabled();
|
|
5445
|
-
}
|
|
5446
|
-
|
|
5447
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
5448
|
-
if (oldValue === newValue) return;
|
|
5449
|
-
if (name === "disabled") this.#syncDisabled();
|
|
5450
|
-
}
|
|
5451
|
-
|
|
5452
|
-
#syncDisabled() {
|
|
5453
|
-
const disabled = this.disabled;
|
|
5454
|
-
if (disabled) {
|
|
5455
|
-
this.setAttribute("aria-disabled", "true");
|
|
5456
|
-
this.setAttribute("tabindex", "-1");
|
|
5457
|
-
} else {
|
|
5458
|
-
this.removeAttribute("aria-disabled");
|
|
5459
|
-
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5460
|
-
}
|
|
5461
|
-
}
|
|
5462
|
-
}
|
|
5463
|
-
figLabDefineElement("fig-select-option", FigSelectOption);
|
|
5464
|
-
|
|
5465
|
-
function figLabSyncOverflowState(host, scrollEl, threshold = 2) {
|
|
5466
|
-
if (!host || !scrollEl) return false;
|
|
5467
|
-
const scrollable = scrollEl.scrollHeight - scrollEl.clientHeight > threshold;
|
|
5468
|
-
const atStart = !scrollable || scrollEl.scrollTop <= threshold;
|
|
5469
|
-
const atEnd =
|
|
5470
|
-
!scrollable ||
|
|
5471
|
-
scrollEl.scrollTop + scrollEl.clientHeight >=
|
|
5472
|
-
scrollEl.scrollHeight - threshold;
|
|
5473
|
-
host.classList.toggle("overflow-start", !atStart);
|
|
5474
|
-
host.classList.toggle("overflow-end", !atEnd);
|
|
5475
|
-
return scrollable;
|
|
5476
|
-
}
|
|
5477
|
-
|
|
5478
|
-
function figLabScrollOverflowPage(scrollEl, direction = 1) {
|
|
5479
|
-
if (!scrollEl) return;
|
|
5480
|
-
scrollEl.scrollBy({
|
|
5481
|
-
top: scrollEl.clientHeight * 0.8 * direction,
|
|
5482
|
-
behavior: "smooth",
|
|
5483
|
-
});
|
|
5484
|
-
}
|
|
5485
|
-
|
|
5486
|
-
function figLabCreateOverflowButtons({ onStart, onEnd } = {}) {
|
|
5487
|
-
const makeButton = (direction, onClick) => {
|
|
5488
|
-
const button = document.createElement("button");
|
|
5489
|
-
button.type = "button";
|
|
5490
|
-
button.className = `fig-overflow fig-overflow-${direction}`;
|
|
5491
|
-
button.dataset.figOverflow = direction;
|
|
5492
|
-
button.setAttribute("data-fig-select-nav", direction);
|
|
5493
|
-
button.setAttribute("tabindex", "-1");
|
|
5494
|
-
button.setAttribute(
|
|
5495
|
-
"aria-label",
|
|
5496
|
-
direction === "start" ? "Scroll up" : "Scroll down",
|
|
5497
|
-
);
|
|
5498
|
-
const icon = document.createElement("fig-icon");
|
|
5499
|
-
icon.setAttribute("name", "chevron");
|
|
5500
|
-
icon.setAttribute("size", "small");
|
|
5501
|
-
icon.className = "fig-overflow-chevron";
|
|
5502
|
-
button.appendChild(icon);
|
|
5503
|
-
button.addEventListener("click", (event) => {
|
|
5504
|
-
event.preventDefault();
|
|
5505
|
-
event.stopPropagation();
|
|
5506
|
-
onClick?.(event);
|
|
5507
|
-
});
|
|
5508
|
-
return button;
|
|
5509
|
-
};
|
|
5510
|
-
return {
|
|
5511
|
-
start: makeButton("start", onStart),
|
|
5512
|
-
end: makeButton("end", onEnd),
|
|
5513
|
-
};
|
|
5514
|
-
}
|
|
5515
|
-
|
|
5516
|
-
/** Light-DOM panel wrapper projected into fig-select's popup; owns overflow buttons. */
|
|
5517
|
-
class FigSelectOptions extends HTMLElement {
|
|
5518
|
-
#navStart = null;
|
|
5519
|
-
#navEnd = null;
|
|
5520
|
-
#resizeObserver = null;
|
|
5521
|
-
#boundSyncOverflow = this.syncOverflow.bind(this);
|
|
5522
|
-
|
|
5523
|
-
connectedCallback() {
|
|
5524
|
-
if (!this.hasAttribute("slot")) this.setAttribute("slot", "panel");
|
|
5525
|
-
this.#unwrapLegacyChooser();
|
|
5526
|
-
this.#ensureNavButtons();
|
|
5527
|
-
this.addEventListener("scroll", this.#boundSyncOverflow, { passive: true });
|
|
5528
|
-
this.#resizeObserver?.disconnect();
|
|
5529
|
-
this.#resizeObserver = new ResizeObserver(() => this.syncOverflow());
|
|
5530
|
-
this.#resizeObserver.observe(this);
|
|
5531
|
-
requestAnimationFrame(() => this.syncOverflow());
|
|
5532
|
-
}
|
|
5533
|
-
|
|
5534
|
-
disconnectedCallback() {
|
|
5535
|
-
this.removeEventListener("scroll", this.#boundSyncOverflow);
|
|
5536
|
-
this.#resizeObserver?.disconnect();
|
|
5537
|
-
this.#resizeObserver = null;
|
|
5538
|
-
this.#removeNavButtons();
|
|
5539
|
-
}
|
|
5540
|
-
|
|
5541
|
-
syncOverflow() {
|
|
5542
|
-
return figLabSyncOverflowState(this, this);
|
|
5543
|
-
}
|
|
5544
|
-
|
|
5545
|
-
scrollToOption(option, behavior = "auto") {
|
|
5546
|
-
if (!option || !this.contains(option)) return;
|
|
5547
|
-
requestAnimationFrame(() => {
|
|
5548
|
-
if (!option.isConnected) return;
|
|
5549
|
-
if (this.scrollHeight <= this.clientHeight + 1) {
|
|
5550
|
-
this.syncOverflow();
|
|
5551
|
-
return;
|
|
5552
|
-
}
|
|
5553
|
-
const optionRect = option.getBoundingClientRect();
|
|
5554
|
-
const hostRect = this.getBoundingClientRect();
|
|
5555
|
-
const optionTop = optionRect.top - hostRect.top + this.scrollTop;
|
|
5556
|
-
const maxScroll = this.scrollHeight - this.clientHeight;
|
|
5557
|
-
const top = Math.max(
|
|
5558
|
-
0,
|
|
5559
|
-
Math.min(
|
|
5560
|
-
optionTop + optionRect.height / 2 - this.clientHeight / 2,
|
|
5561
|
-
maxScroll,
|
|
5562
|
-
),
|
|
5563
|
-
);
|
|
5564
|
-
this.scrollTo({ top, behavior });
|
|
5565
|
-
this.syncOverflow();
|
|
5566
|
-
});
|
|
5567
|
-
}
|
|
5568
|
-
|
|
5569
|
-
#unwrapLegacyChooser() {
|
|
5570
|
-
const chooser = this.querySelector(":scope > fig-chooser");
|
|
5571
|
-
if (!chooser) return;
|
|
5572
|
-
while (chooser.firstChild) {
|
|
5573
|
-
this.insertBefore(chooser.firstChild, chooser);
|
|
5574
|
-
}
|
|
5575
|
-
chooser.remove();
|
|
5576
|
-
}
|
|
5577
|
-
|
|
5578
|
-
#ensureNavButtons() {
|
|
5579
|
-
if (
|
|
5580
|
-
this.#navStart &&
|
|
5581
|
-
this.#navEnd &&
|
|
5582
|
-
this.contains(this.#navStart) &&
|
|
5583
|
-
this.contains(this.#navEnd)
|
|
5584
|
-
) {
|
|
5585
|
-
return;
|
|
5586
|
-
}
|
|
5587
|
-
this.#removeNavButtons();
|
|
5588
|
-
const buttons = figLabCreateOverflowButtons({
|
|
5589
|
-
onStart: () => figLabScrollOverflowPage(this, -1),
|
|
5590
|
-
onEnd: () => figLabScrollOverflowPage(this, 1),
|
|
5591
|
-
});
|
|
5592
|
-
this.#navStart = buttons.start;
|
|
5593
|
-
this.#navEnd = buttons.end;
|
|
5594
|
-
this.prepend(this.#navStart);
|
|
5595
|
-
this.append(this.#navEnd);
|
|
5596
|
-
}
|
|
5597
|
-
|
|
5598
|
-
#removeNavButtons() {
|
|
5599
|
-
this.#navStart?.remove();
|
|
5600
|
-
this.#navEnd?.remove();
|
|
5601
|
-
this.#navStart = null;
|
|
5602
|
-
this.#navEnd = null;
|
|
5603
|
-
this.classList.remove("overflow-start", "overflow-end");
|
|
5604
|
-
}
|
|
5605
|
-
}
|
|
5606
|
-
figLabDefineElement("fig-select-options", FigSelectOptions);
|
|
5607
|
-
|
|
5608
|
-
class FigSelect extends HTMLElement {
|
|
5609
|
-
#button = null;
|
|
5610
|
-
#popup = null;
|
|
5611
|
-
#labelEl = null;
|
|
5612
|
-
#panelSlot = null;
|
|
5613
|
-
#observer = null;
|
|
5614
|
-
#initialized = false;
|
|
5615
|
-
#focusedIndex = -1;
|
|
5616
|
-
#syncingValue = false;
|
|
5617
|
-
#popupPositionPatched = false;
|
|
5618
|
-
#originalPositionPopup = null;
|
|
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
|
-
*/
|
|
5624
|
-
#freezeMenuPosition = false;
|
|
5625
|
-
#frozenLabelRect = null;
|
|
5626
|
-
#frozenViewport = null;
|
|
5627
|
-
#syncingOptions = false;
|
|
5628
|
-
#boundTriggerClick = this.#handleTriggerClick.bind(this);
|
|
5629
|
-
#boundOptionClick = this.#handleOptionClick.bind(this);
|
|
5630
|
-
#boundKeydown = this.#handleKeydown.bind(this);
|
|
5631
|
-
#boundPopupClose = this.#handlePopupClose.bind(this);
|
|
5632
|
-
#boundSlotChange = this.#handleSlotChange.bind(this);
|
|
5633
|
-
|
|
5634
|
-
static get observedAttributes() {
|
|
5635
|
-
return [
|
|
5636
|
-
"value",
|
|
5637
|
-
"disabled",
|
|
5638
|
-
"label",
|
|
5639
|
-
"options",
|
|
5640
|
-
"position",
|
|
5641
|
-
"offset",
|
|
5642
|
-
"closedby",
|
|
5643
|
-
"open",
|
|
5644
|
-
];
|
|
5645
|
-
}
|
|
5646
|
-
|
|
5647
|
-
get value() {
|
|
5648
|
-
return this.getAttribute("value") ?? "";
|
|
5649
|
-
}
|
|
5650
|
-
|
|
5651
|
-
set value(val) {
|
|
5652
|
-
if (val === null || val === undefined) this.removeAttribute("value");
|
|
5653
|
-
else this.setAttribute("value", String(val));
|
|
5654
|
-
}
|
|
5655
|
-
|
|
5656
|
-
get open() {
|
|
5657
|
-
return figLabBooleanAttribute(this, "open");
|
|
5658
|
-
}
|
|
5659
|
-
|
|
5660
|
-
set open(val) {
|
|
5661
|
-
if (val) this.setAttribute("open", "");
|
|
5662
|
-
else this.removeAttribute("open");
|
|
5663
|
-
}
|
|
5664
|
-
|
|
5665
|
-
connectedCallback() {
|
|
5666
|
-
if (!this.#initialized) this.#initialize();
|
|
5667
|
-
this.#ensurePanelSlotAttrs();
|
|
5668
|
-
this.#syncOptionsFromAttribute();
|
|
5669
|
-
this.#syncDisabled();
|
|
5670
|
-
this.#syncPopupAttrs();
|
|
5671
|
-
this.#syncValue();
|
|
5672
|
-
this.#setupObserver();
|
|
5673
|
-
if (this.open) this.#openList();
|
|
5674
|
-
}
|
|
5675
|
-
|
|
5676
|
-
disconnectedCallback() {
|
|
5677
|
-
this.#teardownListeners();
|
|
5678
|
-
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
5679
|
-
this.#observer?.disconnect();
|
|
5680
|
-
this.#observer = null;
|
|
5681
|
-
}
|
|
5682
|
-
|
|
5683
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
5684
|
-
if (oldValue === newValue || !this.#initialized) return;
|
|
5685
|
-
if (name === "options") {
|
|
5686
|
-
this.#syncOptionsFromAttribute();
|
|
5687
|
-
this.#syncValue();
|
|
5688
|
-
return;
|
|
5689
|
-
}
|
|
5690
|
-
if (name === "value" || name === "label") {
|
|
5691
|
-
this.#syncValue();
|
|
5692
|
-
return;
|
|
5693
|
-
}
|
|
5694
|
-
if (name === "disabled") {
|
|
5695
|
-
this.#syncDisabled();
|
|
5696
|
-
return;
|
|
5697
|
-
}
|
|
5698
|
-
if (name === "open") {
|
|
5699
|
-
if (newValue === null || newValue === "false") this.#closeList();
|
|
5700
|
-
else this.#openList();
|
|
5701
|
-
return;
|
|
5702
|
-
}
|
|
5703
|
-
if (name === "position" || name === "offset" || name === "closedby") {
|
|
5704
|
-
this.#syncPopupAttrs();
|
|
5705
|
-
}
|
|
5706
|
-
}
|
|
5707
|
-
|
|
5708
|
-
focus(options) {
|
|
5709
|
-
this.#button?.focus(options);
|
|
5710
|
-
}
|
|
5711
|
-
|
|
5712
|
-
blur() {
|
|
5713
|
-
this.#button?.blur();
|
|
5714
|
-
}
|
|
5715
|
-
|
|
5716
|
-
#isMenuChild(node) {
|
|
5717
|
-
return (
|
|
5718
|
-
node?.nodeType === 1 &&
|
|
5719
|
-
(node.tagName === "FIG-SELECT-OPTION" ||
|
|
5720
|
-
node.tagName === "FIG-MENU-SEPARATOR" ||
|
|
5721
|
-
node.tagName === "FIG-SELECT-OPTIONS")
|
|
5722
|
-
);
|
|
5723
|
-
}
|
|
5724
|
-
|
|
5725
|
-
#ensurePanelSlotAttrs() {
|
|
5726
|
-
for (const panel of this.querySelectorAll(":scope > fig-select-options")) {
|
|
5727
|
-
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5728
|
-
}
|
|
5729
|
-
}
|
|
5730
|
-
|
|
5731
|
-
#getPanel() {
|
|
5732
|
-
const assigned = this.#panelSlot?.assignedElements({ flatten: true }) ?? [];
|
|
5733
|
-
const fromSlot = assigned.find(
|
|
5734
|
-
(el) => el.tagName === "FIG-SELECT-OPTIONS",
|
|
5735
|
-
);
|
|
5736
|
-
if (fromSlot) return fromSlot;
|
|
5737
|
-
return this.querySelector(":scope > fig-select-options");
|
|
5738
|
-
}
|
|
5739
|
-
|
|
5740
|
-
#hasAuthoredOptions() {
|
|
5741
|
-
return Boolean(
|
|
5742
|
-
this.querySelector(
|
|
5743
|
-
":scope > fig-select-option:not([data-fig-generated]), :scope > fig-select-options > fig-select-option:not([data-fig-generated])",
|
|
5744
|
-
),
|
|
5745
|
-
);
|
|
5746
|
-
}
|
|
5747
|
-
|
|
5748
|
-
#ensureOptionsPanel() {
|
|
5749
|
-
let panel = this.#getPanel();
|
|
5750
|
-
if (panel) {
|
|
5751
|
-
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5752
|
-
return panel;
|
|
5753
|
-
}
|
|
5754
|
-
panel = document.createElement("fig-select-options");
|
|
5755
|
-
panel.setAttribute("slot", "panel");
|
|
5756
|
-
panel.setAttribute("data-fig-generated", "");
|
|
5757
|
-
this.appendChild(panel);
|
|
5758
|
-
return panel;
|
|
5759
|
-
}
|
|
5760
|
-
|
|
5761
|
-
/**
|
|
5762
|
-
* When no authored fig-select-option exists, build panel/options from the
|
|
5763
|
-
* options attribute (comma / newline / JSON — same as fig-options).
|
|
5764
|
-
*/
|
|
5765
|
-
#syncOptionsFromAttribute() {
|
|
5766
|
-
if (this.#hasAuthoredOptions()) return;
|
|
5767
|
-
|
|
5768
|
-
const hasOptionsAttr = this.hasAttribute("options");
|
|
5769
|
-
const panel = hasOptionsAttr
|
|
5770
|
-
? this.#ensureOptionsPanel()
|
|
5771
|
-
: this.#getPanel();
|
|
5772
|
-
if (!panel) return;
|
|
5773
|
-
|
|
5774
|
-
this.#syncingOptions = true;
|
|
5775
|
-
try {
|
|
5776
|
-
for (const opt of panel.querySelectorAll(
|
|
5777
|
-
":scope > fig-select-option[data-fig-generated]",
|
|
5778
|
-
)) {
|
|
5779
|
-
opt.remove();
|
|
5886
|
+
const observer = new MutationObserver((mutations) => {
|
|
5887
|
+
let selectedOption = null;
|
|
5888
|
+
let selectedWasRemoved = false;
|
|
5889
|
+
for (const mutation of mutations) {
|
|
5890
|
+
if (
|
|
5891
|
+
mutation.type !== "attributes" ||
|
|
5892
|
+
mutation.attributeName !== "selected" ||
|
|
5893
|
+
mutation.target.tagName !== "FIG-SELECT-OPTION"
|
|
5894
|
+
) {
|
|
5895
|
+
continue;
|
|
5780
5896
|
}
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
el.setAttribute("data-fig-generated", "");
|
|
5789
|
-
el.setAttribute("value", figLabOptionEntryValue(entry));
|
|
5790
|
-
el.textContent = figLabOptionEntryLabel(entry);
|
|
5791
|
-
if (endBtn) panel.insertBefore(el, endBtn);
|
|
5792
|
-
else panel.appendChild(el);
|
|
5897
|
+
if (figLabBooleanAttribute(mutation.target, "selected")) {
|
|
5898
|
+
selectedOption = mutation.target;
|
|
5899
|
+
} else if (
|
|
5900
|
+
String(mutation.target.value ?? "") ===
|
|
5901
|
+
(select.getAttribute("value") ?? "")
|
|
5902
|
+
) {
|
|
5903
|
+
selectedWasRemoved = true;
|
|
5793
5904
|
}
|
|
5794
|
-
} finally {
|
|
5795
|
-
this.#syncingOptions = false;
|
|
5796
|
-
}
|
|
5797
|
-
}
|
|
5798
|
-
|
|
5799
|
-
#initialize() {
|
|
5800
|
-
this.#initialized = true;
|
|
5801
|
-
const shadow = this.attachShadow({ mode: "open" });
|
|
5802
|
-
shadow.innerHTML = `
|
|
5803
|
-
<style>
|
|
5804
|
-
:host {
|
|
5805
|
-
display: inline-flex;
|
|
5806
|
-
position: relative;
|
|
5807
|
-
align-items: center;
|
|
5808
|
-
min-width: 0;
|
|
5809
|
-
}
|
|
5810
|
-
:host([full]:not([full="false"])) {
|
|
5811
|
-
display: flex;
|
|
5812
|
-
width: 100%;
|
|
5813
|
-
}
|
|
5814
|
-
.fig-select-trigger {
|
|
5815
|
-
display: flex;
|
|
5816
|
-
align-items: center;
|
|
5817
|
-
justify-content: flex-start;
|
|
5818
|
-
flex: 1;
|
|
5819
|
-
min-width: 0;
|
|
5820
|
-
width: var(--fig-select-trigger-width, 100%);
|
|
5821
|
-
height: 100%;
|
|
5822
|
-
margin: 0;
|
|
5823
|
-
padding: 0 var(--spacer-4, 1rem) 0 var(--spacer-2, 0.5rem);
|
|
5824
|
-
border: 0;
|
|
5825
|
-
border-radius: inherit;
|
|
5826
|
-
background: transparent;
|
|
5827
|
-
box-shadow: none;
|
|
5828
|
-
color: inherit;
|
|
5829
|
-
font: inherit;
|
|
5830
|
-
font-weight: inherit;
|
|
5831
|
-
text-align: left;
|
|
5832
|
-
white-space: nowrap;
|
|
5833
|
-
overflow: hidden;
|
|
5834
|
-
text-overflow: ellipsis;
|
|
5835
|
-
cursor: default;
|
|
5836
|
-
}
|
|
5837
|
-
.fig-select-trigger:hover,
|
|
5838
|
-
.fig-select-trigger:active,
|
|
5839
|
-
.fig-select-trigger:active:hover {
|
|
5840
|
-
background: transparent;
|
|
5841
|
-
box-shadow: none;
|
|
5842
|
-
color: inherit;
|
|
5843
|
-
}
|
|
5844
|
-
.fig-select-trigger:focus-visible,
|
|
5845
|
-
.fig-select-trigger[data-focus-visible] {
|
|
5846
|
-
outline: var(--figma-focus-outline);
|
|
5847
|
-
outline-offset: var(--figma-focus-outline-offset);
|
|
5848
|
-
}
|
|
5849
|
-
:host([disabled]:not([disabled="false"])) .fig-select-trigger,
|
|
5850
|
-
:host([disabled]:not([disabled="false"])) .fig-select-label {
|
|
5851
|
-
color: var(--figma-color-text-tertiary);
|
|
5852
|
-
}
|
|
5853
|
-
.fig-select-label {
|
|
5854
|
-
display: block;
|
|
5855
|
-
width: 100%;
|
|
5856
|
-
min-width: 0;
|
|
5857
|
-
overflow: hidden;
|
|
5858
|
-
text-overflow: ellipsis;
|
|
5859
|
-
white-space: nowrap;
|
|
5860
|
-
text-align: left;
|
|
5861
|
-
}
|
|
5862
|
-
/* Listbox chrome from document fig-select::part(listbox).
|
|
5863
|
-
Overflow UI lives on slotted fig-select-options.
|
|
5864
|
-
Never set display except when open — closed <dialog> must stay display:none. */
|
|
5865
|
-
dialog[is="fig-popup"] {
|
|
5866
|
-
flex-direction: column;
|
|
5867
|
-
overflow: hidden;
|
|
5868
|
-
}
|
|
5869
|
-
dialog[is="fig-popup"][open] {
|
|
5870
|
-
display: flex;
|
|
5871
|
-
}
|
|
5872
|
-
::slotted(fig-select-options) {
|
|
5873
|
-
flex: 1 1 auto;
|
|
5874
|
-
min-height: 0;
|
|
5875
|
-
max-height: inherit;
|
|
5876
|
-
}
|
|
5877
|
-
</style>
|
|
5878
|
-
`;
|
|
5879
|
-
|
|
5880
|
-
const button = document.createElement("fig-button");
|
|
5881
|
-
button.className = "fig-select-trigger";
|
|
5882
|
-
button.setAttribute("part", "trigger");
|
|
5883
|
-
button.setAttribute("variant", "ghost");
|
|
5884
|
-
button.setAttribute("aria-haspopup", "listbox");
|
|
5885
|
-
button.setAttribute("aria-expanded", "false");
|
|
5886
|
-
|
|
5887
|
-
const labelEl = document.createElement("span");
|
|
5888
|
-
labelEl.className = "fig-select-label";
|
|
5889
|
-
labelEl.setAttribute("part", "label");
|
|
5890
|
-
button.appendChild(labelEl);
|
|
5891
|
-
|
|
5892
|
-
const popup = document.createElement("dialog", { is: "fig-popup" });
|
|
5893
|
-
popup.setAttribute("is", "fig-popup");
|
|
5894
|
-
popup.setAttribute("part", "listbox");
|
|
5895
|
-
popup.setAttribute("theme", "menu");
|
|
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
|
-
}
|
|
5903
|
-
popup.id = figLabUniqueId("fig-select-list");
|
|
5904
|
-
button.setAttribute("aria-controls", popup.id);
|
|
5905
|
-
|
|
5906
|
-
const panelSlot = document.createElement("slot");
|
|
5907
|
-
panelSlot.setAttribute("name", "panel");
|
|
5908
|
-
popup.appendChild(panelSlot);
|
|
5909
|
-
|
|
5910
|
-
shadow.append(button, popup);
|
|
5911
|
-
|
|
5912
|
-
this.#button = button;
|
|
5913
|
-
this.#labelEl = labelEl;
|
|
5914
|
-
this.#popup = popup;
|
|
5915
|
-
this.#panelSlot = panelSlot;
|
|
5916
|
-
popup.anchor = button;
|
|
5917
|
-
|
|
5918
|
-
this.#ensurePanelSlotAttrs();
|
|
5919
|
-
this.#setupListeners();
|
|
5920
|
-
this.#installPopupPositioning();
|
|
5921
|
-
|
|
5922
|
-
if (!this.hasAttribute("value")) {
|
|
5923
|
-
const selected = this.#getOptions().find((opt) =>
|
|
5924
|
-
figLabBooleanAttribute(opt, "selected"),
|
|
5925
|
-
);
|
|
5926
|
-
if (selected) this.setAttribute("value", selected.value);
|
|
5927
5905
|
}
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
if (typeof this.#popup.positionPopup !== "function") return;
|
|
5933
|
-
this.#originalPositionPopup = this.#popup.positionPopup.bind(this.#popup);
|
|
5934
|
-
this.#popup.positionPopup = () => {
|
|
5935
|
-
if (!this.open) {
|
|
5936
|
-
this.#originalPositionPopup?.();
|
|
5937
|
-
return;
|
|
5906
|
+
if (selectedOption) {
|
|
5907
|
+
const optionValue = String(selectedOption.value ?? "");
|
|
5908
|
+
if ((select.getAttribute("value") ?? "") !== optionValue) {
|
|
5909
|
+
figLabSyncSelectedOption(select, selectedOption);
|
|
5938
5910
|
}
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
this.#popupPositionPatched = true;
|
|
5942
|
-
}
|
|
5943
|
-
|
|
5944
|
-
#getOptionTextRect(option) {
|
|
5945
|
-
if (!option) return null;
|
|
5946
|
-
const range = document.createRange();
|
|
5947
|
-
range.selectNodeContents(option);
|
|
5948
|
-
const rects = [...range.getClientRects()].filter(
|
|
5949
|
-
(rect) => rect.width > 0 && rect.height > 0,
|
|
5950
|
-
);
|
|
5951
|
-
if (rects.length) return rects[0];
|
|
5952
|
-
return option.getBoundingClientRect();
|
|
5953
|
-
}
|
|
5954
|
-
|
|
5955
|
-
#getViewportMargins() {
|
|
5956
|
-
if (typeof this.#popup?.parseViewportMargins === "function") {
|
|
5957
|
-
return this.#popup.parseViewportMargins();
|
|
5958
|
-
}
|
|
5959
|
-
return { top: 8, right: 8, bottom: 8, left: 8 };
|
|
5960
|
-
}
|
|
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
|
-
|
|
6025
|
-
#positionPopupOverSelected() {
|
|
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;
|
|
6029
|
-
|
|
6030
|
-
const popup = this.#popup;
|
|
6031
|
-
const label = this.#labelEl;
|
|
6032
|
-
if (!popup || !label) {
|
|
6033
|
-
this.#originalPositionPopup?.();
|
|
6034
|
-
return;
|
|
6035
|
-
}
|
|
6036
|
-
|
|
6037
|
-
const options = this.#getOptions();
|
|
6038
|
-
const selected =
|
|
6039
|
-
options.find((opt) => this.#optionValue(opt) === this.value) ||
|
|
6040
|
-
options[0];
|
|
6041
|
-
if (!selected) {
|
|
6042
|
-
this.#originalPositionPopup?.();
|
|
6043
|
-
return;
|
|
5911
|
+
} else if (selectedWasRemoved) {
|
|
5912
|
+
figLabSyncSelectedOption(select, null);
|
|
6044
5913
|
}
|
|
5914
|
+
});
|
|
5915
|
+
observer.observe(select, {
|
|
5916
|
+
attributes: true,
|
|
5917
|
+
subtree: true,
|
|
5918
|
+
attributeFilter: ["selected"],
|
|
5919
|
+
});
|
|
5920
|
+
}
|
|
6045
5921
|
|
|
6046
|
-
|
|
6047
|
-
|
|
5922
|
+
function figLabInstallSelectReconnectFallback(select) {
|
|
5923
|
+
if (figLabReconnectFallbacks.has(select)) return;
|
|
5924
|
+
const trigger = select.shadowRoot?.querySelector(".fig-select-trigger");
|
|
5925
|
+
if (!trigger) return;
|
|
5926
|
+
figLabReconnectFallbacks.add(select);
|
|
6048
5927
|
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
5928
|
+
trigger.addEventListener("click", () => {
|
|
5929
|
+
if (figLabBooleanAttribute(select, "disabled")) return;
|
|
5930
|
+
select.open = !select.open;
|
|
5931
|
+
});
|
|
5932
|
+
select.addEventListener("click", (event) => {
|
|
5933
|
+
const option = event
|
|
5934
|
+
.composedPath()
|
|
5935
|
+
.find((node) => node?.tagName === "FIG-SELECT-OPTION");
|
|
6052
5936
|
if (
|
|
6053
|
-
!
|
|
6054
|
-
!
|
|
6055
|
-
|
|
6056
|
-
!optionTextRect
|
|
5937
|
+
!option ||
|
|
5938
|
+
!select.contains(option) ||
|
|
5939
|
+
figLabBooleanAttribute(option, "disabled")
|
|
6057
5940
|
) {
|
|
6058
5941
|
return;
|
|
6059
5942
|
}
|
|
5943
|
+
figLabSyncSelectedOption(select, option);
|
|
5944
|
+
select.open = false;
|
|
5945
|
+
});
|
|
5946
|
+
}
|
|
6060
5947
|
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
// [full]: pin menu to host width/edges. Otherwise overlay selected
|
|
6065
|
-
// option text on the trigger label (blend-mode style).
|
|
6066
|
-
let left = full
|
|
6067
|
-
? this.getBoundingClientRect().left
|
|
6068
|
-
: labelRect.left - selectedOffsetX;
|
|
6069
|
-
let top = labelRect.top - selectedOffsetY;
|
|
6070
|
-
|
|
6071
|
-
// Keep the whole menu in-view when aligning over the selected option
|
|
6072
|
-
// would otherwise push it past a viewport edge (corners / far sides).
|
|
6073
|
-
const margins = this.#getViewportMargins();
|
|
6074
|
-
if (typeof popup.clampToViewport === "function") {
|
|
6075
|
-
({ left, top } = popup.clampToViewport({ left, top }, popupRect, margins));
|
|
6076
|
-
} else {
|
|
6077
|
-
const minLeft = margins.left;
|
|
6078
|
-
const minTop = margins.top;
|
|
6079
|
-
const maxLeft = window.innerWidth - popupRect.width - margins.right;
|
|
6080
|
-
const maxTop = window.innerHeight - popupRect.height - margins.bottom;
|
|
6081
|
-
left = Math.min(Math.max(left, minLeft), Math.max(minLeft, maxLeft));
|
|
6082
|
-
top = Math.min(Math.max(top, minTop), Math.max(minTop, maxTop));
|
|
6083
|
-
}
|
|
6084
|
-
|
|
6085
|
-
// !important: fig-select::part(listbox) and dialog UA rules can otherwise
|
|
6086
|
-
// keep the menu at its static/anchor position past the viewport edge.
|
|
6087
|
-
popup.style.setProperty("right", "auto", "important");
|
|
6088
|
-
popup.style.setProperty("bottom", "auto", "important");
|
|
6089
|
-
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
6090
|
-
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
6091
|
-
|
|
6092
|
-
// Nudge the panel scroller so the selected label stays over the trigger.
|
|
6093
|
-
const panel = this.#getPanel();
|
|
6094
|
-
const alignedTextRect = this.#getOptionTextRect(selected);
|
|
6095
|
-
if (
|
|
6096
|
-
alignedTextRect &&
|
|
6097
|
-
panel &&
|
|
6098
|
-
panel.scrollHeight > panel.clientHeight + 1
|
|
6099
|
-
) {
|
|
6100
|
-
const deltaY = alignedTextRect.top - labelRect.top;
|
|
6101
|
-
if (Math.abs(deltaY) > 0.5) {
|
|
6102
|
-
panel.scrollTop += deltaY;
|
|
6103
|
-
}
|
|
6104
|
-
panel.syncOverflow?.();
|
|
6105
|
-
}
|
|
6106
|
-
|
|
6107
|
-
if (this.#freezeMenuPosition || this.open) {
|
|
6108
|
-
this.#rememberFrozenGeometry();
|
|
6109
|
-
}
|
|
6110
|
-
}
|
|
6111
|
-
|
|
6112
|
-
#setupListeners() {
|
|
6113
|
-
this.#button?.addEventListener("click", this.#boundTriggerClick);
|
|
6114
|
-
this.#button?.addEventListener("keydown", this.#boundKeydown);
|
|
6115
|
-
// Host click: slotted options stay in light DOM (not dialog.contains).
|
|
6116
|
-
this.addEventListener("click", this.#boundOptionClick);
|
|
6117
|
-
this.#popup?.addEventListener("keydown", this.#boundKeydown);
|
|
6118
|
-
this.#popup?.addEventListener("close", this.#boundPopupClose);
|
|
6119
|
-
this.#panelSlot?.addEventListener("slotchange", this.#boundSlotChange);
|
|
6120
|
-
}
|
|
6121
|
-
|
|
6122
|
-
#teardownListeners() {
|
|
6123
|
-
this.#button?.removeEventListener("click", this.#boundTriggerClick);
|
|
6124
|
-
this.#button?.removeEventListener("keydown", this.#boundKeydown);
|
|
6125
|
-
this.removeEventListener("click", this.#boundOptionClick);
|
|
6126
|
-
this.#popup?.removeEventListener("keydown", this.#boundKeydown);
|
|
6127
|
-
this.#popup?.removeEventListener("close", this.#boundPopupClose);
|
|
6128
|
-
this.#panelSlot?.removeEventListener("slotchange", this.#boundSlotChange);
|
|
6129
|
-
}
|
|
6130
|
-
|
|
6131
|
-
#handleSlotChange() {
|
|
6132
|
-
this.#ensurePanelSlotAttrs();
|
|
6133
|
-
this.#syncValue();
|
|
6134
|
-
}
|
|
6135
|
-
|
|
6136
|
-
#setupObserver() {
|
|
6137
|
-
if (this.#observer) return;
|
|
6138
|
-
this.#observer = new MutationObserver((mutations) => {
|
|
6139
|
-
if (this.#syncingValue || this.#syncingOptions) return;
|
|
6140
|
-
let needsSync = false;
|
|
6141
|
-
for (const mutation of mutations) {
|
|
6142
|
-
if (mutation.type === "childList") {
|
|
6143
|
-
if (
|
|
6144
|
-
[...mutation.addedNodes].some((node) => this.#isMenuChild(node)) ||
|
|
6145
|
-
[...mutation.removedNodes].some((node) => this.#isMenuChild(node))
|
|
6146
|
-
) {
|
|
6147
|
-
needsSync = true;
|
|
6148
|
-
}
|
|
6149
|
-
}
|
|
6150
|
-
if (
|
|
6151
|
-
mutation.type === "attributes" &&
|
|
6152
|
-
mutation.target?.tagName === "FIG-SELECT-OPTION" &&
|
|
6153
|
-
(mutation.attributeName === "value" ||
|
|
6154
|
-
mutation.attributeName === "disabled" ||
|
|
6155
|
-
mutation.attributeName === "label")
|
|
6156
|
-
) {
|
|
6157
|
-
needsSync = true;
|
|
6158
|
-
}
|
|
6159
|
-
if (
|
|
6160
|
-
mutation.type === "characterData" &&
|
|
6161
|
-
mutation.target?.parentElement?.tagName === "FIG-SELECT-OPTION"
|
|
6162
|
-
) {
|
|
6163
|
-
needsSync = true;
|
|
6164
|
-
}
|
|
6165
|
-
}
|
|
6166
|
-
if (needsSync) this.#syncValue();
|
|
6167
|
-
});
|
|
6168
|
-
this.#observer.observe(this, {
|
|
6169
|
-
childList: true,
|
|
6170
|
-
subtree: true,
|
|
6171
|
-
characterData: true,
|
|
6172
|
-
attributes: true,
|
|
6173
|
-
attributeFilter: ["value", "disabled", "selected", "label"],
|
|
6174
|
-
});
|
|
6175
|
-
}
|
|
6176
|
-
|
|
6177
|
-
#getOptions({ enabledOnly = false } = {}) {
|
|
6178
|
-
const panel = this.#getPanel();
|
|
6179
|
-
const options = panel
|
|
6180
|
-
? Array.from(panel.querySelectorAll(":scope > fig-select-option"))
|
|
6181
|
-
: [];
|
|
6182
|
-
if (!enabledOnly) return options;
|
|
6183
|
-
return options.filter((opt) => !figLabBooleanAttribute(opt, "disabled"));
|
|
6184
|
-
}
|
|
6185
|
-
|
|
6186
|
-
#optionValue(option) {
|
|
6187
|
-
if (!option) return "";
|
|
6188
|
-
if (typeof option.value === "string") return option.value;
|
|
6189
|
-
const attr = option.getAttribute?.("value");
|
|
6190
|
-
if (attr != null) return attr;
|
|
6191
|
-
return (option.textContent || "").trim();
|
|
6192
|
-
}
|
|
6193
|
-
|
|
6194
|
-
#optionLabel(option) {
|
|
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();
|
|
6215
|
-
}
|
|
6216
|
-
|
|
6217
|
-
#syncPopupAttrs() {
|
|
6218
|
-
if (!this.#popup) return;
|
|
6219
|
-
this.#popup.setAttribute(
|
|
6220
|
-
"position",
|
|
6221
|
-
this.getAttribute("position") || "bottom left",
|
|
6222
|
-
);
|
|
6223
|
-
const offset = this.getAttribute("offset");
|
|
6224
|
-
if (offset) this.#popup.setAttribute("offset", offset);
|
|
6225
|
-
else this.#popup.removeAttribute("offset");
|
|
6226
|
-
const closedby = this.getAttribute("closedby");
|
|
6227
|
-
if (closedby) this.#popup.setAttribute("closedby", closedby);
|
|
6228
|
-
else this.#popup.removeAttribute("closedby");
|
|
5948
|
+
function figLabMarkDisconnectedSelectTree(root) {
|
|
5949
|
+
if (root instanceof Element && root.matches("fig-select")) {
|
|
5950
|
+
figLabDisconnectedSelects.add(root);
|
|
6229
5951
|
}
|
|
5952
|
+
root
|
|
5953
|
+
.querySelectorAll?.("fig-select")
|
|
5954
|
+
.forEach((select) => figLabDisconnectedSelects.add(select));
|
|
5955
|
+
}
|
|
6230
5956
|
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
if (disabled) this.#button.setAttribute("disabled", "");
|
|
6235
|
-
else this.#button.removeAttribute("disabled");
|
|
6236
|
-
}
|
|
6237
|
-
if (disabled && this.open) this.open = false;
|
|
5957
|
+
function figLabEnhanceSelectTree(root) {
|
|
5958
|
+
if (root instanceof Element && root.matches("fig-select")) {
|
|
5959
|
+
figLabEnhanceSelect(root);
|
|
6238
5960
|
}
|
|
5961
|
+
root
|
|
5962
|
+
.querySelectorAll?.("fig-select")
|
|
5963
|
+
.forEach((select) => figLabEnhanceSelect(select));
|
|
5964
|
+
}
|
|
6239
5965
|
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
if (selected && !figLabBooleanAttribute(selected, "disabled")) {
|
|
6246
|
-
return selected;
|
|
5966
|
+
figLabEnhanceSelectTree(document);
|
|
5967
|
+
new MutationObserver((mutations) => {
|
|
5968
|
+
for (const mutation of mutations) {
|
|
5969
|
+
for (const node of mutation.removedNodes) {
|
|
5970
|
+
if (node instanceof Element) figLabMarkDisconnectedSelectTree(node);
|
|
6247
5971
|
}
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
new CustomEvent("input", {
|
|
6258
|
-
detail: value,
|
|
6259
|
-
bubbles: true,
|
|
6260
|
-
composed: true,
|
|
6261
|
-
}),
|
|
6262
|
-
);
|
|
6263
|
-
this.dispatchEvent(
|
|
6264
|
-
new CustomEvent("change", {
|
|
6265
|
-
detail: value,
|
|
6266
|
-
bubbles: true,
|
|
6267
|
-
composed: true,
|
|
6268
|
-
}),
|
|
6269
|
-
);
|
|
6270
|
-
}
|
|
6271
|
-
|
|
6272
|
-
#syncValue() {
|
|
6273
|
-
if (this.#syncingValue) return;
|
|
6274
|
-
this.#syncingValue = true;
|
|
6275
|
-
try {
|
|
6276
|
-
const options = this.#getOptions();
|
|
6277
|
-
const hasValueAttr = this.hasAttribute("value");
|
|
6278
|
-
const previousValue = hasValueAttr ? this.getAttribute("value") : null;
|
|
6279
|
-
let match = hasValueAttr
|
|
6280
|
-
? options.find((opt) => this.#optionValue(opt) === previousValue)
|
|
6281
|
-
: null;
|
|
6282
|
-
let valueCorrected = false;
|
|
6283
|
-
|
|
6284
|
-
if (!match) {
|
|
6285
|
-
if (hasValueAttr) {
|
|
6286
|
-
// Options may not be built yet (options attr sync). Keep value until then.
|
|
6287
|
-
if (!options.length) {
|
|
6288
|
-
if (this.#labelEl) {
|
|
6289
|
-
this.#labelEl.textContent =
|
|
6290
|
-
previousValue || this.getAttribute("label") || "";
|
|
6291
|
-
}
|
|
6292
|
-
return;
|
|
6293
|
-
}
|
|
6294
|
-
// Value orphaned (option removed / value attr changed) — clamp or clear.
|
|
6295
|
-
match = this.#pickFallbackOption(options);
|
|
6296
|
-
if (match) {
|
|
6297
|
-
const nextValue = this.#optionValue(match);
|
|
6298
|
-
if (previousValue !== nextValue) {
|
|
6299
|
-
this.setAttribute("value", nextValue);
|
|
6300
|
-
valueCorrected = true;
|
|
6301
|
-
}
|
|
6302
|
-
} else {
|
|
6303
|
-
this.removeAttribute("value");
|
|
6304
|
-
valueCorrected = true;
|
|
6305
|
-
}
|
|
6306
|
-
} else {
|
|
6307
|
-
// No host value yet — honor a selected option if present.
|
|
6308
|
-
match = options.find((opt) =>
|
|
6309
|
-
figLabBooleanAttribute(opt, "selected"),
|
|
6310
|
-
);
|
|
6311
|
-
if (match) {
|
|
6312
|
-
this.setAttribute("value", this.#optionValue(match));
|
|
6313
|
-
valueCorrected = true;
|
|
6314
|
-
}
|
|
5972
|
+
for (const node of mutation.addedNodes) {
|
|
5973
|
+
if (!(node instanceof Element)) continue;
|
|
5974
|
+
figLabEnhanceSelectTree(node);
|
|
5975
|
+
const selects = node.matches("fig-select")
|
|
5976
|
+
? [node]
|
|
5977
|
+
: [...node.querySelectorAll("fig-select")];
|
|
5978
|
+
for (const select of selects) {
|
|
5979
|
+
if (figLabDisconnectedSelects.has(select)) {
|
|
5980
|
+
figLabInstallSelectReconnectFallback(select);
|
|
6315
5981
|
}
|
|
6316
5982
|
}
|
|
6317
|
-
|
|
6318
|
-
for (const opt of options) {
|
|
6319
|
-
const selected = opt === match;
|
|
6320
|
-
opt.setAttribute("aria-selected", selected ? "true" : "false");
|
|
6321
|
-
if (selected) opt.setAttribute("selected", "");
|
|
6322
|
-
else opt.removeAttribute("selected");
|
|
6323
|
-
}
|
|
6324
|
-
|
|
6325
|
-
const label =
|
|
6326
|
-
(match && this.#optionLabel(match)) || this.getAttribute("label") || "";
|
|
6327
|
-
if (this.#labelEl) this.#labelEl.textContent = label;
|
|
6328
|
-
|
|
6329
|
-
const ariaLabel = this.getAttribute("label") || "Select";
|
|
6330
|
-
this.#button?.setAttribute("aria-label", ariaLabel);
|
|
6331
|
-
|
|
6332
|
-
// Don't scrollToOption while open — reposition/sync would fight overflow paging.
|
|
6333
|
-
this.#getPanel()?.syncOverflow?.();
|
|
6334
|
-
|
|
6335
|
-
if (valueCorrected) {
|
|
6336
|
-
this.#emitValueEvents(this.getAttribute("value") ?? "");
|
|
6337
|
-
}
|
|
6338
|
-
} finally {
|
|
6339
|
-
this.#syncingValue = false;
|
|
6340
5983
|
}
|
|
6341
5984
|
}
|
|
6342
|
-
|
|
6343
|
-
#handleTriggerClick(e) {
|
|
6344
|
-
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
6345
|
-
e.preventDefault();
|
|
6346
|
-
e.stopPropagation();
|
|
6347
|
-
const nextOpen = !this.open;
|
|
6348
|
-
if (nextOpen && this.#popup && this.#button) {
|
|
6349
|
-
this.#popup.anchor = this.#button;
|
|
6350
|
-
}
|
|
6351
|
-
this.open = nextOpen;
|
|
6352
|
-
}
|
|
6353
|
-
|
|
6354
|
-
#handleOptionClick(e) {
|
|
6355
|
-
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
6356
|
-
const option = path.find(
|
|
6357
|
-
(node) => node?.tagName === "FIG-SELECT-OPTION",
|
|
6358
|
-
);
|
|
6359
|
-
if (!option || !this.contains(option)) return;
|
|
6360
|
-
if (figLabBooleanAttribute(option, "disabled")) return;
|
|
6361
|
-
// Do not stopPropagation — React light-DOM onClick must still fire.
|
|
6362
|
-
this.#selectOption(option);
|
|
6363
|
-
}
|
|
6364
|
-
|
|
6365
|
-
#handleKeydown(e) {
|
|
6366
|
-
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
6367
|
-
|
|
6368
|
-
const listOpen = this.open && (this.#popup?.matches?.(":open") ?? false);
|
|
6369
|
-
if (!listOpen) {
|
|
6370
|
-
if (
|
|
6371
|
-
this.#button?.contains(e.target) &&
|
|
6372
|
-
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")
|
|
6373
|
-
) {
|
|
6374
|
-
e.preventDefault();
|
|
6375
|
-
if (this.#popup && this.#button) this.#popup.anchor = this.#button;
|
|
6376
|
-
this.open = true;
|
|
6377
|
-
requestAnimationFrame(() => {
|
|
6378
|
-
const options = this.#getOptions({ enabledOnly: true });
|
|
6379
|
-
const selectedIndex = options.findIndex(
|
|
6380
|
-
(opt) => this.#optionValue(opt) === this.value,
|
|
6381
|
-
);
|
|
6382
|
-
this.#focusOptionAt(selectedIndex >= 0 ? selectedIndex : 0);
|
|
6383
|
-
});
|
|
6384
|
-
}
|
|
6385
|
-
return;
|
|
6386
|
-
}
|
|
6387
|
-
|
|
6388
|
-
const options = this.#getOptions({ enabledOnly: true });
|
|
6389
|
-
if (!options.length) return;
|
|
6390
|
-
|
|
6391
|
-
switch (e.key) {
|
|
6392
|
-
case "ArrowDown":
|
|
6393
|
-
e.preventDefault();
|
|
6394
|
-
this.#syncFocusedIndex();
|
|
6395
|
-
this.#focusOptionAt(this.#focusedIndex + 1);
|
|
6396
|
-
break;
|
|
6397
|
-
case "ArrowUp":
|
|
6398
|
-
e.preventDefault();
|
|
6399
|
-
this.#syncFocusedIndex();
|
|
6400
|
-
this.#focusOptionAt(this.#focusedIndex - 1);
|
|
6401
|
-
break;
|
|
6402
|
-
case "Home":
|
|
6403
|
-
e.preventDefault();
|
|
6404
|
-
this.#focusOptionAt(0);
|
|
6405
|
-
break;
|
|
6406
|
-
case "End":
|
|
6407
|
-
e.preventDefault();
|
|
6408
|
-
this.#focusOptionAt(options.length - 1);
|
|
6409
|
-
break;
|
|
6410
|
-
case "Escape":
|
|
6411
|
-
e.preventDefault();
|
|
6412
|
-
this.open = false;
|
|
6413
|
-
this.#button?.focus();
|
|
6414
|
-
break;
|
|
6415
|
-
case "Enter":
|
|
6416
|
-
case " ": {
|
|
6417
|
-
this.#syncFocusedIndex();
|
|
6418
|
-
const focused = options[this.#focusedIndex];
|
|
6419
|
-
if (!focused) return;
|
|
6420
|
-
e.preventDefault();
|
|
6421
|
-
this.#selectOption(focused);
|
|
6422
|
-
break;
|
|
6423
|
-
}
|
|
6424
|
-
}
|
|
6425
|
-
}
|
|
6426
|
-
|
|
6427
|
-
#handlePopupClose() {
|
|
6428
|
-
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
6429
|
-
this.#button?.setAttribute("aria-expanded", "false");
|
|
6430
|
-
this.#button?.focus();
|
|
6431
|
-
this.#focusedIndex = -1;
|
|
6432
|
-
}
|
|
6433
|
-
|
|
6434
|
-
#selectOption(option) {
|
|
6435
|
-
const value = this.#optionValue(option);
|
|
6436
|
-
this.setAttribute("value", value);
|
|
6437
|
-
this.#syncValue();
|
|
6438
|
-
this.#emitValueEvents(value);
|
|
6439
|
-
this.open = false;
|
|
6440
|
-
}
|
|
6441
|
-
|
|
6442
|
-
#getEnabledOptions() {
|
|
6443
|
-
return this.#getOptions({ enabledOnly: true });
|
|
6444
|
-
}
|
|
6445
|
-
|
|
6446
|
-
#syncFocusedIndex() {
|
|
6447
|
-
const options = this.#getEnabledOptions();
|
|
6448
|
-
if (!options.length) {
|
|
6449
|
-
this.#focusedIndex = -1;
|
|
6450
|
-
return;
|
|
6451
|
-
}
|
|
6452
|
-
const active = options.find((opt) => opt === document.activeElement);
|
|
6453
|
-
const index = active ? options.indexOf(active) : -1;
|
|
6454
|
-
this.#focusedIndex = index >= 0 ? index : this.#focusedIndex;
|
|
6455
|
-
}
|
|
6456
|
-
|
|
6457
|
-
#focusOptionAt(index) {
|
|
6458
|
-
const options = this.#getEnabledOptions();
|
|
6459
|
-
if (!options.length) return;
|
|
6460
|
-
const next = ((index % options.length) + options.length) % options.length;
|
|
6461
|
-
this.#focusedIndex = next;
|
|
6462
|
-
options[next]?.focus();
|
|
6463
|
-
}
|
|
6464
|
-
|
|
6465
|
-
#syncPopupWidth() {
|
|
6466
|
-
if (!this.#popup || !this.#button) return;
|
|
6467
|
-
const hostWidth = Math.ceil(this.getBoundingClientRect().width);
|
|
6468
|
-
const triggerWidth = Math.ceil(this.#button.getBoundingClientRect().width);
|
|
6469
|
-
const anchorWidth = Math.max(hostWidth, triggerWidth, 96);
|
|
6470
|
-
const full = figLabBooleanAttribute(this, "full");
|
|
6471
|
-
|
|
6472
|
-
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
6473
|
-
// [full]: lock to host. Otherwise content-sized with host as min and 20rem max.
|
|
6474
|
-
if (full) {
|
|
6475
|
-
this.#popup.style.setProperty("width", `${anchorWidth}px`, "important");
|
|
6476
|
-
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
6477
|
-
this.#popup.style.setProperty("max-width", `${anchorWidth}px`, "important");
|
|
6478
|
-
} else {
|
|
6479
|
-
this.#popup.style.setProperty("width", "max-content", "important");
|
|
6480
|
-
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
6481
|
-
this.#popup.style.setProperty(
|
|
6482
|
-
"max-width",
|
|
6483
|
-
"min(20rem, calc(100vw - 1rem))",
|
|
6484
|
-
"important",
|
|
6485
|
-
);
|
|
6486
|
-
}
|
|
6487
|
-
}
|
|
6488
|
-
|
|
6489
|
-
#openList() {
|
|
6490
|
-
if (!this.#popup || figLabBooleanAttribute(this, "disabled")) return;
|
|
6491
|
-
if (this.#button) this.#popup.anchor = this.#button;
|
|
6492
|
-
this.#installPopupPositioning();
|
|
6493
|
-
this.#freezeMenuPosition = false;
|
|
6494
|
-
this.#frozenLabelRect = null;
|
|
6495
|
-
this.#frozenViewport = null;
|
|
6496
|
-
this.#syncValue();
|
|
6497
|
-
this.#syncPopupWidth();
|
|
6498
|
-
this.#popup.open = true;
|
|
6499
|
-
document.addEventListener("keydown", this.#boundKeydown, true);
|
|
6500
|
-
this.#button?.setAttribute("aria-expanded", "true");
|
|
6501
|
-
this.#focusedIndex = -1;
|
|
6502
|
-
requestAnimationFrame(() => {
|
|
6503
|
-
this.#syncPopupWidth();
|
|
6504
|
-
this.#positionPopupOverSelected();
|
|
6505
|
-
const panel = this.#getPanel();
|
|
6506
|
-
const options = this.#getEnabledOptions();
|
|
6507
|
-
const selectedIndex = options.findIndex(
|
|
6508
|
-
(opt) => this.#optionValue(opt) === this.value,
|
|
6509
|
-
);
|
|
6510
|
-
if (selectedIndex >= 0) {
|
|
6511
|
-
this.#focusOptionAt(selectedIndex);
|
|
6512
|
-
} else if (
|
|
6513
|
-
this.#button?.hasAttribute("data-focus-visible") ||
|
|
6514
|
-
this.#button?.matches?.(":focus-visible")
|
|
6515
|
-
) {
|
|
6516
|
-
this.#focusOptionAt(0);
|
|
6517
|
-
}
|
|
6518
|
-
panel?.syncOverflow?.();
|
|
6519
|
-
// Freeze after open align so later positionPopup passes don't undo scroll.
|
|
6520
|
-
// Window resize / trigger movement still realigns via geometry checks.
|
|
6521
|
-
this.#freezeMenuPosition = true;
|
|
6522
|
-
this.#rememberFrozenGeometry();
|
|
6523
|
-
});
|
|
6524
|
-
}
|
|
6525
|
-
|
|
6526
|
-
#closeList() {
|
|
6527
|
-
if (!this.#popup) return;
|
|
6528
|
-
this.#freezeMenuPosition = false;
|
|
6529
|
-
this.#frozenLabelRect = null;
|
|
6530
|
-
this.#frozenViewport = null;
|
|
6531
|
-
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
6532
|
-
this.#popup.open = false;
|
|
6533
|
-
this.#button?.setAttribute("aria-expanded", "false");
|
|
6534
|
-
}
|
|
6535
|
-
}
|
|
6536
|
-
figLabDefineElement("fig-select", FigSelect);
|
|
5985
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|