@rogieking/figui3 6.21.0 → 6.22.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 +338 -200
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +63 -191
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +32 -103
- package/dist/fig.css +1 -1
- package/dist/fig.js +128 -39
- package/dist/propskit.css +1 -1
- package/dist/propskit.js +1 -591
- package/fig-editor.js +193 -68
- package/fig-lab.css +32 -307
- package/fig-lab.js +743 -1288
- package/fig.js +1653 -342
- package/package.json +2 -2
package/fig-lab.js
CHANGED
|
@@ -8,10 +8,23 @@
|
|
|
8
8
|
* <script src="fig-lab.js"></script>
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
function figLabDefineElement(name, constructor) {
|
|
12
|
+
if (!customElements.get(name)) {
|
|
13
|
+
customElements.define(name, constructor);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
function figLabBooleanAttribute(element, name) {
|
|
12
18
|
return element.hasAttribute(name) && element.getAttribute(name) !== "false";
|
|
13
19
|
}
|
|
14
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
|
+
|
|
15
28
|
/* Field + Switch wrapper */
|
|
16
29
|
class PropskitSwitch extends HTMLElement {
|
|
17
30
|
#field = null;
|
|
@@ -212,7 +225,9 @@ class PropskitSwitch extends HTMLElement {
|
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
set checked(nextChecked) {
|
|
215
|
-
|
|
228
|
+
const checked = Boolean(nextChecked);
|
|
229
|
+
this.toggleAttribute("checked", checked);
|
|
230
|
+
if (this.#switch) this.#switch.value = checked ? "on" : "off";
|
|
216
231
|
}
|
|
217
232
|
|
|
218
233
|
get value() {
|
|
@@ -230,7 +245,7 @@ class PropskitSwitch extends HTMLElement {
|
|
|
230
245
|
selected?.focus(options);
|
|
231
246
|
}
|
|
232
247
|
}
|
|
233
|
-
|
|
248
|
+
figLabDefineElement("propskit-switch", PropskitSwitch);
|
|
234
249
|
|
|
235
250
|
/* Field + Color wrapper */
|
|
236
251
|
class PropskitColor extends HTMLElement {
|
|
@@ -516,7 +531,7 @@ class PropskitColor extends HTMLElement {
|
|
|
516
531
|
this.#input?.querySelector("input:not([tabindex='-1'])")?.focus(options);
|
|
517
532
|
}
|
|
518
533
|
}
|
|
519
|
-
|
|
534
|
+
figLabDefineElement("propskit-color", PropskitColor);
|
|
520
535
|
|
|
521
536
|
/* Field + Select wrapper */
|
|
522
537
|
class PropskitSelect extends HTMLElement {
|
|
@@ -771,7 +786,7 @@ class PropskitSelect extends HTMLElement {
|
|
|
771
786
|
this.#select?.focus(options);
|
|
772
787
|
}
|
|
773
788
|
}
|
|
774
|
-
|
|
789
|
+
figLabDefineElement("propskit-select", PropskitSelect);
|
|
775
790
|
|
|
776
791
|
/* Field + Text wrapper */
|
|
777
792
|
class PropskitText extends HTMLElement {
|
|
@@ -972,8 +987,11 @@ class PropskitText extends HTMLElement {
|
|
|
972
987
|
set value(nextValue) {
|
|
973
988
|
if (nextValue === null || nextValue === undefined) {
|
|
974
989
|
this.removeAttribute("value");
|
|
990
|
+
if (this.#input) this.#input.value = "";
|
|
975
991
|
} else {
|
|
976
|
-
|
|
992
|
+
const next = String(nextValue);
|
|
993
|
+
this.setAttribute("value", next);
|
|
994
|
+
if (this.#input) this.#input.value = next;
|
|
977
995
|
}
|
|
978
996
|
}
|
|
979
997
|
|
|
@@ -981,7 +999,7 @@ class PropskitText extends HTMLElement {
|
|
|
981
999
|
this.#input?.focus(options);
|
|
982
1000
|
}
|
|
983
1001
|
}
|
|
984
|
-
|
|
1002
|
+
figLabDefineElement("propskit-text", PropskitText);
|
|
985
1003
|
|
|
986
1004
|
/* Field + Number wrapper */
|
|
987
1005
|
class PropskitNumber extends HTMLElement {
|
|
@@ -1172,8 +1190,11 @@ class PropskitNumber extends HTMLElement {
|
|
|
1172
1190
|
set value(nextValue) {
|
|
1173
1191
|
if (nextValue === null || nextValue === undefined || nextValue === "") {
|
|
1174
1192
|
this.removeAttribute("value");
|
|
1193
|
+
if (this.#input) this.#input.value = "";
|
|
1175
1194
|
} else {
|
|
1176
|
-
|
|
1195
|
+
const next = String(nextValue);
|
|
1196
|
+
this.setAttribute("value", next);
|
|
1197
|
+
if (this.#input) this.#input.value = next;
|
|
1177
1198
|
}
|
|
1178
1199
|
}
|
|
1179
1200
|
|
|
@@ -1181,7 +1202,7 @@ class PropskitNumber extends HTMLElement {
|
|
|
1181
1202
|
this.#input?.focus(options);
|
|
1182
1203
|
}
|
|
1183
1204
|
}
|
|
1184
|
-
|
|
1205
|
+
figLabDefineElement("propskit-number", PropskitNumber);
|
|
1185
1206
|
|
|
1186
1207
|
/* Collapsible property group — always collapsible (no collapsible attr). */
|
|
1187
1208
|
class PropskitGroup extends HTMLElement {
|
|
@@ -1197,6 +1218,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1197
1218
|
].join(",");
|
|
1198
1219
|
|
|
1199
1220
|
#header = null;
|
|
1221
|
+
#disclosure = null;
|
|
1200
1222
|
#chevron = null;
|
|
1201
1223
|
#resetTooltip = null;
|
|
1202
1224
|
#defaults = new WeakMap();
|
|
@@ -1219,10 +1241,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1219
1241
|
cancelAnimationFrame(this.#dirtyFrame);
|
|
1220
1242
|
this.#dirtyFrame = 0;
|
|
1221
1243
|
}
|
|
1222
|
-
|
|
1223
|
-
this.#header.removeEventListener("click", this.#handleToggle);
|
|
1224
|
-
this.#header.removeEventListener("keydown", this.#handleHeaderKeyDown);
|
|
1225
|
-
}
|
|
1244
|
+
this.#disclosure?.removeEventListener("click", this.#handleToggle);
|
|
1226
1245
|
const resetBtn = this.#resetTooltip?.querySelector("fig-button");
|
|
1227
1246
|
resetBtn?.removeEventListener("click", this.#handleReset);
|
|
1228
1247
|
}
|
|
@@ -1230,7 +1249,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1230
1249
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
1231
1250
|
if (oldValue === newValue) return;
|
|
1232
1251
|
if (name === "open") {
|
|
1233
|
-
this.#
|
|
1252
|
+
this.#disclosure?.setAttribute("aria-expanded", String(this.open));
|
|
1234
1253
|
return;
|
|
1235
1254
|
}
|
|
1236
1255
|
if (name === "show-reset") {
|
|
@@ -1253,7 +1272,7 @@ class PropskitGroup extends HTMLElement {
|
|
|
1253
1272
|
} else {
|
|
1254
1273
|
this.setAttribute("open", "false");
|
|
1255
1274
|
}
|
|
1256
|
-
this.#
|
|
1275
|
+
this.#disclosure?.setAttribute("aria-expanded", String(!!value));
|
|
1257
1276
|
if (was !== !!value) {
|
|
1258
1277
|
this.dispatchEvent(
|
|
1259
1278
|
new CustomEvent("openchange", {
|
|
@@ -1333,14 +1352,6 @@ class PropskitGroup extends HTMLElement {
|
|
|
1333
1352
|
this.open = !this.open;
|
|
1334
1353
|
};
|
|
1335
1354
|
|
|
1336
|
-
#handleHeaderKeyDown = (e) => {
|
|
1337
|
-
if (this.#isResetTarget(e.target)) return;
|
|
1338
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
1339
|
-
e.preventDefault();
|
|
1340
|
-
e.stopPropagation();
|
|
1341
|
-
this.open = !this.open;
|
|
1342
|
-
};
|
|
1343
|
-
|
|
1344
1355
|
#handleReset = (e) => {
|
|
1345
1356
|
e.preventDefault();
|
|
1346
1357
|
e.stopPropagation();
|
|
@@ -1522,17 +1533,34 @@ class PropskitGroup extends HTMLElement {
|
|
|
1522
1533
|
|
|
1523
1534
|
if (userHeader) {
|
|
1524
1535
|
this.#header = userHeader;
|
|
1525
|
-
} else if (
|
|
1536
|
+
} else if (
|
|
1537
|
+
!this.#header ||
|
|
1538
|
+
!this.#header.dataset.generated ||
|
|
1539
|
+
this.#header.parentElement !== this
|
|
1540
|
+
) {
|
|
1526
1541
|
this.#header = document.createElement("fig-header");
|
|
1527
1542
|
this.#header.setAttribute("borderless", "");
|
|
1528
1543
|
this.#header.dataset.generated = "true";
|
|
1529
1544
|
this.prepend(this.#header);
|
|
1530
1545
|
}
|
|
1531
1546
|
|
|
1532
|
-
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");
|
|
1533
1561
|
if (!h3) {
|
|
1534
1562
|
h3 = document.createElement("h3");
|
|
1535
|
-
|
|
1563
|
+
disclosure.appendChild(h3);
|
|
1536
1564
|
}
|
|
1537
1565
|
if (!h3.id) h3.id = figLabUniqueId("propskit-group");
|
|
1538
1566
|
if (this.#header.dataset.generated) {
|
|
@@ -1558,21 +1586,21 @@ class PropskitGroup extends HTMLElement {
|
|
|
1558
1586
|
}
|
|
1559
1587
|
this.#chevron = h3.querySelector(".propskit-group-chevron");
|
|
1560
1588
|
this.#syncResetButton();
|
|
1561
|
-
this.#header.
|
|
1562
|
-
this.#header.
|
|
1563
|
-
this.#header.
|
|
1564
|
-
this.#
|
|
1565
|
-
this.#
|
|
1566
|
-
this.#
|
|
1567
|
-
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);
|
|
1568
1596
|
|
|
1569
1597
|
if (!this.hasAttribute("open")) {
|
|
1570
1598
|
this.setAttribute("open", "false");
|
|
1571
|
-
this.#
|
|
1599
|
+
this.#disclosure.setAttribute("aria-expanded", "false");
|
|
1572
1600
|
}
|
|
1573
1601
|
}
|
|
1574
1602
|
}
|
|
1575
|
-
|
|
1603
|
+
figLabDefineElement("propskit-group", PropskitGroup);
|
|
1576
1604
|
|
|
1577
1605
|
/* Field + Slider wrapper */
|
|
1578
1606
|
class PropskitSlider extends HTMLElement {
|
|
@@ -1601,6 +1629,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
1601
1629
|
#boundHandleRangeDoubleClick = this.#handleRangeDoubleClick.bind(this);
|
|
1602
1630
|
#boundHandleContextMenu = this.#handleContextMenu.bind(this);
|
|
1603
1631
|
#boundHandleContextMenuChange = this.#handleContextMenuChange.bind(this);
|
|
1632
|
+
#boundHandleClick = this.#handleClick.bind(this);
|
|
1604
1633
|
#ignoredSliderAttrs = new Set([
|
|
1605
1634
|
"variant",
|
|
1606
1635
|
"color",
|
|
@@ -1638,6 +1667,16 @@ class PropskitSlider extends HTMLElement {
|
|
|
1638
1667
|
});
|
|
1639
1668
|
this.removeEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
1640
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
|
+
);
|
|
1641
1680
|
|
|
1642
1681
|
if (!this.#observer) {
|
|
1643
1682
|
this.#observer = new MutationObserver((mutations) => {
|
|
@@ -1693,6 +1732,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
1693
1732
|
capture: true,
|
|
1694
1733
|
});
|
|
1695
1734
|
this.removeEventListener("contextmenu", this.#boundHandleContextMenu);
|
|
1735
|
+
this.removeEventListener("click", this.#boundHandleClick, true);
|
|
1696
1736
|
this.#contextMenu?.removeEventListener("change", this.#boundHandleContextMenuChange);
|
|
1697
1737
|
}
|
|
1698
1738
|
|
|
@@ -1792,8 +1832,10 @@ class PropskitSlider extends HTMLElement {
|
|
|
1792
1832
|
|
|
1793
1833
|
for (const attrName of hostAttrs) {
|
|
1794
1834
|
if (attrName === "text") continue;
|
|
1795
|
-
const value = this.getAttribute(attrName);
|
|
1796
|
-
this.#slider.
|
|
1835
|
+
const value = this.getAttribute(attrName) ?? "";
|
|
1836
|
+
if (this.#slider.getAttribute(attrName) !== value) {
|
|
1837
|
+
this.#slider.setAttribute(attrName, value);
|
|
1838
|
+
}
|
|
1797
1839
|
}
|
|
1798
1840
|
|
|
1799
1841
|
this.#slider.removeAttribute("variant");
|
|
@@ -1869,8 +1911,45 @@ class PropskitSlider extends HTMLElement {
|
|
|
1869
1911
|
if (rangeInput !== this.#rangeInput) {
|
|
1870
1912
|
this.#bindRangeInput(rangeInput);
|
|
1871
1913
|
}
|
|
1872
|
-
rangeInput
|
|
1873
|
-
|
|
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
|
+
});
|
|
1874
1953
|
}
|
|
1875
1954
|
|
|
1876
1955
|
#bindRangeInput(rangeInput) {
|
|
@@ -1898,6 +1977,9 @@ class PropskitSlider extends HTMLElement {
|
|
|
1898
1977
|
|
|
1899
1978
|
#handleElasticPointerDown(event) {
|
|
1900
1979
|
if (event.button !== 0 || figLabBooleanAttribute(this, "disabled")) return;
|
|
1980
|
+
if (!event.target?.closest?.("fig-input-number, fig-menu")) {
|
|
1981
|
+
this.#queueRangeFocus();
|
|
1982
|
+
}
|
|
1901
1983
|
if (this.getAttribute("elastic") === "false") return;
|
|
1902
1984
|
if (event.target?.closest?.("fig-input-number")) return;
|
|
1903
1985
|
const rangeInput =
|
|
@@ -2110,6 +2192,7 @@ class PropskitSlider extends HTMLElement {
|
|
|
2110
2192
|
#setSliderValue(value, eventType) {
|
|
2111
2193
|
if (!this.#slider || value === null || value === undefined) return;
|
|
2112
2194
|
this.#slider.value = value;
|
|
2195
|
+
this.setAttribute("value", String(this.#slider.value));
|
|
2113
2196
|
this.dispatchEvent(
|
|
2114
2197
|
new CustomEvent(eventType, {
|
|
2115
2198
|
detail: this.#slider.value,
|
|
@@ -2174,6 +2257,9 @@ class PropskitSlider extends HTMLElement {
|
|
|
2174
2257
|
event instanceof CustomEvent && event.detail !== undefined
|
|
2175
2258
|
? event.detail
|
|
2176
2259
|
: this.#slider?.value;
|
|
2260
|
+
if (this.#slider?.value !== undefined) {
|
|
2261
|
+
this.setAttribute("value", String(this.#slider.value));
|
|
2262
|
+
}
|
|
2177
2263
|
this.dispatchEvent(
|
|
2178
2264
|
new CustomEvent(type, {
|
|
2179
2265
|
detail,
|
|
@@ -2185,14 +2271,32 @@ class PropskitSlider extends HTMLElement {
|
|
|
2185
2271
|
}
|
|
2186
2272
|
|
|
2187
2273
|
focus(options) {
|
|
2188
|
-
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;
|
|
2189
2293
|
}
|
|
2190
2294
|
|
|
2191
2295
|
resetToDefault() {
|
|
2192
2296
|
this.#resetToDefault();
|
|
2193
2297
|
}
|
|
2194
2298
|
}
|
|
2195
|
-
|
|
2299
|
+
figLabDefineElement("propskit-slider", PropskitSlider);
|
|
2196
2300
|
|
|
2197
2301
|
/* Canvas Control */
|
|
2198
2302
|
class FigCanvasControl extends HTMLElement {
|
|
@@ -2236,6 +2340,8 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2236
2340
|
#rotateCursorPrevBodyCursor = "";
|
|
2237
2341
|
#rotateCursorPrevBodyCursorPriority = "";
|
|
2238
2342
|
#boundRotateCursorEnd = null;
|
|
2343
|
+
#activeGestureController = null;
|
|
2344
|
+
#activeGestureFinish = null;
|
|
2239
2345
|
|
|
2240
2346
|
get #type() {
|
|
2241
2347
|
return this.getAttribute("type") || "point";
|
|
@@ -2334,6 +2440,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2334
2440
|
}
|
|
2335
2441
|
|
|
2336
2442
|
disconnectedCallback() {
|
|
2443
|
+
this.#cancelActiveGesture();
|
|
2337
2444
|
this.#teardownRadiusDrag();
|
|
2338
2445
|
this.#deactivateMoveCursor();
|
|
2339
2446
|
this.#deactivateRotateCursor();
|
|
@@ -2355,6 +2462,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2355
2462
|
else this.#render();
|
|
2356
2463
|
}
|
|
2357
2464
|
if (name === "type") {
|
|
2465
|
+
this.#cancelActiveGesture();
|
|
2358
2466
|
this.#parseValue();
|
|
2359
2467
|
this.#render();
|
|
2360
2468
|
}
|
|
@@ -2363,6 +2471,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2363
2471
|
else this.#pointHandle.removeAttribute("color");
|
|
2364
2472
|
}
|
|
2365
2473
|
if (name === "disabled") {
|
|
2474
|
+
this.#cancelActiveGesture();
|
|
2366
2475
|
this.#render();
|
|
2367
2476
|
}
|
|
2368
2477
|
if (name === "tooltips") {
|
|
@@ -2434,6 +2543,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2434
2543
|
}
|
|
2435
2544
|
|
|
2436
2545
|
#render() {
|
|
2546
|
+
this.#cancelActiveGesture();
|
|
2437
2547
|
this.innerHTML = "";
|
|
2438
2548
|
this.#pointHandle = null;
|
|
2439
2549
|
this.#secondHandle = null;
|
|
@@ -2856,7 +2966,6 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2856
2966
|
e.stopPropagation();
|
|
2857
2967
|
const container = this.#container;
|
|
2858
2968
|
if (!container) return;
|
|
2859
|
-
const rect0 = container.getBoundingClientRect();
|
|
2860
2969
|
const startX = e.clientX;
|
|
2861
2970
|
const startY = e.clientY;
|
|
2862
2971
|
const x0 = this.#x;
|
|
@@ -2888,21 +2997,24 @@ class FigCanvasControl extends HTMLElement {
|
|
|
2888
2997
|
this.#emitInput();
|
|
2889
2998
|
};
|
|
2890
2999
|
|
|
2891
|
-
const
|
|
3000
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
2892
3001
|
document.body.classList.remove("fig-lab-move-active");
|
|
2893
3002
|
hitLine.style.pointerEvents = "stroke";
|
|
2894
|
-
this.#
|
|
2895
|
-
this.#
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
});
|
|
2902
|
-
};
|
|
3003
|
+
this.#isDragging = false;
|
|
3004
|
+
this.#isSecondDragging = false;
|
|
3005
|
+
if (commit) {
|
|
3006
|
+
this.#syncValueAttribute();
|
|
3007
|
+
this.#emitChange();
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
2903
3010
|
|
|
2904
|
-
window.addEventListener("pointermove", onMove);
|
|
2905
|
-
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
|
+
});
|
|
2906
3018
|
});
|
|
2907
3019
|
}
|
|
2908
3020
|
|
|
@@ -3060,7 +3172,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3060
3172
|
svg.style.top = "0";
|
|
3061
3173
|
svg.setAttribute("viewBox", `0 0 ${rect.width} ${rect.height}`);
|
|
3062
3174
|
const lines = svg.querySelectorAll(
|
|
3063
|
-
".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",
|
|
3064
3176
|
);
|
|
3065
3177
|
for (const line of lines) {
|
|
3066
3178
|
line.setAttribute("x1", String(cx));
|
|
@@ -3248,19 +3360,24 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3248
3360
|
this.#emitInput();
|
|
3249
3361
|
};
|
|
3250
3362
|
|
|
3251
|
-
const
|
|
3363
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
3252
3364
|
this.#isAngleDragging = false;
|
|
3253
3365
|
this.classList.remove("fig-canvas-control-ring-active");
|
|
3254
3366
|
this.#angleHandle.removeAttribute("selected");
|
|
3255
3367
|
if (this.#angleTooltip) this.#angleTooltip.removeAttribute("show");
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
};
|
|
3368
|
+
if (commit) {
|
|
3369
|
+
this.#syncValueAttribute();
|
|
3370
|
+
this.#emitChange();
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3261
3373
|
|
|
3262
|
-
window.addEventListener("pointermove", onMove);
|
|
3263
|
-
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
|
+
});
|
|
3264
3381
|
});
|
|
3265
3382
|
}
|
|
3266
3383
|
|
|
@@ -3350,17 +3467,22 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3350
3467
|
this.#emitInput();
|
|
3351
3468
|
};
|
|
3352
3469
|
|
|
3353
|
-
const
|
|
3470
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
3354
3471
|
this.#isDragging = false;
|
|
3355
3472
|
if (tooltip) tooltip.removeAttribute("show");
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
};
|
|
3473
|
+
if (commit) {
|
|
3474
|
+
this.#syncValueAttribute();
|
|
3475
|
+
this.#emitChange();
|
|
3476
|
+
}
|
|
3477
|
+
});
|
|
3361
3478
|
|
|
3362
|
-
window.addEventListener("pointermove", onMove);
|
|
3363
|
-
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
|
+
});
|
|
3364
3486
|
});
|
|
3365
3487
|
}
|
|
3366
3488
|
|
|
@@ -3456,7 +3578,7 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3456
3578
|
this.#emitInput();
|
|
3457
3579
|
};
|
|
3458
3580
|
|
|
3459
|
-
const
|
|
3581
|
+
const gesture = this.#beginActiveGesture((commit) => {
|
|
3460
3582
|
this.#isRadiusDragging = false;
|
|
3461
3583
|
this.classList.remove("fig-canvas-control-ring-active");
|
|
3462
3584
|
circle.style.pointerEvents = "";
|
|
@@ -3468,14 +3590,19 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3468
3590
|
}
|
|
3469
3591
|
document.body.style.cursor = prevBodyCursor;
|
|
3470
3592
|
if (this.#radiusTooltip) this.#radiusTooltip.removeAttribute("show");
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
};
|
|
3593
|
+
if (commit) {
|
|
3594
|
+
this.#syncValueAttribute();
|
|
3595
|
+
this.#emitChange();
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3476
3598
|
|
|
3477
|
-
window.addEventListener("pointermove", onMove);
|
|
3478
|
-
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
|
+
});
|
|
3479
3606
|
};
|
|
3480
3607
|
circle.addEventListener("pointerdown", onDown);
|
|
3481
3608
|
this._radiusDragCleanup = () =>
|
|
@@ -3488,8 +3615,34 @@ class FigCanvasControl extends HTMLElement {
|
|
|
3488
3615
|
this._radiusDragCleanup = null;
|
|
3489
3616
|
}
|
|
3490
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
|
+
}
|
|
3491
3644
|
}
|
|
3492
|
-
|
|
3645
|
+
figLabDefineElement("fig-canvas-control", FigCanvasControl);
|
|
3493
3646
|
|
|
3494
3647
|
/* Oscillator Input */
|
|
3495
3648
|
/**
|
|
@@ -3522,6 +3675,8 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3522
3675
|
#expandedWaveIndices = new Set();
|
|
3523
3676
|
#resizeObserver = null;
|
|
3524
3677
|
#activeFieldInput = null;
|
|
3678
|
+
#dragController = null;
|
|
3679
|
+
#dragCleanup = null;
|
|
3525
3680
|
|
|
3526
3681
|
static TYPES = [
|
|
3527
3682
|
{ name: "Wave", value: "sine" },
|
|
@@ -3553,7 +3708,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3553
3708
|
}
|
|
3554
3709
|
|
|
3555
3710
|
disconnectedCallback() {
|
|
3556
|
-
this.#
|
|
3711
|
+
this.#cancelDrag();
|
|
3557
3712
|
this.#stopPlayhead();
|
|
3558
3713
|
if (this.#resizeObserver) {
|
|
3559
3714
|
this.#resizeObserver.disconnect();
|
|
@@ -3758,6 +3913,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3758
3913
|
}
|
|
3759
3914
|
|
|
3760
3915
|
#render() {
|
|
3916
|
+
this.#cancelDrag();
|
|
3761
3917
|
this.#stopPlayhead();
|
|
3762
3918
|
this.innerHTML = this.#getInnerHTML();
|
|
3763
3919
|
this.#cacheRefs();
|
|
@@ -3768,7 +3924,8 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3768
3924
|
}
|
|
3769
3925
|
|
|
3770
3926
|
#getInnerHTML() {
|
|
3771
|
-
const
|
|
3927
|
+
const interactive = this.#isEditEnabled() && !this.#isDisabled();
|
|
3928
|
+
const disabled = interactive ? "" : " disabled";
|
|
3772
3929
|
|
|
3773
3930
|
return `<div class="propskit-oscillator-svg-container">
|
|
3774
3931
|
<svg viewBox="0 0 ${this.#drawWidth} ${this.#drawHeight}" class="propskit-oscillator-svg">
|
|
@@ -3776,8 +3933,8 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3776
3933
|
<line class="propskit-oscillator-baseline"></line>
|
|
3777
3934
|
<path class="propskit-oscillator-path"></path>
|
|
3778
3935
|
<circle class="propskit-oscillator-playhead"></circle>
|
|
3779
|
-
|
|
3780
|
-
<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>` : ""}
|
|
3781
3938
|
</svg>
|
|
3782
3939
|
</div>
|
|
3783
3940
|
${this.#isEditEnabled() ? this.#getWaveControlsHTML(disabled) : ""}`;
|
|
@@ -3801,10 +3958,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3801
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>
|
|
3802
3959
|
</fig-tooltip>
|
|
3803
3960
|
<fig-tooltip text="Add form">
|
|
3804
|
-
|
|
3805
|
-
<fig-icon name="add"></fig-icon>
|
|
3806
|
-
${this.#getWaveTypeDropdownHTML("propskit-oscillator-add-type", "sine", disabled, index)}
|
|
3807
|
-
</fig-button>
|
|
3961
|
+
${this.#getWaveTypeSelectHTML("propskit-oscillator-add-type propskit-oscillator-add-type-button", "sine", disabled, index)}
|
|
3808
3962
|
</fig-tooltip>
|
|
3809
3963
|
</fig-header>
|
|
3810
3964
|
<div class="propskit-oscillator-fields" data-wave-index="${index}"${active}>
|
|
@@ -3816,17 +3970,17 @@ class PropskitOscillator extends HTMLElement {
|
|
|
3816
3970
|
</fig-group>`;
|
|
3817
3971
|
}
|
|
3818
3972
|
|
|
3819
|
-
#
|
|
3973
|
+
#getWaveTypeSelectHTML(className, value, disabled, index = null) {
|
|
3820
3974
|
const options = PropskitOscillator.TYPES.map((type) => {
|
|
3821
3975
|
const selected = type.value === value ? " selected" : "";
|
|
3822
|
-
return `<option value="${type.value}"${selected}>
|
|
3823
|
-
|
|
3824
|
-
<
|
|
3825
|
-
</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>`;
|
|
3826
3980
|
}).join("");
|
|
3827
3981
|
const indexAttr =
|
|
3828
3982
|
index === null ? "" : ` data-wave-index="${PropskitOscillator.#escapeAttribute(String(index))}"`;
|
|
3829
|
-
return `<fig-
|
|
3983
|
+
return `<fig-select class="${className}" value="${value}" label="Add form"${indexAttr}${disabled}><fig-select-options>${options}</fig-select-options></fig-select>`;
|
|
3830
3984
|
}
|
|
3831
3985
|
|
|
3832
3986
|
#getNumberFieldHTML(index, name, label, min, max, step, units) {
|
|
@@ -4033,6 +4187,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4033
4187
|
}
|
|
4034
4188
|
|
|
4035
4189
|
#setupEvents() {
|
|
4190
|
+
if (!this.#isEditEnabled() || this.#isDisabled()) return;
|
|
4036
4191
|
for (const typeControl of this.#typeControls) {
|
|
4037
4192
|
typeControl.addEventListener("change", (event) => {
|
|
4038
4193
|
if (this.#isDisabled()) return;
|
|
@@ -4267,6 +4422,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4267
4422
|
}
|
|
4268
4423
|
|
|
4269
4424
|
#startDrag(event, type) {
|
|
4425
|
+
this.#cancelDrag();
|
|
4270
4426
|
this.#isDragging = type;
|
|
4271
4427
|
this.#svg?.classList.add("dragging");
|
|
4272
4428
|
const dragCursor =
|
|
@@ -4307,19 +4463,42 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4307
4463
|
this.#emit("input");
|
|
4308
4464
|
};
|
|
4309
4465
|
|
|
4310
|
-
const
|
|
4466
|
+
const controller = new AbortController();
|
|
4467
|
+
const finish = (commit = false) => {
|
|
4311
4468
|
this.#isDragging = null;
|
|
4312
4469
|
this.#svg?.classList.remove("dragging");
|
|
4313
4470
|
if (dragCursor) {
|
|
4314
4471
|
document.body.style.cursor = prevBodyCursor;
|
|
4315
4472
|
}
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4473
|
+
controller.abort();
|
|
4474
|
+
if (this.#dragController === controller) {
|
|
4475
|
+
this.#dragController = null;
|
|
4476
|
+
this.#dragCleanup = null;
|
|
4477
|
+
}
|
|
4478
|
+
if (commit) this.#emit("change");
|
|
4319
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
|
+
}
|
|
4320
4494
|
|
|
4321
|
-
|
|
4322
|
-
|
|
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");
|
|
4323
4502
|
}
|
|
4324
4503
|
|
|
4325
4504
|
#sampleAtWithoutWave(excludedIndex, t) {
|
|
@@ -4391,7 +4570,7 @@ class PropskitOscillator extends HTMLElement {
|
|
|
4391
4570
|
);
|
|
4392
4571
|
}
|
|
4393
4572
|
}
|
|
4394
|
-
|
|
4573
|
+
figLabDefineElement("propskit-oscillator", PropskitOscillator);
|
|
4395
4574
|
|
|
4396
4575
|
/* Angle Input */
|
|
4397
4576
|
/**
|
|
@@ -4415,6 +4594,9 @@ class FigInputAngle extends HTMLElement {
|
|
|
4415
4594
|
#boundHandleKeyDown;
|
|
4416
4595
|
#boundHandleKeyUp;
|
|
4417
4596
|
#boundHandleAngleInput;
|
|
4597
|
+
#boundHandleDialKeyDown;
|
|
4598
|
+
#gestureController = null;
|
|
4599
|
+
#gestureCleanup = null;
|
|
4418
4600
|
|
|
4419
4601
|
constructor() {
|
|
4420
4602
|
super();
|
|
@@ -4440,10 +4622,12 @@ class FigInputAngle extends HTMLElement {
|
|
|
4440
4622
|
this.#boundHandleKeyDown = this.#handleKeyDown.bind(this);
|
|
4441
4623
|
this.#boundHandleKeyUp = this.#handleKeyUp.bind(this);
|
|
4442
4624
|
this.#boundHandleAngleInput = this.#handleAngleInput.bind(this);
|
|
4625
|
+
this.#boundHandleDialKeyDown = this.#handleDialKeyDown.bind(this);
|
|
4443
4626
|
}
|
|
4444
4627
|
|
|
4445
4628
|
connectedCallback() {
|
|
4446
4629
|
requestAnimationFrame(() => {
|
|
4630
|
+
if (!this.isConnected) return;
|
|
4447
4631
|
this.precision = this.getAttribute("precision") || 1;
|
|
4448
4632
|
this.precision = parseInt(this.precision);
|
|
4449
4633
|
this.text = this.getAttribute("text") === "true";
|
|
@@ -4475,10 +4659,13 @@ class FigInputAngle extends HTMLElement {
|
|
|
4475
4659
|
}
|
|
4476
4660
|
|
|
4477
4661
|
disconnectedCallback() {
|
|
4662
|
+
this.#cancelGesture();
|
|
4478
4663
|
this.#cleanupListeners();
|
|
4479
4664
|
}
|
|
4480
4665
|
|
|
4481
4666
|
#render() {
|
|
4667
|
+
this.#cancelGesture();
|
|
4668
|
+
this.#cleanupListeners();
|
|
4482
4669
|
this.innerHTML = this.#getInnerHTML();
|
|
4483
4670
|
}
|
|
4484
4671
|
|
|
@@ -4505,10 +4692,23 @@ class FigInputAngle extends HTMLElement {
|
|
|
4505
4692
|
const step = this.#getStepForUnit();
|
|
4506
4693
|
const minAttr = this.min !== null ? `min="${this.min}"` : "";
|
|
4507
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);
|
|
4508
4700
|
return `
|
|
4509
4701
|
${
|
|
4510
4702
|
this.dial
|
|
4511
|
-
? `<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"' : ""}>
|
|
4512
4712
|
<div class="fig-input-angle-handle"></div>
|
|
4513
4713
|
</div>`
|
|
4514
4714
|
: ""
|
|
@@ -4521,7 +4721,9 @@ class FigInputAngle extends HTMLElement {
|
|
|
4521
4721
|
value="${this.angle}"
|
|
4522
4722
|
${minAttr}
|
|
4523
4723
|
${maxAttr}
|
|
4524
|
-
units="${this.units}"
|
|
4724
|
+
units="${this.units}"
|
|
4725
|
+
aria-label="${FigInputAngle.#escapeAttribute(name)}"
|
|
4726
|
+
${disabled ? "disabled" : ""}>
|
|
4525
4727
|
${this.showRotations ? `<span slot="append" class="fig-input-angle-rotations"></span>` : ""}
|
|
4526
4728
|
</fig-input-number>`
|
|
4527
4729
|
: ""
|
|
@@ -4534,6 +4736,57 @@ class FigInputAngle extends HTMLElement {
|
|
|
4534
4736
|
return Math.floor(degrees / 360);
|
|
4535
4737
|
}
|
|
4536
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
|
+
|
|
4537
4790
|
#updateRotationDisplay() {
|
|
4538
4791
|
if (!this.rotationSpan) return;
|
|
4539
4792
|
const rotations = this.#getRotationCount();
|
|
@@ -4609,10 +4862,12 @@ class FigInputAngle extends HTMLElement {
|
|
|
4609
4862
|
this.#updateRotationDisplay();
|
|
4610
4863
|
this.plane?.addEventListener("mousedown", this.#boundHandleMouseDown);
|
|
4611
4864
|
this.plane?.addEventListener("touchstart", this.#boundHandleTouchStart);
|
|
4865
|
+
this.plane?.addEventListener("keydown", this.#boundHandleDialKeyDown);
|
|
4612
4866
|
window.addEventListener("keydown", this.#boundHandleKeyDown);
|
|
4613
4867
|
window.addEventListener("keyup", this.#boundHandleKeyUp);
|
|
4614
4868
|
if (this.text && this.angleInput) {
|
|
4615
4869
|
this.angleInput.addEventListener("input", this.#boundHandleAngleInput);
|
|
4870
|
+
this.angleInput.addEventListener("change", this.#boundHandleAngleInput);
|
|
4616
4871
|
}
|
|
4617
4872
|
this.addEventListener("change", this.#boundHandleRawChange, true);
|
|
4618
4873
|
}
|
|
@@ -4620,10 +4875,12 @@ class FigInputAngle extends HTMLElement {
|
|
|
4620
4875
|
#cleanupListeners() {
|
|
4621
4876
|
this.plane?.removeEventListener("mousedown", this.#boundHandleMouseDown);
|
|
4622
4877
|
this.plane?.removeEventListener("touchstart", this.#boundHandleTouchStart);
|
|
4878
|
+
this.plane?.removeEventListener("keydown", this.#boundHandleDialKeyDown);
|
|
4623
4879
|
window.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
4624
4880
|
window.removeEventListener("keyup", this.#boundHandleKeyUp);
|
|
4625
4881
|
if (this.text && this.angleInput) {
|
|
4626
4882
|
this.angleInput.removeEventListener("input", this.#boundHandleAngleInput);
|
|
4883
|
+
this.angleInput.removeEventListener("change", this.#boundHandleAngleInput);
|
|
4627
4884
|
}
|
|
4628
4885
|
this.removeEventListener("change", this.#boundHandleRawChange, true);
|
|
4629
4886
|
}
|
|
@@ -4645,12 +4902,10 @@ class FigInputAngle extends HTMLElement {
|
|
|
4645
4902
|
|
|
4646
4903
|
#handleAngleInput(e) {
|
|
4647
4904
|
e.stopPropagation();
|
|
4648
|
-
|
|
4649
|
-
this.#
|
|
4650
|
-
this.#
|
|
4651
|
-
this.#
|
|
4652
|
-
this.#emitInputEvent();
|
|
4653
|
-
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();
|
|
4654
4909
|
}
|
|
4655
4910
|
|
|
4656
4911
|
#calculateAdjacentAndOpposite() {
|
|
@@ -4683,7 +4938,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
4683
4938
|
const isBounded = this.min !== null || this.max !== null;
|
|
4684
4939
|
|
|
4685
4940
|
if (isBounded) {
|
|
4686
|
-
this.angle = this.#fromDegrees(normalizedAngle);
|
|
4941
|
+
this.angle = this.#clampValue(this.#fromDegrees(normalizedAngle));
|
|
4687
4942
|
} else {
|
|
4688
4943
|
if (this.#prevRawAngle === null) {
|
|
4689
4944
|
this.#prevRawAngle = normalizedAngle;
|
|
@@ -4692,23 +4947,25 @@ class FigInputAngle extends HTMLElement {
|
|
|
4692
4947
|
let delta = normalizedAngle - currentMod;
|
|
4693
4948
|
if (delta > 180) delta -= 360;
|
|
4694
4949
|
if (delta < -180) delta += 360;
|
|
4695
|
-
this.angle
|
|
4950
|
+
this.angle = this.#clampValue(this.angle + this.#fromDegrees(delta));
|
|
4696
4951
|
} else {
|
|
4697
4952
|
let delta = normalizedAngle - this.#prevRawAngle;
|
|
4698
4953
|
if (delta > 180) delta -= 360;
|
|
4699
4954
|
if (delta < -180) delta += 360;
|
|
4700
|
-
this.angle
|
|
4955
|
+
this.angle = this.#clampValue(this.angle + this.#fromDegrees(delta));
|
|
4701
4956
|
this.#prevRawAngle = normalizedAngle;
|
|
4702
4957
|
}
|
|
4703
4958
|
}
|
|
4704
4959
|
|
|
4705
4960
|
this.#calculateAdjacentAndOpposite();
|
|
4961
|
+
this.setAttribute("value", String(this.angle));
|
|
4706
4962
|
|
|
4707
4963
|
this.#syncHandlePosition();
|
|
4708
4964
|
if (this.text && this.angleInput) {
|
|
4709
4965
|
this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
|
|
4710
4966
|
}
|
|
4711
4967
|
this.#updateRotationDisplay();
|
|
4968
|
+
this.#syncDialState();
|
|
4712
4969
|
|
|
4713
4970
|
this.#emitInputEvent();
|
|
4714
4971
|
}
|
|
@@ -4745,6 +5002,8 @@ class FigInputAngle extends HTMLElement {
|
|
|
4745
5002
|
}
|
|
4746
5003
|
|
|
4747
5004
|
#handleMouseDown(e) {
|
|
5005
|
+
if (this.#isDisabled() || e.button !== 0) return;
|
|
5006
|
+
this.#cancelGesture();
|
|
4748
5007
|
this.isDragging = true;
|
|
4749
5008
|
this.#prevRawAngle = null;
|
|
4750
5009
|
this.#updateAngle(e);
|
|
@@ -4754,21 +5013,36 @@ class FigInputAngle extends HTMLElement {
|
|
|
4754
5013
|
if (this.isDragging) this.#updateAngle(e);
|
|
4755
5014
|
};
|
|
4756
5015
|
|
|
4757
|
-
const
|
|
5016
|
+
const controller = new AbortController();
|
|
5017
|
+
const finish = (commit = false) => {
|
|
4758
5018
|
this.isDragging = false;
|
|
4759
5019
|
this.#prevRawAngle = null;
|
|
4760
5020
|
this.plane.classList.remove("dragging");
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
5021
|
+
controller.abort();
|
|
5022
|
+
if (this.#gestureController === controller) {
|
|
5023
|
+
this.#gestureController = null;
|
|
5024
|
+
this.#gestureCleanup = null;
|
|
5025
|
+
}
|
|
5026
|
+
if (commit) this.#emitChangeEvent();
|
|
4764
5027
|
};
|
|
5028
|
+
this.#gestureController = controller;
|
|
5029
|
+
this.#gestureCleanup = finish;
|
|
4765
5030
|
|
|
4766
|
-
window.addEventListener("mousemove", handleMouseMove
|
|
4767
|
-
|
|
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
|
+
});
|
|
4768
5040
|
}
|
|
4769
5041
|
|
|
4770
5042
|
#handleTouchStart(e) {
|
|
5043
|
+
if (this.#isDisabled()) return;
|
|
4771
5044
|
e.preventDefault();
|
|
5045
|
+
this.#cancelGesture();
|
|
4772
5046
|
this.isDragging = true;
|
|
4773
5047
|
this.#prevRawAngle = null;
|
|
4774
5048
|
this.#updateAngle(e.touches[0]);
|
|
@@ -4778,17 +5052,56 @@ class FigInputAngle extends HTMLElement {
|
|
|
4778
5052
|
if (this.isDragging) this.#updateAngle(e.touches[0]);
|
|
4779
5053
|
};
|
|
4780
5054
|
|
|
4781
|
-
const
|
|
5055
|
+
const controller = new AbortController();
|
|
5056
|
+
const finish = (commit = false) => {
|
|
4782
5057
|
this.isDragging = false;
|
|
4783
5058
|
this.#prevRawAngle = null;
|
|
4784
5059
|
this.plane.classList.remove("dragging");
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5060
|
+
controller.abort();
|
|
5061
|
+
if (this.#gestureController === controller) {
|
|
5062
|
+
this.#gestureController = null;
|
|
5063
|
+
this.#gestureCleanup = null;
|
|
5064
|
+
}
|
|
5065
|
+
if (commit) this.#emitChangeEvent();
|
|
4788
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
|
+
}
|
|
5080
|
+
|
|
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
|
+
}
|
|
4789
5090
|
|
|
4790
|
-
|
|
4791
|
-
|
|
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();
|
|
4792
5105
|
}
|
|
4793
5106
|
|
|
4794
5107
|
#handleKeyDown(e) {
|
|
@@ -4814,6 +5127,9 @@ class FigInputAngle extends HTMLElement {
|
|
|
4814
5127
|
"dial",
|
|
4815
5128
|
"rotations",
|
|
4816
5129
|
"show-rotations",
|
|
5130
|
+
"disabled",
|
|
5131
|
+
"aria-label",
|
|
5132
|
+
"name",
|
|
4817
5133
|
];
|
|
4818
5134
|
}
|
|
4819
5135
|
|
|
@@ -4834,20 +5150,14 @@ class FigInputAngle extends HTMLElement {
|
|
|
4834
5150
|
console.error("Invalid value: must be a number.");
|
|
4835
5151
|
return;
|
|
4836
5152
|
}
|
|
4837
|
-
this
|
|
4838
|
-
this.#calculateAdjacentAndOpposite();
|
|
4839
|
-
this.#syncHandlePosition();
|
|
4840
|
-
if (this.angleInput) {
|
|
4841
|
-
this.angleInput.setAttribute("value", this.angle.toFixed(this.precision));
|
|
4842
|
-
}
|
|
4843
|
-
this.#updateRotationDisplay();
|
|
5153
|
+
this.#setValue(Number(value));
|
|
4844
5154
|
}
|
|
4845
5155
|
|
|
4846
5156
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
4847
5157
|
switch (name) {
|
|
4848
5158
|
case "value":
|
|
4849
5159
|
if (this.isDragging) break;
|
|
4850
|
-
|
|
5160
|
+
if (newValue !== null) this.#setValue(Number(newValue), { reflect: false });
|
|
4851
5161
|
break;
|
|
4852
5162
|
case "precision":
|
|
4853
5163
|
this.precision = parseInt(newValue);
|
|
@@ -4883,6 +5193,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
4883
5193
|
}
|
|
4884
5194
|
case "min":
|
|
4885
5195
|
this.min = newValue !== null ? Number(newValue) : null;
|
|
5196
|
+
this.#setValue(this.angle);
|
|
4886
5197
|
if (this.isConnected) {
|
|
4887
5198
|
this.#render();
|
|
4888
5199
|
this.#setupListeners();
|
|
@@ -4891,6 +5202,23 @@ class FigInputAngle extends HTMLElement {
|
|
|
4891
5202
|
break;
|
|
4892
5203
|
case "max":
|
|
4893
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":
|
|
4894
5222
|
if (this.isConnected) {
|
|
4895
5223
|
this.#render();
|
|
4896
5224
|
this.#setupListeners();
|
|
@@ -4909,7 +5237,7 @@ class FigInputAngle extends HTMLElement {
|
|
|
4909
5237
|
}
|
|
4910
5238
|
}
|
|
4911
5239
|
}
|
|
4912
|
-
|
|
5240
|
+
figLabDefineElement("fig-input-angle", FigInputAngle);
|
|
4913
5241
|
|
|
4914
5242
|
/* Reorder wrapper */
|
|
4915
5243
|
class FigReorder extends HTMLElement {
|
|
@@ -4949,9 +5277,15 @@ class FigReorder extends HTMLElement {
|
|
|
4949
5277
|
#bindings = new Map();
|
|
4950
5278
|
#drag = null;
|
|
4951
5279
|
#indicator = null;
|
|
5280
|
+
#liveRegion = null;
|
|
4952
5281
|
|
|
4953
5282
|
connectedCallback() {
|
|
4954
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();
|
|
4955
5289
|
this.#syncChildren();
|
|
4956
5290
|
this.#childObserver = new MutationObserver(() => this.#syncChildren());
|
|
4957
5291
|
this.#childObserver.observe(this, { childList: true });
|
|
@@ -4963,10 +5297,15 @@ class FigReorder extends HTMLElement {
|
|
|
4963
5297
|
this.#unbindAll();
|
|
4964
5298
|
this.#cancelDrag();
|
|
4965
5299
|
this.#removeIndicator();
|
|
5300
|
+
this.#liveRegion?.remove();
|
|
5301
|
+
this.#liveRegion = null;
|
|
4966
5302
|
}
|
|
4967
5303
|
|
|
4968
5304
|
attributeChangedCallback() {
|
|
4969
|
-
if (this.isConnected)
|
|
5305
|
+
if (this.isConnected) {
|
|
5306
|
+
if (this.#disabled) this.#cancelDrag();
|
|
5307
|
+
this.#syncChildren();
|
|
5308
|
+
}
|
|
4970
5309
|
}
|
|
4971
5310
|
|
|
4972
5311
|
get #disabled() {
|
|
@@ -4985,7 +5324,11 @@ class FigReorder extends HTMLElement {
|
|
|
4985
5324
|
}
|
|
4986
5325
|
|
|
4987
5326
|
#getElementChildren() {
|
|
4988
|
-
return [...this.children].filter(
|
|
5327
|
+
return [...this.children].filter(
|
|
5328
|
+
(node) =>
|
|
5329
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
5330
|
+
!node.hasAttribute("data-reorder-live"),
|
|
5331
|
+
);
|
|
4989
5332
|
}
|
|
4990
5333
|
|
|
4991
5334
|
#syncChildren() {
|
|
@@ -4999,6 +5342,8 @@ class FigReorder extends HTMLElement {
|
|
|
4999
5342
|
binding.onPointerDown,
|
|
5000
5343
|
true,
|
|
5001
5344
|
);
|
|
5345
|
+
binding.target.removeEventListener("keydown", binding.onKeyDown);
|
|
5346
|
+
this.#restoreKeyboardAttrs(binding);
|
|
5002
5347
|
this.#bindings.delete(child);
|
|
5003
5348
|
}
|
|
5004
5349
|
}
|
|
@@ -5034,12 +5379,20 @@ class FigReorder extends HTMLElement {
|
|
|
5034
5379
|
#clearReorderItemMarks(children) {
|
|
5035
5380
|
for (const child of children) {
|
|
5036
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
|
+
}
|
|
5037
5386
|
}
|
|
5038
5387
|
}
|
|
5039
5388
|
|
|
5040
5389
|
#markReorderItems(children) {
|
|
5041
5390
|
for (const child of children) {
|
|
5042
5391
|
child.setAttribute("data-reorder-item", "");
|
|
5392
|
+
if (!child.hasAttribute("role")) {
|
|
5393
|
+
child.setAttribute("role", "listitem");
|
|
5394
|
+
child.setAttribute("data-reorder-generated-role", "");
|
|
5395
|
+
}
|
|
5043
5396
|
}
|
|
5044
5397
|
}
|
|
5045
5398
|
|
|
@@ -5047,7 +5400,10 @@ class FigReorder extends HTMLElement {
|
|
|
5047
5400
|
for (const child of children) {
|
|
5048
5401
|
child
|
|
5049
5402
|
.querySelectorAll("[data-reorder-handle]")
|
|
5050
|
-
.forEach((node) =>
|
|
5403
|
+
.forEach((node) => {
|
|
5404
|
+
node.removeAttribute("data-reorder-handle");
|
|
5405
|
+
node.removeAttribute("aria-roledescription");
|
|
5406
|
+
});
|
|
5051
5407
|
}
|
|
5052
5408
|
}
|
|
5053
5409
|
|
|
@@ -5055,7 +5411,10 @@ class FigReorder extends HTMLElement {
|
|
|
5055
5411
|
if (!this.#handleSelector) return;
|
|
5056
5412
|
for (const child of children) {
|
|
5057
5413
|
const handle = child.querySelector(this.#handleSelector);
|
|
5058
|
-
if (handle)
|
|
5414
|
+
if (handle) {
|
|
5415
|
+
handle.setAttribute("data-reorder-handle", "");
|
|
5416
|
+
handle.setAttribute("aria-roledescription", "reorder handle");
|
|
5417
|
+
}
|
|
5059
5418
|
}
|
|
5060
5419
|
}
|
|
5061
5420
|
|
|
@@ -5078,9 +5437,107 @@ class FigReorder extends HTMLElement {
|
|
|
5078
5437
|
}
|
|
5079
5438
|
this.#startPendingDrag(event, child, target);
|
|
5080
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
|
+
};
|
|
5081
5490
|
|
|
5082
5491
|
target.addEventListener("pointerdown", onPointerDown, true);
|
|
5083
|
-
|
|
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
|
+
});
|
|
5084
5541
|
}
|
|
5085
5542
|
|
|
5086
5543
|
#isInteractiveTarget(target, child) {
|
|
@@ -5298,6 +5755,13 @@ class FigReorder extends HTMLElement {
|
|
|
5298
5755
|
if (ref !== item) this.insertBefore(item, ref);
|
|
5299
5756
|
}
|
|
5300
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
|
+
|
|
5301
5765
|
#finishDrag(state, revert) {
|
|
5302
5766
|
const { item, oldIndex, active, onMove, onUp, onKeyDown } = state;
|
|
5303
5767
|
|
|
@@ -5317,6 +5781,13 @@ class FigReorder extends HTMLElement {
|
|
|
5317
5781
|
detail: { oldIndex, newIndex, item },
|
|
5318
5782
|
}),
|
|
5319
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
|
+
);
|
|
5320
5791
|
}
|
|
5321
5792
|
}
|
|
5322
5793
|
}
|
|
@@ -5334,6 +5805,8 @@ class FigReorder extends HTMLElement {
|
|
|
5334
5805
|
binding.onPointerDown,
|
|
5335
5806
|
true,
|
|
5336
5807
|
);
|
|
5808
|
+
binding.target.removeEventListener("keydown", binding.onKeyDown);
|
|
5809
|
+
this.#restoreKeyboardAttrs(binding);
|
|
5337
5810
|
}
|
|
5338
5811
|
this.#bindings.clear();
|
|
5339
5812
|
}
|
|
@@ -5344,1187 +5817,169 @@ class FigReorder extends HTMLElement {
|
|
|
5344
5817
|
}
|
|
5345
5818
|
}
|
|
5346
5819
|
|
|
5347
|
-
|
|
5820
|
+
figLabDefineElement("fig-reorder", FigReorder);
|
|
5348
5821
|
|
|
5349
|
-
/*
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
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();
|
|
5355
5830
|
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
const text = raw || "";
|
|
5359
|
-
if (text.startsWith("[")) {
|
|
5360
|
-
try {
|
|
5361
|
-
const parsed = JSON.parse(text);
|
|
5362
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
5363
|
-
} catch {
|
|
5364
|
-
/* fall through */
|
|
5365
|
-
}
|
|
5366
|
-
}
|
|
5367
|
-
const delimiter = text.includes("\n") ? "\n" : ",";
|
|
5368
|
-
return text
|
|
5369
|
-
.split(delimiter)
|
|
5370
|
-
.map((s) => s.trim())
|
|
5371
|
-
.filter(Boolean);
|
|
5831
|
+
function figLabSelectOptions(select) {
|
|
5832
|
+
return Array.from(select.querySelectorAll("fig-select-option"));
|
|
5372
5833
|
}
|
|
5373
5834
|
|
|
5374
|
-
function
|
|
5375
|
-
|
|
5376
|
-
|
|
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];
|
|
5377
5846
|
}
|
|
5378
|
-
|
|
5379
|
-
}
|
|
5847
|
+
if (!option) return;
|
|
5380
5848
|
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
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));
|
|
5384
5856
|
}
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
return ["value", "disabled", "selected", "label"];
|
|
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
|
+
);
|
|
5401
5872
|
}
|
|
5873
|
+
}
|
|
5402
5874
|
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
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;
|
|
5407
5883
|
}
|
|
5884
|
+
figLabEnhancedSelects.add(select);
|
|
5408
5885
|
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
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;
|
|
5896
|
+
}
|
|
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;
|
|
5904
|
+
}
|
|
5414
5905
|
}
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
if (val) this.setAttribute("disabled", "");
|
|
5423
|
-
else this.removeAttribute("disabled");
|
|
5424
|
-
}
|
|
5425
|
-
|
|
5426
|
-
get selected() {
|
|
5427
|
-
return figLabBooleanAttribute(this, "selected");
|
|
5428
|
-
}
|
|
5429
|
-
|
|
5430
|
-
set selected(val) {
|
|
5431
|
-
if (val) this.setAttribute("selected", "");
|
|
5432
|
-
else this.removeAttribute("selected");
|
|
5433
|
-
}
|
|
5434
|
-
|
|
5435
|
-
connectedCallback() {
|
|
5436
|
-
if (!this.hasAttribute("role")) this.setAttribute("role", "option");
|
|
5437
|
-
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5438
|
-
this.#syncDisabled();
|
|
5439
|
-
}
|
|
5440
|
-
|
|
5441
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
5442
|
-
if (oldValue === newValue) return;
|
|
5443
|
-
if (name === "disabled") this.#syncDisabled();
|
|
5444
|
-
}
|
|
5445
|
-
|
|
5446
|
-
#syncDisabled() {
|
|
5447
|
-
const disabled = this.disabled;
|
|
5448
|
-
if (disabled) {
|
|
5449
|
-
this.setAttribute("aria-disabled", "true");
|
|
5450
|
-
this.setAttribute("tabindex", "-1");
|
|
5451
|
-
} else {
|
|
5452
|
-
this.removeAttribute("aria-disabled");
|
|
5453
|
-
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5906
|
+
if (selectedOption) {
|
|
5907
|
+
const optionValue = String(selectedOption.value ?? "");
|
|
5908
|
+
if ((select.getAttribute("value") ?? "") !== optionValue) {
|
|
5909
|
+
figLabSyncSelectedOption(select, selectedOption);
|
|
5910
|
+
}
|
|
5911
|
+
} else if (selectedWasRemoved) {
|
|
5912
|
+
figLabSyncSelectedOption(select, null);
|
|
5454
5913
|
}
|
|
5455
|
-
}
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
if (!host || !scrollEl) return false;
|
|
5461
|
-
const scrollable = scrollEl.scrollHeight - scrollEl.clientHeight > threshold;
|
|
5462
|
-
const atStart = !scrollable || scrollEl.scrollTop <= threshold;
|
|
5463
|
-
const atEnd =
|
|
5464
|
-
!scrollable ||
|
|
5465
|
-
scrollEl.scrollTop + scrollEl.clientHeight >=
|
|
5466
|
-
scrollEl.scrollHeight - threshold;
|
|
5467
|
-
host.classList.toggle("overflow-start", !atStart);
|
|
5468
|
-
host.classList.toggle("overflow-end", !atEnd);
|
|
5469
|
-
return scrollable;
|
|
5470
|
-
}
|
|
5471
|
-
|
|
5472
|
-
function figLabScrollOverflowPage(scrollEl, direction = 1) {
|
|
5473
|
-
if (!scrollEl) return;
|
|
5474
|
-
scrollEl.scrollBy({
|
|
5475
|
-
top: scrollEl.clientHeight * 0.8 * direction,
|
|
5476
|
-
behavior: "smooth",
|
|
5914
|
+
});
|
|
5915
|
+
observer.observe(select, {
|
|
5916
|
+
attributes: true,
|
|
5917
|
+
subtree: true,
|
|
5918
|
+
attributeFilter: ["selected"],
|
|
5477
5919
|
});
|
|
5478
5920
|
}
|
|
5479
5921
|
|
|
5480
|
-
function
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
button.dataset.figOverflow = direction;
|
|
5486
|
-
button.setAttribute("data-fig-select-nav", direction);
|
|
5487
|
-
button.setAttribute("tabindex", "-1");
|
|
5488
|
-
button.setAttribute(
|
|
5489
|
-
"aria-label",
|
|
5490
|
-
direction === "start" ? "Scroll up" : "Scroll down",
|
|
5491
|
-
);
|
|
5492
|
-
const icon = document.createElement("fig-icon");
|
|
5493
|
-
icon.setAttribute("name", "chevron");
|
|
5494
|
-
icon.setAttribute("size", "small");
|
|
5495
|
-
icon.className = "fig-overflow-chevron";
|
|
5496
|
-
button.appendChild(icon);
|
|
5497
|
-
button.addEventListener("click", (event) => {
|
|
5498
|
-
event.preventDefault();
|
|
5499
|
-
event.stopPropagation();
|
|
5500
|
-
onClick?.(event);
|
|
5501
|
-
});
|
|
5502
|
-
return button;
|
|
5503
|
-
};
|
|
5504
|
-
return {
|
|
5505
|
-
start: makeButton("start", onStart),
|
|
5506
|
-
end: makeButton("end", onEnd),
|
|
5507
|
-
};
|
|
5508
|
-
}
|
|
5509
|
-
|
|
5510
|
-
/** Light-DOM panel wrapper projected into fig-select's popup; owns overflow buttons. */
|
|
5511
|
-
class FigSelectOptions extends HTMLElement {
|
|
5512
|
-
#navStart = null;
|
|
5513
|
-
#navEnd = null;
|
|
5514
|
-
#resizeObserver = null;
|
|
5515
|
-
#boundSyncOverflow = this.syncOverflow.bind(this);
|
|
5516
|
-
|
|
5517
|
-
connectedCallback() {
|
|
5518
|
-
if (!this.hasAttribute("slot")) this.setAttribute("slot", "panel");
|
|
5519
|
-
this.#unwrapLegacyChooser();
|
|
5520
|
-
this.#ensureNavButtons();
|
|
5521
|
-
this.addEventListener("scroll", this.#boundSyncOverflow, { passive: true });
|
|
5522
|
-
this.#resizeObserver?.disconnect();
|
|
5523
|
-
this.#resizeObserver = new ResizeObserver(() => this.syncOverflow());
|
|
5524
|
-
this.#resizeObserver.observe(this);
|
|
5525
|
-
requestAnimationFrame(() => this.syncOverflow());
|
|
5526
|
-
}
|
|
5527
|
-
|
|
5528
|
-
disconnectedCallback() {
|
|
5529
|
-
this.removeEventListener("scroll", this.#boundSyncOverflow);
|
|
5530
|
-
this.#resizeObserver?.disconnect();
|
|
5531
|
-
this.#resizeObserver = null;
|
|
5532
|
-
this.#removeNavButtons();
|
|
5533
|
-
}
|
|
5534
|
-
|
|
5535
|
-
syncOverflow() {
|
|
5536
|
-
return figLabSyncOverflowState(this, this);
|
|
5537
|
-
}
|
|
5538
|
-
|
|
5539
|
-
scrollToOption(option, behavior = "auto") {
|
|
5540
|
-
if (!option || !this.contains(option)) return;
|
|
5541
|
-
requestAnimationFrame(() => {
|
|
5542
|
-
if (!option.isConnected) return;
|
|
5543
|
-
if (this.scrollHeight <= this.clientHeight + 1) {
|
|
5544
|
-
this.syncOverflow();
|
|
5545
|
-
return;
|
|
5546
|
-
}
|
|
5547
|
-
const optionRect = option.getBoundingClientRect();
|
|
5548
|
-
const hostRect = this.getBoundingClientRect();
|
|
5549
|
-
const optionTop = optionRect.top - hostRect.top + this.scrollTop;
|
|
5550
|
-
const maxScroll = this.scrollHeight - this.clientHeight;
|
|
5551
|
-
const top = Math.max(
|
|
5552
|
-
0,
|
|
5553
|
-
Math.min(
|
|
5554
|
-
optionTop + optionRect.height / 2 - this.clientHeight / 2,
|
|
5555
|
-
maxScroll,
|
|
5556
|
-
),
|
|
5557
|
-
);
|
|
5558
|
-
this.scrollTo({ top, behavior });
|
|
5559
|
-
this.syncOverflow();
|
|
5560
|
-
});
|
|
5561
|
-
}
|
|
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);
|
|
5562
5927
|
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
#ensureNavButtons() {
|
|
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");
|
|
5573
5936
|
if (
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
this.contains(this.#navEnd)
|
|
5937
|
+
!option ||
|
|
5938
|
+
!select.contains(option) ||
|
|
5939
|
+
figLabBooleanAttribute(option, "disabled")
|
|
5578
5940
|
) {
|
|
5579
5941
|
return;
|
|
5580
5942
|
}
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
onEnd: () => figLabScrollOverflowPage(this, 1),
|
|
5585
|
-
});
|
|
5586
|
-
this.#navStart = buttons.start;
|
|
5587
|
-
this.#navEnd = buttons.end;
|
|
5588
|
-
this.prepend(this.#navStart);
|
|
5589
|
-
this.append(this.#navEnd);
|
|
5590
|
-
}
|
|
5591
|
-
|
|
5592
|
-
#removeNavButtons() {
|
|
5593
|
-
this.#navStart?.remove();
|
|
5594
|
-
this.#navEnd?.remove();
|
|
5595
|
-
this.#navStart = null;
|
|
5596
|
-
this.#navEnd = null;
|
|
5597
|
-
this.classList.remove("overflow-start", "overflow-end");
|
|
5598
|
-
}
|
|
5943
|
+
figLabSyncSelectedOption(select, option);
|
|
5944
|
+
select.open = false;
|
|
5945
|
+
});
|
|
5599
5946
|
}
|
|
5600
|
-
customElements.define("fig-select-options", FigSelectOptions);
|
|
5601
|
-
|
|
5602
|
-
class FigSelect extends HTMLElement {
|
|
5603
|
-
#button = null;
|
|
5604
|
-
#popup = null;
|
|
5605
|
-
#labelEl = null;
|
|
5606
|
-
#panelSlot = null;
|
|
5607
|
-
#observer = null;
|
|
5608
|
-
#initialized = false;
|
|
5609
|
-
#focusedIndex = -1;
|
|
5610
|
-
#syncingValue = false;
|
|
5611
|
-
#popupPositionPatched = false;
|
|
5612
|
-
#originalPositionPopup = null;
|
|
5613
|
-
/**
|
|
5614
|
-
* After open align, ignore content/scroll-driven positionPopup passes so
|
|
5615
|
-
* overflow paging isn't yanked back. Still realign when the trigger moves
|
|
5616
|
-
* or the viewport size changes (window resize, layout shift, page scroll).
|
|
5617
|
-
*/
|
|
5618
|
-
#freezeMenuPosition = false;
|
|
5619
|
-
#frozenLabelRect = null;
|
|
5620
|
-
#frozenViewport = null;
|
|
5621
|
-
#syncingOptions = false;
|
|
5622
|
-
#boundTriggerClick = this.#handleTriggerClick.bind(this);
|
|
5623
|
-
#boundOptionClick = this.#handleOptionClick.bind(this);
|
|
5624
|
-
#boundKeydown = this.#handleKeydown.bind(this);
|
|
5625
|
-
#boundPopupClose = this.#handlePopupClose.bind(this);
|
|
5626
|
-
#boundSlotChange = this.#handleSlotChange.bind(this);
|
|
5627
|
-
|
|
5628
|
-
static get observedAttributes() {
|
|
5629
|
-
return [
|
|
5630
|
-
"value",
|
|
5631
|
-
"disabled",
|
|
5632
|
-
"label",
|
|
5633
|
-
"options",
|
|
5634
|
-
"position",
|
|
5635
|
-
"offset",
|
|
5636
|
-
"closedby",
|
|
5637
|
-
"open",
|
|
5638
|
-
];
|
|
5639
|
-
}
|
|
5640
|
-
|
|
5641
|
-
get value() {
|
|
5642
|
-
return this.getAttribute("value") ?? "";
|
|
5643
|
-
}
|
|
5644
|
-
|
|
5645
|
-
set value(val) {
|
|
5646
|
-
if (val === null || val === undefined) this.removeAttribute("value");
|
|
5647
|
-
else this.setAttribute("value", String(val));
|
|
5648
|
-
}
|
|
5649
|
-
|
|
5650
|
-
get open() {
|
|
5651
|
-
return figLabBooleanAttribute(this, "open");
|
|
5652
|
-
}
|
|
5653
|
-
|
|
5654
|
-
set open(val) {
|
|
5655
|
-
if (val) this.setAttribute("open", "");
|
|
5656
|
-
else this.removeAttribute("open");
|
|
5657
|
-
}
|
|
5658
|
-
|
|
5659
|
-
connectedCallback() {
|
|
5660
|
-
if (!this.#initialized) this.#initialize();
|
|
5661
|
-
this.#ensurePanelSlotAttrs();
|
|
5662
|
-
this.#syncOptionsFromAttribute();
|
|
5663
|
-
this.#syncDisabled();
|
|
5664
|
-
this.#syncPopupAttrs();
|
|
5665
|
-
this.#syncValue();
|
|
5666
|
-
this.#setupObserver();
|
|
5667
|
-
if (this.open) this.#openList();
|
|
5668
|
-
}
|
|
5669
|
-
|
|
5670
|
-
disconnectedCallback() {
|
|
5671
|
-
this.#teardownListeners();
|
|
5672
|
-
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
5673
|
-
this.#observer?.disconnect();
|
|
5674
|
-
this.#observer = null;
|
|
5675
|
-
}
|
|
5676
|
-
|
|
5677
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
5678
|
-
if (oldValue === newValue || !this.#initialized) return;
|
|
5679
|
-
if (name === "options") {
|
|
5680
|
-
this.#syncOptionsFromAttribute();
|
|
5681
|
-
this.#syncValue();
|
|
5682
|
-
return;
|
|
5683
|
-
}
|
|
5684
|
-
if (name === "value" || name === "label") {
|
|
5685
|
-
this.#syncValue();
|
|
5686
|
-
return;
|
|
5687
|
-
}
|
|
5688
|
-
if (name === "disabled") {
|
|
5689
|
-
this.#syncDisabled();
|
|
5690
|
-
return;
|
|
5691
|
-
}
|
|
5692
|
-
if (name === "open") {
|
|
5693
|
-
if (newValue === null || newValue === "false") this.#closeList();
|
|
5694
|
-
else this.#openList();
|
|
5695
|
-
return;
|
|
5696
|
-
}
|
|
5697
|
-
if (name === "position" || name === "offset" || name === "closedby") {
|
|
5698
|
-
this.#syncPopupAttrs();
|
|
5699
|
-
}
|
|
5700
|
-
}
|
|
5701
|
-
|
|
5702
|
-
focus(options) {
|
|
5703
|
-
this.#button?.focus(options);
|
|
5704
|
-
}
|
|
5705
|
-
|
|
5706
|
-
blur() {
|
|
5707
|
-
this.#button?.blur();
|
|
5708
|
-
}
|
|
5709
|
-
|
|
5710
|
-
#isMenuChild(node) {
|
|
5711
|
-
return (
|
|
5712
|
-
node?.nodeType === 1 &&
|
|
5713
|
-
(node.tagName === "FIG-SELECT-OPTION" ||
|
|
5714
|
-
node.tagName === "FIG-MENU-SEPARATOR" ||
|
|
5715
|
-
node.tagName === "FIG-SELECT-OPTIONS")
|
|
5716
|
-
);
|
|
5717
|
-
}
|
|
5718
|
-
|
|
5719
|
-
#ensurePanelSlotAttrs() {
|
|
5720
|
-
for (const panel of this.querySelectorAll(":scope > fig-select-options")) {
|
|
5721
|
-
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5722
|
-
}
|
|
5723
|
-
}
|
|
5724
|
-
|
|
5725
|
-
#getPanel() {
|
|
5726
|
-
const assigned = this.#panelSlot?.assignedElements({ flatten: true }) ?? [];
|
|
5727
|
-
const fromSlot = assigned.find(
|
|
5728
|
-
(el) => el.tagName === "FIG-SELECT-OPTIONS",
|
|
5729
|
-
);
|
|
5730
|
-
if (fromSlot) return fromSlot;
|
|
5731
|
-
return this.querySelector(":scope > fig-select-options");
|
|
5732
|
-
}
|
|
5733
|
-
|
|
5734
|
-
#hasAuthoredOptions() {
|
|
5735
|
-
return Boolean(
|
|
5736
|
-
this.querySelector(
|
|
5737
|
-
":scope > fig-select-option:not([data-fig-generated]), :scope > fig-select-options > fig-select-option:not([data-fig-generated])",
|
|
5738
|
-
),
|
|
5739
|
-
);
|
|
5740
|
-
}
|
|
5741
|
-
|
|
5742
|
-
#ensureOptionsPanel() {
|
|
5743
|
-
let panel = this.#getPanel();
|
|
5744
|
-
if (panel) {
|
|
5745
|
-
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5746
|
-
return panel;
|
|
5747
|
-
}
|
|
5748
|
-
panel = document.createElement("fig-select-options");
|
|
5749
|
-
panel.setAttribute("slot", "panel");
|
|
5750
|
-
panel.setAttribute("data-fig-generated", "");
|
|
5751
|
-
this.appendChild(panel);
|
|
5752
|
-
return panel;
|
|
5753
|
-
}
|
|
5754
|
-
|
|
5755
|
-
/**
|
|
5756
|
-
* When no authored fig-select-option exists, build panel/options from the
|
|
5757
|
-
* options attribute (comma / newline / JSON — same as fig-options).
|
|
5758
|
-
*/
|
|
5759
|
-
#syncOptionsFromAttribute() {
|
|
5760
|
-
if (this.#hasAuthoredOptions()) return;
|
|
5761
5947
|
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
: this.#getPanel();
|
|
5766
|
-
if (!panel) return;
|
|
5767
|
-
|
|
5768
|
-
this.#syncingOptions = true;
|
|
5769
|
-
try {
|
|
5770
|
-
for (const opt of panel.querySelectorAll(
|
|
5771
|
-
":scope > fig-select-option[data-fig-generated]",
|
|
5772
|
-
)) {
|
|
5773
|
-
opt.remove();
|
|
5774
|
-
}
|
|
5775
|
-
|
|
5776
|
-
if (!hasOptionsAttr) return;
|
|
5777
|
-
|
|
5778
|
-
const parsed = figLabParseOptionsAttribute(this.getAttribute("options"));
|
|
5779
|
-
const endBtn = panel.querySelector(":scope > .fig-overflow-end");
|
|
5780
|
-
for (const entry of parsed) {
|
|
5781
|
-
const el = document.createElement("fig-select-option");
|
|
5782
|
-
el.setAttribute("data-fig-generated", "");
|
|
5783
|
-
el.setAttribute("value", figLabOptionEntryValue(entry));
|
|
5784
|
-
el.textContent = figLabOptionEntryLabel(entry);
|
|
5785
|
-
if (endBtn) panel.insertBefore(el, endBtn);
|
|
5786
|
-
else panel.appendChild(el);
|
|
5787
|
-
}
|
|
5788
|
-
} finally {
|
|
5789
|
-
this.#syncingOptions = false;
|
|
5790
|
-
}
|
|
5791
|
-
}
|
|
5792
|
-
|
|
5793
|
-
#initialize() {
|
|
5794
|
-
this.#initialized = true;
|
|
5795
|
-
const shadow = this.attachShadow({ mode: "open" });
|
|
5796
|
-
shadow.innerHTML = `
|
|
5797
|
-
<style>
|
|
5798
|
-
:host {
|
|
5799
|
-
display: inline-flex;
|
|
5800
|
-
position: relative;
|
|
5801
|
-
align-items: center;
|
|
5802
|
-
min-width: 0;
|
|
5803
|
-
}
|
|
5804
|
-
:host([full]:not([full="false"])) {
|
|
5805
|
-
display: flex;
|
|
5806
|
-
width: 100%;
|
|
5807
|
-
}
|
|
5808
|
-
.fig-select-trigger {
|
|
5809
|
-
display: flex;
|
|
5810
|
-
align-items: center;
|
|
5811
|
-
justify-content: flex-start;
|
|
5812
|
-
flex: 1;
|
|
5813
|
-
min-width: 0;
|
|
5814
|
-
width: var(--fig-select-trigger-width, 100%);
|
|
5815
|
-
height: 100%;
|
|
5816
|
-
margin: 0;
|
|
5817
|
-
padding: 0 var(--spacer-4, 1rem) 0 var(--spacer-2, 0.5rem);
|
|
5818
|
-
border: 0;
|
|
5819
|
-
border-radius: inherit;
|
|
5820
|
-
background: transparent;
|
|
5821
|
-
box-shadow: none;
|
|
5822
|
-
color: inherit;
|
|
5823
|
-
font: inherit;
|
|
5824
|
-
font-weight: inherit;
|
|
5825
|
-
text-align: left;
|
|
5826
|
-
white-space: nowrap;
|
|
5827
|
-
overflow: hidden;
|
|
5828
|
-
text-overflow: ellipsis;
|
|
5829
|
-
cursor: default;
|
|
5830
|
-
}
|
|
5831
|
-
.fig-select-trigger:hover,
|
|
5832
|
-
.fig-select-trigger:active,
|
|
5833
|
-
.fig-select-trigger:active:hover {
|
|
5834
|
-
background: transparent;
|
|
5835
|
-
box-shadow: none;
|
|
5836
|
-
color: inherit;
|
|
5837
|
-
}
|
|
5838
|
-
.fig-select-trigger:focus-visible,
|
|
5839
|
-
.fig-select-trigger[data-focus-visible] {
|
|
5840
|
-
outline: var(--figma-focus-outline);
|
|
5841
|
-
outline-offset: var(--figma-focus-outline-offset);
|
|
5842
|
-
}
|
|
5843
|
-
:host([disabled]:not([disabled="false"])) .fig-select-trigger,
|
|
5844
|
-
:host([disabled]:not([disabled="false"])) .fig-select-label {
|
|
5845
|
-
color: var(--figma-color-text-tertiary);
|
|
5846
|
-
}
|
|
5847
|
-
.fig-select-label {
|
|
5848
|
-
display: block;
|
|
5849
|
-
width: 100%;
|
|
5850
|
-
min-width: 0;
|
|
5851
|
-
overflow: hidden;
|
|
5852
|
-
text-overflow: ellipsis;
|
|
5853
|
-
white-space: nowrap;
|
|
5854
|
-
text-align: left;
|
|
5855
|
-
}
|
|
5856
|
-
/* Listbox chrome from document fig-select::part(listbox).
|
|
5857
|
-
Overflow UI lives on slotted fig-select-options.
|
|
5858
|
-
Never set display except when open — closed <dialog> must stay display:none. */
|
|
5859
|
-
dialog[is="fig-popup"] {
|
|
5860
|
-
flex-direction: column;
|
|
5861
|
-
overflow: hidden;
|
|
5862
|
-
}
|
|
5863
|
-
dialog[is="fig-popup"][open] {
|
|
5864
|
-
display: flex;
|
|
5865
|
-
}
|
|
5866
|
-
::slotted(fig-select-options) {
|
|
5867
|
-
flex: 1 1 auto;
|
|
5868
|
-
min-height: 0;
|
|
5869
|
-
max-height: inherit;
|
|
5870
|
-
}
|
|
5871
|
-
</style>
|
|
5872
|
-
`;
|
|
5873
|
-
|
|
5874
|
-
const button = document.createElement("fig-button");
|
|
5875
|
-
button.className = "fig-select-trigger";
|
|
5876
|
-
button.setAttribute("part", "trigger");
|
|
5877
|
-
button.setAttribute("variant", "ghost");
|
|
5878
|
-
button.setAttribute("aria-haspopup", "listbox");
|
|
5879
|
-
button.setAttribute("aria-expanded", "false");
|
|
5880
|
-
|
|
5881
|
-
const labelEl = document.createElement("span");
|
|
5882
|
-
labelEl.className = "fig-select-label";
|
|
5883
|
-
labelEl.setAttribute("part", "label");
|
|
5884
|
-
button.appendChild(labelEl);
|
|
5885
|
-
|
|
5886
|
-
const popup = document.createElement("dialog", { is: "fig-popup" });
|
|
5887
|
-
popup.setAttribute("is", "fig-popup");
|
|
5888
|
-
popup.setAttribute("part", "listbox");
|
|
5889
|
-
popup.setAttribute("theme", "menu");
|
|
5890
|
-
popup.setAttribute("role", "listbox");
|
|
5891
|
-
// Top-layer via popover so the menu escapes ancestor contain/overflow
|
|
5892
|
-
// (e.g. fig-fill-picker-dialog). Stays in shadow so option slots still work —
|
|
5893
|
-
// unlike tooltips, we cannot portal this popup to the overlay root.
|
|
5894
|
-
if ("popover" in HTMLElement.prototype) {
|
|
5895
|
-
popup.setAttribute("popover", "manual");
|
|
5896
|
-
}
|
|
5897
|
-
popup.id = figLabUniqueId("fig-select-list");
|
|
5898
|
-
button.setAttribute("aria-controls", popup.id);
|
|
5899
|
-
|
|
5900
|
-
const panelSlot = document.createElement("slot");
|
|
5901
|
-
panelSlot.setAttribute("name", "panel");
|
|
5902
|
-
popup.appendChild(panelSlot);
|
|
5903
|
-
|
|
5904
|
-
shadow.append(button, popup);
|
|
5905
|
-
|
|
5906
|
-
this.#button = button;
|
|
5907
|
-
this.#labelEl = labelEl;
|
|
5908
|
-
this.#popup = popup;
|
|
5909
|
-
this.#panelSlot = panelSlot;
|
|
5910
|
-
popup.anchor = button;
|
|
5911
|
-
|
|
5912
|
-
this.#ensurePanelSlotAttrs();
|
|
5913
|
-
this.#setupListeners();
|
|
5914
|
-
this.#installPopupPositioning();
|
|
5915
|
-
|
|
5916
|
-
if (!this.hasAttribute("value")) {
|
|
5917
|
-
const selected = this.#getOptions().find((opt) =>
|
|
5918
|
-
figLabBooleanAttribute(opt, "selected"),
|
|
5919
|
-
);
|
|
5920
|
-
if (selected) this.setAttribute("value", selected.value);
|
|
5921
|
-
}
|
|
5922
|
-
}
|
|
5923
|
-
|
|
5924
|
-
#installPopupPositioning() {
|
|
5925
|
-
if (!this.#popup || this.#popupPositionPatched) return;
|
|
5926
|
-
if (typeof this.#popup.positionPopup !== "function") return;
|
|
5927
|
-
this.#originalPositionPopup = this.#popup.positionPopup.bind(this.#popup);
|
|
5928
|
-
this.#popup.positionPopup = () => {
|
|
5929
|
-
if (!this.open) {
|
|
5930
|
-
this.#originalPositionPopup?.();
|
|
5931
|
-
return;
|
|
5932
|
-
}
|
|
5933
|
-
this.#positionPopupOverSelected();
|
|
5934
|
-
};
|
|
5935
|
-
this.#popupPositionPatched = true;
|
|
5936
|
-
}
|
|
5937
|
-
|
|
5938
|
-
#getOptionTextRect(option) {
|
|
5939
|
-
if (!option) return null;
|
|
5940
|
-
const range = document.createRange();
|
|
5941
|
-
range.selectNodeContents(option);
|
|
5942
|
-
const rects = [...range.getClientRects()].filter(
|
|
5943
|
-
(rect) => rect.width > 0 && rect.height > 0,
|
|
5944
|
-
);
|
|
5945
|
-
if (rects.length) return rects[0];
|
|
5946
|
-
return option.getBoundingClientRect();
|
|
5947
|
-
}
|
|
5948
|
-
|
|
5949
|
-
#getViewportMargins() {
|
|
5950
|
-
if (typeof this.#popup?.parseViewportMargins === "function") {
|
|
5951
|
-
return this.#popup.parseViewportMargins();
|
|
5952
|
-
}
|
|
5953
|
-
return { top: 8, right: 8, bottom: 8, left: 8 };
|
|
5954
|
-
}
|
|
5955
|
-
|
|
5956
|
-
#readLabelRectSnapshot() {
|
|
5957
|
-
const rect = this.#labelEl?.getBoundingClientRect();
|
|
5958
|
-
if (!rect) return null;
|
|
5959
|
-
return {
|
|
5960
|
-
x: rect.x,
|
|
5961
|
-
y: rect.y,
|
|
5962
|
-
width: rect.width,
|
|
5963
|
-
height: rect.height,
|
|
5964
|
-
};
|
|
5965
|
-
}
|
|
5966
|
-
|
|
5967
|
-
#readViewportSnapshot() {
|
|
5968
|
-
const vv = window.visualViewport;
|
|
5969
|
-
return {
|
|
5970
|
-
width: vv?.width ?? window.innerWidth,
|
|
5971
|
-
height: vv?.height ?? window.innerHeight,
|
|
5972
|
-
offsetLeft: vv?.offsetLeft ?? 0,
|
|
5973
|
-
offsetTop: vv?.offsetTop ?? 0,
|
|
5974
|
-
};
|
|
5975
|
-
}
|
|
5976
|
-
|
|
5977
|
-
#rectSnapshotChanged(prev, next, epsilon = 0.25) {
|
|
5978
|
-
if (!prev && !next) return false;
|
|
5979
|
-
if (!prev || !next) return true;
|
|
5980
|
-
return (
|
|
5981
|
-
Math.abs(prev.x - next.x) > epsilon ||
|
|
5982
|
-
Math.abs(prev.y - next.y) > epsilon ||
|
|
5983
|
-
Math.abs(prev.width - next.width) > epsilon ||
|
|
5984
|
-
Math.abs(prev.height - next.height) > epsilon
|
|
5985
|
-
);
|
|
5986
|
-
}
|
|
5987
|
-
|
|
5988
|
-
#viewportSnapshotChanged(prev, next, epsilon = 0.25) {
|
|
5989
|
-
if (!prev && !next) return false;
|
|
5990
|
-
if (!prev || !next) return true;
|
|
5991
|
-
return (
|
|
5992
|
-
Math.abs(prev.width - next.width) > epsilon ||
|
|
5993
|
-
Math.abs(prev.height - next.height) > epsilon ||
|
|
5994
|
-
Math.abs(prev.offsetLeft - next.offsetLeft) > epsilon ||
|
|
5995
|
-
Math.abs(prev.offsetTop - next.offsetTop) > epsilon
|
|
5996
|
-
);
|
|
5997
|
-
}
|
|
5998
|
-
|
|
5999
|
-
#shouldSkipFrozenPositionPass() {
|
|
6000
|
-
if (!this.#freezeMenuPosition) return false;
|
|
6001
|
-
const labelMoved = this.#rectSnapshotChanged(
|
|
6002
|
-
this.#frozenLabelRect,
|
|
6003
|
-
this.#readLabelRectSnapshot(),
|
|
6004
|
-
);
|
|
6005
|
-
const viewportChanged = this.#viewportSnapshotChanged(
|
|
6006
|
-
this.#frozenViewport,
|
|
6007
|
-
this.#readViewportSnapshot(),
|
|
6008
|
-
);
|
|
6009
|
-
// Skip only when neither the trigger nor the viewport moved — typical of
|
|
6010
|
-
// overflow scroll / content sync fighting the open-time alignment.
|
|
6011
|
-
return !labelMoved && !viewportChanged;
|
|
5948
|
+
function figLabMarkDisconnectedSelectTree(root) {
|
|
5949
|
+
if (root instanceof Element && root.matches("fig-select")) {
|
|
5950
|
+
figLabDisconnectedSelects.add(root);
|
|
6012
5951
|
}
|
|
5952
|
+
root
|
|
5953
|
+
.querySelectorAll?.("fig-select")
|
|
5954
|
+
.forEach((select) => figLabDisconnectedSelects.add(select));
|
|
5955
|
+
}
|
|
6013
5956
|
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
5957
|
+
function figLabEnhanceSelectTree(root) {
|
|
5958
|
+
if (root instanceof Element && root.matches("fig-select")) {
|
|
5959
|
+
figLabEnhanceSelect(root);
|
|
6017
5960
|
}
|
|
5961
|
+
root
|
|
5962
|
+
.querySelectorAll?.("fig-select")
|
|
5963
|
+
.forEach((select) => figLabEnhanceSelect(select));
|
|
5964
|
+
}
|
|
6018
5965
|
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
const popup = this.#popup;
|
|
6025
|
-
const label = this.#labelEl;
|
|
6026
|
-
if (!popup || !label) {
|
|
6027
|
-
this.#originalPositionPopup?.();
|
|
6028
|
-
return;
|
|
6029
|
-
}
|
|
6030
|
-
|
|
6031
|
-
const options = this.#getOptions();
|
|
6032
|
-
const selected =
|
|
6033
|
-
options.find((opt) => this.#optionValue(opt) === this.value) ||
|
|
6034
|
-
options[0];
|
|
6035
|
-
if (!selected) {
|
|
6036
|
-
this.#originalPositionPopup?.();
|
|
6037
|
-
return;
|
|
6038
|
-
}
|
|
6039
|
-
|
|
6040
|
-
// Lay out with the default positioning first so option metrics are valid.
|
|
6041
|
-
this.#originalPositionPopup?.();
|
|
6042
|
-
|
|
6043
|
-
const popupRect = popup.getBoundingClientRect();
|
|
6044
|
-
const labelRect = label.getBoundingClientRect();
|
|
6045
|
-
const optionTextRect = this.#getOptionTextRect(selected);
|
|
6046
|
-
if (
|
|
6047
|
-
!popupRect.width ||
|
|
6048
|
-
!popupRect.height ||
|
|
6049
|
-
!labelRect.width ||
|
|
6050
|
-
!optionTextRect
|
|
6051
|
-
) {
|
|
6052
|
-
return;
|
|
6053
|
-
}
|
|
6054
|
-
|
|
6055
|
-
const selectedOffsetX = optionTextRect.left - popupRect.left;
|
|
6056
|
-
const selectedOffsetY = optionTextRect.top - popupRect.top;
|
|
6057
|
-
const full = figLabBooleanAttribute(this, "full");
|
|
6058
|
-
// [full]: pin menu to host width/edges. Otherwise overlay selected
|
|
6059
|
-
// option text on the trigger label (blend-mode style).
|
|
6060
|
-
let left = full
|
|
6061
|
-
? this.getBoundingClientRect().left
|
|
6062
|
-
: labelRect.left - selectedOffsetX;
|
|
6063
|
-
let top = labelRect.top - selectedOffsetY;
|
|
6064
|
-
|
|
6065
|
-
// Keep the whole menu in-view when aligning over the selected option
|
|
6066
|
-
// would otherwise push it past a viewport edge (corners / far sides).
|
|
6067
|
-
const margins = this.#getViewportMargins();
|
|
6068
|
-
if (typeof popup.clampToViewport === "function") {
|
|
6069
|
-
({ left, top } = popup.clampToViewport({ left, top }, popupRect, margins));
|
|
6070
|
-
} else {
|
|
6071
|
-
const minLeft = margins.left;
|
|
6072
|
-
const minTop = margins.top;
|
|
6073
|
-
const maxLeft = window.innerWidth - popupRect.width - margins.right;
|
|
6074
|
-
const maxTop = window.innerHeight - popupRect.height - margins.bottom;
|
|
6075
|
-
left = Math.min(Math.max(left, minLeft), Math.max(minLeft, maxLeft));
|
|
6076
|
-
top = Math.min(Math.max(top, minTop), Math.max(minTop, maxTop));
|
|
6077
|
-
}
|
|
6078
|
-
|
|
6079
|
-
// !important: fig-select::part(listbox) and dialog UA rules can otherwise
|
|
6080
|
-
// keep the menu at its static/anchor position past the viewport edge.
|
|
6081
|
-
popup.style.setProperty("right", "auto", "important");
|
|
6082
|
-
popup.style.setProperty("bottom", "auto", "important");
|
|
6083
|
-
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
6084
|
-
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
6085
|
-
|
|
6086
|
-
// Nudge the panel scroller so the selected label stays over the trigger.
|
|
6087
|
-
const panel = this.#getPanel();
|
|
6088
|
-
const alignedTextRect = this.#getOptionTextRect(selected);
|
|
6089
|
-
if (
|
|
6090
|
-
alignedTextRect &&
|
|
6091
|
-
panel &&
|
|
6092
|
-
panel.scrollHeight > panel.clientHeight + 1
|
|
6093
|
-
) {
|
|
6094
|
-
const deltaY = alignedTextRect.top - labelRect.top;
|
|
6095
|
-
if (Math.abs(deltaY) > 0.5) {
|
|
6096
|
-
panel.scrollTop += deltaY;
|
|
6097
|
-
}
|
|
6098
|
-
panel.syncOverflow?.();
|
|
6099
|
-
}
|
|
6100
|
-
|
|
6101
|
-
if (this.#freezeMenuPosition || this.open) {
|
|
6102
|
-
this.#rememberFrozenGeometry();
|
|
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);
|
|
6103
5971
|
}
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
#setupListeners() {
|
|
6107
|
-
this.#button?.addEventListener("click", this.#boundTriggerClick);
|
|
6108
|
-
this.#button?.addEventListener("keydown", this.#boundKeydown);
|
|
6109
|
-
// Host click: slotted options stay in light DOM (not dialog.contains).
|
|
6110
|
-
this.addEventListener("click", this.#boundOptionClick);
|
|
6111
|
-
this.#popup?.addEventListener("keydown", this.#boundKeydown);
|
|
6112
|
-
this.#popup?.addEventListener("close", this.#boundPopupClose);
|
|
6113
|
-
this.#panelSlot?.addEventListener("slotchange", this.#boundSlotChange);
|
|
6114
|
-
}
|
|
6115
|
-
|
|
6116
|
-
#teardownListeners() {
|
|
6117
|
-
this.#button?.removeEventListener("click", this.#boundTriggerClick);
|
|
6118
|
-
this.#button?.removeEventListener("keydown", this.#boundKeydown);
|
|
6119
|
-
this.removeEventListener("click", this.#boundOptionClick);
|
|
6120
|
-
this.#popup?.removeEventListener("keydown", this.#boundKeydown);
|
|
6121
|
-
this.#popup?.removeEventListener("close", this.#boundPopupClose);
|
|
6122
|
-
this.#panelSlot?.removeEventListener("slotchange", this.#boundSlotChange);
|
|
6123
|
-
}
|
|
6124
|
-
|
|
6125
|
-
#handleSlotChange() {
|
|
6126
|
-
this.#ensurePanelSlotAttrs();
|
|
6127
|
-
this.#syncValue();
|
|
6128
|
-
}
|
|
6129
|
-
|
|
6130
|
-
#setupObserver() {
|
|
6131
|
-
if (this.#observer) return;
|
|
6132
|
-
this.#observer = new MutationObserver((mutations) => {
|
|
6133
|
-
if (this.#syncingValue || this.#syncingOptions) return;
|
|
6134
|
-
let needsSync = false;
|
|
6135
|
-
for (const mutation of mutations) {
|
|
6136
|
-
if (mutation.type === "childList") {
|
|
6137
|
-
if (
|
|
6138
|
-
[...mutation.addedNodes].some((node) => this.#isMenuChild(node)) ||
|
|
6139
|
-
[...mutation.removedNodes].some((node) => this.#isMenuChild(node))
|
|
6140
|
-
) {
|
|
6141
|
-
needsSync = true;
|
|
6142
|
-
}
|
|
6143
|
-
}
|
|
6144
|
-
if (
|
|
6145
|
-
mutation.type === "attributes" &&
|
|
6146
|
-
mutation.target?.tagName === "FIG-SELECT-OPTION" &&
|
|
6147
|
-
(mutation.attributeName === "value" ||
|
|
6148
|
-
mutation.attributeName === "disabled" ||
|
|
6149
|
-
mutation.attributeName === "label")
|
|
6150
|
-
) {
|
|
6151
|
-
needsSync = true;
|
|
6152
|
-
}
|
|
6153
|
-
if (
|
|
6154
|
-
mutation.type === "characterData" &&
|
|
6155
|
-
mutation.target?.parentElement?.tagName === "FIG-SELECT-OPTION"
|
|
6156
|
-
) {
|
|
6157
|
-
needsSync = true;
|
|
6158
|
-
}
|
|
6159
|
-
}
|
|
6160
|
-
if (needsSync) this.#syncValue();
|
|
6161
|
-
});
|
|
6162
|
-
this.#observer.observe(this, {
|
|
6163
|
-
childList: true,
|
|
6164
|
-
subtree: true,
|
|
6165
|
-
characterData: true,
|
|
6166
|
-
attributes: true,
|
|
6167
|
-
attributeFilter: ["value", "disabled", "selected", "label"],
|
|
6168
|
-
});
|
|
6169
|
-
}
|
|
6170
|
-
|
|
6171
|
-
#getOptions({ enabledOnly = false } = {}) {
|
|
6172
|
-
const panel = this.#getPanel();
|
|
6173
|
-
const options = panel
|
|
6174
|
-
? Array.from(panel.querySelectorAll(":scope > fig-select-option"))
|
|
6175
|
-
: [];
|
|
6176
|
-
if (!enabledOnly) return options;
|
|
6177
|
-
return options.filter((opt) => !figLabBooleanAttribute(opt, "disabled"));
|
|
6178
|
-
}
|
|
6179
|
-
|
|
6180
|
-
#optionValue(option) {
|
|
6181
|
-
if (!option) return "";
|
|
6182
|
-
if (typeof option.value === "string") return option.value;
|
|
6183
|
-
const attr = option.getAttribute?.("value");
|
|
6184
|
-
if (attr != null) return attr;
|
|
6185
|
-
return (option.textContent || "").trim();
|
|
6186
|
-
}
|
|
6187
|
-
|
|
6188
|
-
#optionLabel(option) {
|
|
6189
|
-
if (!option) return "";
|
|
6190
|
-
const labelAttr = option.getAttribute?.("label");
|
|
6191
|
-
if (labelAttr != null && labelAttr !== "") return labelAttr.trim();
|
|
6192
|
-
|
|
6193
|
-
// Ignore prepend/append slot content when deriving a label from children.
|
|
6194
|
-
const parts = [];
|
|
6195
|
-
for (const node of option.childNodes) {
|
|
6196
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
6197
|
-
const text = node.textContent?.trim();
|
|
6198
|
-
if (text) parts.push(text);
|
|
6199
|
-
continue;
|
|
6200
|
-
}
|
|
5972
|
+
for (const node of mutation.addedNodes) {
|
|
6201
5973
|
if (!(node instanceof Element)) continue;
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
}
|
|
6210
|
-
|
|
6211
|
-
#syncPopupAttrs() {
|
|
6212
|
-
if (!this.#popup) return;
|
|
6213
|
-
this.#popup.setAttribute(
|
|
6214
|
-
"position",
|
|
6215
|
-
this.getAttribute("position") || "bottom left",
|
|
6216
|
-
);
|
|
6217
|
-
const offset = this.getAttribute("offset");
|
|
6218
|
-
if (offset) this.#popup.setAttribute("offset", offset);
|
|
6219
|
-
else this.#popup.removeAttribute("offset");
|
|
6220
|
-
const closedby = this.getAttribute("closedby");
|
|
6221
|
-
if (closedby) this.#popup.setAttribute("closedby", closedby);
|
|
6222
|
-
else this.#popup.removeAttribute("closedby");
|
|
6223
|
-
}
|
|
6224
|
-
|
|
6225
|
-
#syncDisabled() {
|
|
6226
|
-
const disabled = figLabBooleanAttribute(this, "disabled");
|
|
6227
|
-
if (this.#button) {
|
|
6228
|
-
if (disabled) this.#button.setAttribute("disabled", "");
|
|
6229
|
-
else this.#button.removeAttribute("disabled");
|
|
6230
|
-
}
|
|
6231
|
-
if (disabled && this.open) this.open = false;
|
|
6232
|
-
}
|
|
6233
|
-
|
|
6234
|
-
#pickFallbackOption(options) {
|
|
6235
|
-
if (!options.length) return null;
|
|
6236
|
-
const selected = options.find((opt) =>
|
|
6237
|
-
figLabBooleanAttribute(opt, "selected"),
|
|
6238
|
-
);
|
|
6239
|
-
if (selected && !figLabBooleanAttribute(selected, "disabled")) {
|
|
6240
|
-
return selected;
|
|
6241
|
-
}
|
|
6242
|
-
return (
|
|
6243
|
-
options.find((opt) => !figLabBooleanAttribute(opt, "disabled")) ||
|
|
6244
|
-
options[0] ||
|
|
6245
|
-
null
|
|
6246
|
-
);
|
|
6247
|
-
}
|
|
6248
|
-
|
|
6249
|
-
#emitValueEvents(value) {
|
|
6250
|
-
this.dispatchEvent(
|
|
6251
|
-
new CustomEvent("input", {
|
|
6252
|
-
detail: value,
|
|
6253
|
-
bubbles: true,
|
|
6254
|
-
composed: true,
|
|
6255
|
-
}),
|
|
6256
|
-
);
|
|
6257
|
-
this.dispatchEvent(
|
|
6258
|
-
new CustomEvent("change", {
|
|
6259
|
-
detail: value,
|
|
6260
|
-
bubbles: true,
|
|
6261
|
-
composed: true,
|
|
6262
|
-
}),
|
|
6263
|
-
);
|
|
6264
|
-
}
|
|
6265
|
-
|
|
6266
|
-
#syncValue() {
|
|
6267
|
-
if (this.#syncingValue) return;
|
|
6268
|
-
this.#syncingValue = true;
|
|
6269
|
-
try {
|
|
6270
|
-
const options = this.#getOptions();
|
|
6271
|
-
const hasValueAttr = this.hasAttribute("value");
|
|
6272
|
-
const previousValue = hasValueAttr ? this.getAttribute("value") : null;
|
|
6273
|
-
let match = hasValueAttr
|
|
6274
|
-
? options.find((opt) => this.#optionValue(opt) === previousValue)
|
|
6275
|
-
: null;
|
|
6276
|
-
let valueCorrected = false;
|
|
6277
|
-
|
|
6278
|
-
if (!match) {
|
|
6279
|
-
if (hasValueAttr) {
|
|
6280
|
-
// Options may not be built yet (options attr sync). Keep value until then.
|
|
6281
|
-
if (!options.length) {
|
|
6282
|
-
if (this.#labelEl) {
|
|
6283
|
-
this.#labelEl.textContent =
|
|
6284
|
-
previousValue || this.getAttribute("label") || "";
|
|
6285
|
-
}
|
|
6286
|
-
return;
|
|
6287
|
-
}
|
|
6288
|
-
// Value orphaned (option removed / value attr changed) — clamp or clear.
|
|
6289
|
-
match = this.#pickFallbackOption(options);
|
|
6290
|
-
if (match) {
|
|
6291
|
-
const nextValue = this.#optionValue(match);
|
|
6292
|
-
if (previousValue !== nextValue) {
|
|
6293
|
-
this.setAttribute("value", nextValue);
|
|
6294
|
-
valueCorrected = true;
|
|
6295
|
-
}
|
|
6296
|
-
} else {
|
|
6297
|
-
this.removeAttribute("value");
|
|
6298
|
-
valueCorrected = true;
|
|
6299
|
-
}
|
|
6300
|
-
} else {
|
|
6301
|
-
// No host value yet — honor a selected option if present.
|
|
6302
|
-
match = options.find((opt) =>
|
|
6303
|
-
figLabBooleanAttribute(opt, "selected"),
|
|
6304
|
-
);
|
|
6305
|
-
if (match) {
|
|
6306
|
-
this.setAttribute("value", this.#optionValue(match));
|
|
6307
|
-
valueCorrected = true;
|
|
6308
|
-
}
|
|
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);
|
|
6309
5981
|
}
|
|
6310
5982
|
}
|
|
6311
|
-
|
|
6312
|
-
for (const opt of options) {
|
|
6313
|
-
const selected = opt === match;
|
|
6314
|
-
opt.setAttribute("aria-selected", selected ? "true" : "false");
|
|
6315
|
-
if (selected) opt.setAttribute("selected", "");
|
|
6316
|
-
else opt.removeAttribute("selected");
|
|
6317
|
-
}
|
|
6318
|
-
|
|
6319
|
-
const label =
|
|
6320
|
-
(match && this.#optionLabel(match)) || this.getAttribute("label") || "";
|
|
6321
|
-
if (this.#labelEl) this.#labelEl.textContent = label;
|
|
6322
|
-
|
|
6323
|
-
const ariaLabel = this.getAttribute("label") || "Select";
|
|
6324
|
-
this.#button?.setAttribute("aria-label", ariaLabel);
|
|
6325
|
-
|
|
6326
|
-
// Don't scrollToOption while open — reposition/sync would fight overflow paging.
|
|
6327
|
-
this.#getPanel()?.syncOverflow?.();
|
|
6328
|
-
|
|
6329
|
-
if (valueCorrected) {
|
|
6330
|
-
this.#emitValueEvents(this.getAttribute("value") ?? "");
|
|
6331
|
-
}
|
|
6332
|
-
} finally {
|
|
6333
|
-
this.#syncingValue = false;
|
|
6334
5983
|
}
|
|
6335
5984
|
}
|
|
6336
|
-
|
|
6337
|
-
#handleTriggerClick(e) {
|
|
6338
|
-
if (figLabBooleanAttribute(this, "disabled")) return;
|
|
6339
|
-
e.preventDefault();
|
|
6340
|
-
e.stopPropagation();
|
|
6341
|
-
const nextOpen = !this.open;
|
|
6342
|
-
if (nextOpen && this.#popup && this.#button) {
|
|
6343
|
-
this.#popup.anchor = this.#button;
|
|
6344
|
-
}
|
|
6345
|
-
this.open = nextOpen;
|
|
6346
|
-
}
|
|
6347
|
-
|
|
6348
|
-
#handleOptionClick(e) {
|
|
6349
|
-
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
6350
|
-
const option = path.find(
|
|
6351
|
-
(node) => node?.tagName === "FIG-SELECT-OPTION",
|
|
6352
|
-
);
|
|
6353
|
-
if (!option || !this.contains(option)) return;
|
|
6354
|
-
if (figLabBooleanAttribute(option, "disabled")) return;
|
|
6355
|
-
// Do not stopPropagation — React light-DOM onClick must still fire.
|
|
6356
|
-
this.#selectOption(option);
|
|
6357
|
-
}
|
|
6358
|
-
|
|
6359
|
-
#handleKeydown(e) {
|
|
6360
|
-
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
6361
|
-
|
|
6362
|
-
const listOpen = this.open && (this.#popup?.matches?.(":open") ?? false);
|
|
6363
|
-
if (!listOpen) {
|
|
6364
|
-
if (
|
|
6365
|
-
this.#button?.contains(e.target) &&
|
|
6366
|
-
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")
|
|
6367
|
-
) {
|
|
6368
|
-
e.preventDefault();
|
|
6369
|
-
if (this.#popup && this.#button) this.#popup.anchor = this.#button;
|
|
6370
|
-
this.open = true;
|
|
6371
|
-
requestAnimationFrame(() => {
|
|
6372
|
-
const options = this.#getOptions({ enabledOnly: true });
|
|
6373
|
-
const selectedIndex = options.findIndex(
|
|
6374
|
-
(opt) => this.#optionValue(opt) === this.value,
|
|
6375
|
-
);
|
|
6376
|
-
this.#focusOptionAt(selectedIndex >= 0 ? selectedIndex : 0);
|
|
6377
|
-
});
|
|
6378
|
-
}
|
|
6379
|
-
return;
|
|
6380
|
-
}
|
|
6381
|
-
|
|
6382
|
-
const options = this.#getOptions({ enabledOnly: true });
|
|
6383
|
-
if (!options.length) return;
|
|
6384
|
-
|
|
6385
|
-
switch (e.key) {
|
|
6386
|
-
case "ArrowDown":
|
|
6387
|
-
e.preventDefault();
|
|
6388
|
-
this.#syncFocusedIndex();
|
|
6389
|
-
this.#focusOptionAt(this.#focusedIndex + 1);
|
|
6390
|
-
break;
|
|
6391
|
-
case "ArrowUp":
|
|
6392
|
-
e.preventDefault();
|
|
6393
|
-
this.#syncFocusedIndex();
|
|
6394
|
-
this.#focusOptionAt(this.#focusedIndex - 1);
|
|
6395
|
-
break;
|
|
6396
|
-
case "Home":
|
|
6397
|
-
e.preventDefault();
|
|
6398
|
-
this.#focusOptionAt(0);
|
|
6399
|
-
break;
|
|
6400
|
-
case "End":
|
|
6401
|
-
e.preventDefault();
|
|
6402
|
-
this.#focusOptionAt(options.length - 1);
|
|
6403
|
-
break;
|
|
6404
|
-
case "Escape":
|
|
6405
|
-
e.preventDefault();
|
|
6406
|
-
this.open = false;
|
|
6407
|
-
this.#button?.focus();
|
|
6408
|
-
break;
|
|
6409
|
-
case "Enter":
|
|
6410
|
-
case " ": {
|
|
6411
|
-
this.#syncFocusedIndex();
|
|
6412
|
-
const focused = options[this.#focusedIndex];
|
|
6413
|
-
if (!focused) return;
|
|
6414
|
-
e.preventDefault();
|
|
6415
|
-
this.#selectOption(focused);
|
|
6416
|
-
break;
|
|
6417
|
-
}
|
|
6418
|
-
}
|
|
6419
|
-
}
|
|
6420
|
-
|
|
6421
|
-
#handlePopupClose() {
|
|
6422
|
-
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
6423
|
-
this.#button?.setAttribute("aria-expanded", "false");
|
|
6424
|
-
this.#button?.focus();
|
|
6425
|
-
this.#focusedIndex = -1;
|
|
6426
|
-
}
|
|
6427
|
-
|
|
6428
|
-
#selectOption(option) {
|
|
6429
|
-
const value = this.#optionValue(option);
|
|
6430
|
-
this.setAttribute("value", value);
|
|
6431
|
-
this.#syncValue();
|
|
6432
|
-
this.#emitValueEvents(value);
|
|
6433
|
-
this.open = false;
|
|
6434
|
-
}
|
|
6435
|
-
|
|
6436
|
-
#getEnabledOptions() {
|
|
6437
|
-
return this.#getOptions({ enabledOnly: true });
|
|
6438
|
-
}
|
|
6439
|
-
|
|
6440
|
-
#syncFocusedIndex() {
|
|
6441
|
-
const options = this.#getEnabledOptions();
|
|
6442
|
-
if (!options.length) {
|
|
6443
|
-
this.#focusedIndex = -1;
|
|
6444
|
-
return;
|
|
6445
|
-
}
|
|
6446
|
-
const active = options.find((opt) => opt === document.activeElement);
|
|
6447
|
-
const index = active ? options.indexOf(active) : -1;
|
|
6448
|
-
this.#focusedIndex = index >= 0 ? index : this.#focusedIndex;
|
|
6449
|
-
}
|
|
6450
|
-
|
|
6451
|
-
#focusOptionAt(index) {
|
|
6452
|
-
const options = this.#getEnabledOptions();
|
|
6453
|
-
if (!options.length) return;
|
|
6454
|
-
const next = ((index % options.length) + options.length) % options.length;
|
|
6455
|
-
this.#focusedIndex = next;
|
|
6456
|
-
options[next]?.focus();
|
|
6457
|
-
}
|
|
6458
|
-
|
|
6459
|
-
#syncPopupWidth() {
|
|
6460
|
-
if (!this.#popup || !this.#button) return;
|
|
6461
|
-
const hostWidth = Math.ceil(this.getBoundingClientRect().width);
|
|
6462
|
-
const triggerWidth = Math.ceil(this.#button.getBoundingClientRect().width);
|
|
6463
|
-
const anchorWidth = Math.max(hostWidth, triggerWidth, 96);
|
|
6464
|
-
const full = figLabBooleanAttribute(this, "full");
|
|
6465
|
-
|
|
6466
|
-
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
6467
|
-
// [full]: lock to host. Otherwise content-sized with host as min and 20rem max.
|
|
6468
|
-
if (full) {
|
|
6469
|
-
this.#popup.style.setProperty("width", `${anchorWidth}px`, "important");
|
|
6470
|
-
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
6471
|
-
this.#popup.style.setProperty("max-width", `${anchorWidth}px`, "important");
|
|
6472
|
-
} else {
|
|
6473
|
-
this.#popup.style.setProperty("width", "max-content", "important");
|
|
6474
|
-
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
6475
|
-
this.#popup.style.setProperty(
|
|
6476
|
-
"max-width",
|
|
6477
|
-
"min(20rem, calc(100vw - 1rem))",
|
|
6478
|
-
"important",
|
|
6479
|
-
);
|
|
6480
|
-
}
|
|
6481
|
-
}
|
|
6482
|
-
|
|
6483
|
-
#openList() {
|
|
6484
|
-
if (!this.#popup || figLabBooleanAttribute(this, "disabled")) return;
|
|
6485
|
-
if (this.#button) this.#popup.anchor = this.#button;
|
|
6486
|
-
this.#installPopupPositioning();
|
|
6487
|
-
this.#freezeMenuPosition = false;
|
|
6488
|
-
this.#frozenLabelRect = null;
|
|
6489
|
-
this.#frozenViewport = null;
|
|
6490
|
-
this.#syncValue();
|
|
6491
|
-
this.#syncPopupWidth();
|
|
6492
|
-
this.#popup.open = true;
|
|
6493
|
-
document.addEventListener("keydown", this.#boundKeydown, true);
|
|
6494
|
-
this.#button?.setAttribute("aria-expanded", "true");
|
|
6495
|
-
this.#focusedIndex = -1;
|
|
6496
|
-
requestAnimationFrame(() => {
|
|
6497
|
-
this.#syncPopupWidth();
|
|
6498
|
-
this.#positionPopupOverSelected();
|
|
6499
|
-
const panel = this.#getPanel();
|
|
6500
|
-
const options = this.#getEnabledOptions();
|
|
6501
|
-
const selectedIndex = options.findIndex(
|
|
6502
|
-
(opt) => this.#optionValue(opt) === this.value,
|
|
6503
|
-
);
|
|
6504
|
-
if (selectedIndex >= 0) {
|
|
6505
|
-
this.#focusOptionAt(selectedIndex);
|
|
6506
|
-
} else if (
|
|
6507
|
-
this.#button?.hasAttribute("data-focus-visible") ||
|
|
6508
|
-
this.#button?.matches?.(":focus-visible")
|
|
6509
|
-
) {
|
|
6510
|
-
this.#focusOptionAt(0);
|
|
6511
|
-
}
|
|
6512
|
-
panel?.syncOverflow?.();
|
|
6513
|
-
// Freeze after open align so later positionPopup passes don't undo scroll.
|
|
6514
|
-
// Window resize / trigger movement still realigns via geometry checks.
|
|
6515
|
-
this.#freezeMenuPosition = true;
|
|
6516
|
-
this.#rememberFrozenGeometry();
|
|
6517
|
-
});
|
|
6518
|
-
}
|
|
6519
|
-
|
|
6520
|
-
#closeList() {
|
|
6521
|
-
if (!this.#popup) return;
|
|
6522
|
-
this.#freezeMenuPosition = false;
|
|
6523
|
-
this.#frozenLabelRect = null;
|
|
6524
|
-
this.#frozenViewport = null;
|
|
6525
|
-
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
6526
|
-
this.#popup.open = false;
|
|
6527
|
-
this.#button?.setAttribute("aria-expanded", "false");
|
|
6528
|
-
}
|
|
6529
|
-
}
|
|
6530
|
-
customElements.define("fig-select", FigSelect);
|
|
5985
|
+
}).observe(document.documentElement, { childList: true, subtree: true });
|