@rogieking/figui3 7.0.0 → 8.0.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/fig-lab.js CHANGED
@@ -1205,6 +1205,7 @@ class PropskitSelect extends HTMLElement {
1205
1205
  #field = null;
1206
1206
  #label = null;
1207
1207
  #select = null;
1208
+ #usesFigSelect = false;
1208
1209
  #hasCustomLabel = false;
1209
1210
  #observer = null;
1210
1211
  #managedSelectAttrs = new Set();
@@ -1221,6 +1222,49 @@ class PropskitSelect extends HTMLElement {
1221
1222
  return ["label", "direction", "aria-label", "options", "value"];
1222
1223
  }
1223
1224
 
1225
+ static #canUseFigSelect() {
1226
+ return ["fig-select", "fig-select-options", "fig-select-option"].every(
1227
+ (tag) => {
1228
+ const Constructor = customElements.get(tag);
1229
+ return (
1230
+ typeof Constructor === "function" &&
1231
+ Constructor.prototype instanceof HTMLElement
1232
+ );
1233
+ },
1234
+ );
1235
+ }
1236
+
1237
+ static #parseOptionsAttribute(raw) {
1238
+ const text = raw || "";
1239
+ if (text.startsWith("[")) {
1240
+ try {
1241
+ const parsed = JSON.parse(text);
1242
+ return Array.isArray(parsed) ? parsed : [];
1243
+ } catch {
1244
+ /* fall through */
1245
+ }
1246
+ }
1247
+ const delimiter = text.includes("\n") ? "\n" : ",";
1248
+ return text
1249
+ .split(delimiter)
1250
+ .map((s) => s.trim())
1251
+ .filter(Boolean);
1252
+ }
1253
+
1254
+ static #optionEntryValue(opt) {
1255
+ if (opt && typeof opt === "object") {
1256
+ return String(opt.value ?? opt.label ?? "");
1257
+ }
1258
+ return String(opt ?? "");
1259
+ }
1260
+
1261
+ static #optionEntryLabel(opt) {
1262
+ if (opt && typeof opt === "object") {
1263
+ return String(opt.label ?? opt.value ?? "");
1264
+ }
1265
+ return String(opt ?? "");
1266
+ }
1267
+
1224
1268
  connectedCallback() {
1225
1269
  if (!this.#field) this.#initialize();
1226
1270
  this.#syncField();
@@ -1283,9 +1327,12 @@ class PropskitSelect extends HTMLElement {
1283
1327
  const customLabel = this.querySelector(":scope > label");
1284
1328
  const field = document.createElement("fig-field");
1285
1329
  const label = customLabel || document.createElement("label");
1286
- const select = document.createElement("fig-select");
1287
- // Match menu width to the full-surface control.
1288
- select.setAttribute("full", "");
1330
+ this.#usesFigSelect = PropskitSelect.#canUseFigSelect();
1331
+ const select = document.createElement(
1332
+ this.#usesFigSelect ? "fig-select" : "fig-dropdown",
1333
+ );
1334
+ // Match menu width to the full-surface control when fig-select is available.
1335
+ if (this.#usesFigSelect) select.setAttribute("full", "");
1289
1336
  field.append(label, select);
1290
1337
  this.#field = field;
1291
1338
  this.#label = label;
@@ -1322,7 +1369,7 @@ class PropskitSelect extends HTMLElement {
1322
1369
  "Select",
1323
1370
  );
1324
1371
  // Always match menu width to the control surface.
1325
- this.#select.setAttribute("full", "");
1372
+ if (this.#usesFigSelect) this.#select.setAttribute("full", "");
1326
1373
  }
1327
1374
 
1328
1375
  #getForwardedSelectAttrNames() {
@@ -1339,13 +1386,31 @@ class PropskitSelect extends HTMLElement {
1339
1386
  "full",
1340
1387
  "default",
1341
1388
  ]);
1389
+ // fig-dropdown consumes light-DOM <option>s, not an options attribute.
1390
+ if (!this.#usesFigSelect) reserved.add("options");
1342
1391
  return this.getAttributeNames().filter(
1343
1392
  (name) => !reserved.has(name) && !name.startsWith("data-"),
1344
1393
  );
1345
1394
  }
1346
1395
 
1396
+ #syncDropdownOptions() {
1397
+ if (!this.#select || this.#usesFigSelect) return;
1398
+ const parsed = PropskitSelect.#parseOptionsAttribute(
1399
+ this.getAttribute("options"),
1400
+ );
1401
+ this.#select.replaceChildren(
1402
+ ...parsed.map((entry) => {
1403
+ const option = document.createElement("option");
1404
+ option.value = PropskitSelect.#optionEntryValue(entry);
1405
+ option.textContent = PropskitSelect.#optionEntryLabel(entry);
1406
+ return option;
1407
+ }),
1408
+ );
1409
+ }
1410
+
1347
1411
  #syncSelectAttributes() {
1348
1412
  if (!this.#select) return;
1413
+ if (!this.#usesFigSelect) this.#syncDropdownOptions();
1349
1414
  const selectAttrs = this.#getForwardedSelectAttrNames().sort((a, b) => {
1350
1415
  // Build options before applying value so fig-select can resolve selection.
1351
1416
  if (a === "options") return -1;
@@ -1370,13 +1435,15 @@ class PropskitSelect extends HTMLElement {
1370
1435
  if (!this.#select) return;
1371
1436
  this.#boundHandleInput ??= this.#forwardSelectEvent.bind(this, "input");
1372
1437
  this.#boundHandleChange ??= this.#forwardSelectEvent.bind(this, "change");
1373
- this.#boundHandleOptionHover ??= this.#forwardSelectEvent.bind(
1374
- this,
1375
- "optionhover",
1376
- );
1377
1438
  this.#select.addEventListener("input", this.#boundHandleInput);
1378
1439
  this.#select.addEventListener("change", this.#boundHandleChange);
1379
- this.#select.addEventListener("optionhover", this.#boundHandleOptionHover);
1440
+ if (this.#usesFigSelect) {
1441
+ this.#boundHandleOptionHover ??= this.#forwardSelectEvent.bind(
1442
+ this,
1443
+ "optionhover",
1444
+ );
1445
+ this.#select.addEventListener("optionhover", this.#boundHandleOptionHover);
1446
+ }
1380
1447
  }
1381
1448
 
1382
1449
  #unbindSelectEvents() {
@@ -1415,7 +1482,7 @@ class PropskitSelect extends HTMLElement {
1415
1482
  }
1416
1483
 
1417
1484
  #isSelectMenuOpen() {
1418
- if (!this.#select) return false;
1485
+ if (!this.#select || !this.#usesFigSelect) return false;
1419
1486
  if (this.#select.open) return true;
1420
1487
  const popup = this.#select.shadowRoot?.querySelector(
1421
1488
  'dialog[is="fig-popup"]',
@@ -1426,11 +1493,12 @@ class PropskitSelect extends HTMLElement {
1426
1493
  #handlePointerDown(event) {
1427
1494
  if (!(event.target instanceof Element)) return;
1428
1495
  if (event.target.closest("fig-menu")) return;
1429
- // fig-select owns its trigger/option clicks.
1430
- if (event.target.closest("fig-select")) {
1496
+ // Native dropdown / fig-select own their own trigger clicks.
1497
+ if (event.target.closest("fig-select, fig-dropdown")) {
1431
1498
  this.#closeGesture = false;
1432
1499
  return;
1433
1500
  }
1501
+ if (!this.#usesFigSelect) return;
1434
1502
  // Light-dismiss closes on pointerdown; remember so click doesn't reopen.
1435
1503
  this.#closeGesture = this.#isSelectMenuOpen();
1436
1504
  }
@@ -1439,7 +1507,10 @@ class PropskitSelect extends HTMLElement {
1439
1507
  if (event.target instanceof Element && event.target.closest("fig-menu")) {
1440
1508
  return;
1441
1509
  }
1442
- if (event.target instanceof Element && event.target.closest("fig-select")) {
1510
+ if (
1511
+ event.target instanceof Element &&
1512
+ event.target.closest("fig-select, fig-dropdown")
1513
+ ) {
1443
1514
  this.#closeGesture = false;
1444
1515
  return;
1445
1516
  }
@@ -1448,6 +1519,7 @@ class PropskitSelect extends HTMLElement {
1448
1519
  return;
1449
1520
  }
1450
1521
  this.#select.focus();
1522
+ if (!this.#usesFigSelect) return;
1451
1523
 
1452
1524
  if (this.#closeGesture || this.#isSelectMenuOpen()) {
1453
1525
  this.#closeGesture = false;
@@ -6701,168 +6773,3 @@ class FigReorder extends HTMLElement {
6701
6773
  }
6702
6774
 
6703
6775
  figLabDefineElement("fig-reorder", FigReorder);
6704
-
6705
- /*
6706
- * fig-select currently lives in fig.js. Keep the lab consumers resilient to
6707
- * reconnects and selected-option mutations until those core fixes can move
6708
- * with the component.
6709
- */
6710
- const figLabEnhancedSelects = new WeakSet();
6711
- const figLabDisconnectedSelects = new WeakSet();
6712
- const figLabReconnectFallbacks = new WeakSet();
6713
-
6714
- function figLabSelectOptions(select) {
6715
- return Array.from(select.querySelectorAll("fig-select-option"));
6716
- }
6717
-
6718
- function figLabSyncSelectedOption(select, requestedOption, emit = true) {
6719
- const options = figLabSelectOptions(select);
6720
- if (!options.length) return;
6721
- let option =
6722
- requestedOption && options.includes(requestedOption)
6723
- ? requestedOption
6724
- : options.find((candidate) => figLabBooleanAttribute(candidate, "selected"));
6725
- if (!option || figLabBooleanAttribute(option, "disabled")) {
6726
- option =
6727
- options.find((candidate) => !figLabBooleanAttribute(candidate, "disabled")) ||
6728
- options[0];
6729
- }
6730
- if (!option) return;
6731
-
6732
- const value = option.value ?? option.getAttribute("value") ?? "";
6733
- const previousValue = select.getAttribute("value") ?? "";
6734
- select.setAttribute("value", String(value));
6735
- for (const candidate of options) {
6736
- const selected = candidate === option;
6737
- candidate.toggleAttribute("selected", selected);
6738
- candidate.setAttribute("aria-selected", String(selected));
6739
- }
6740
- if (emit && previousValue !== String(value)) {
6741
- select.dispatchEvent(
6742
- new CustomEvent("input", {
6743
- detail: String(value),
6744
- bubbles: true,
6745
- composed: true,
6746
- }),
6747
- );
6748
- select.dispatchEvent(
6749
- new CustomEvent("change", {
6750
- detail: String(value),
6751
- bubbles: true,
6752
- composed: true,
6753
- }),
6754
- );
6755
- }
6756
- }
6757
-
6758
- function figLabEnhanceSelect(select) {
6759
- if (figLabEnhancedSelects.has(select)) return;
6760
- const trigger = select.shadowRoot?.querySelector(".fig-select-trigger");
6761
- if (!trigger) {
6762
- requestAnimationFrame(() => {
6763
- if (select.isConnected) figLabEnhanceSelect(select);
6764
- });
6765
- return;
6766
- }
6767
- figLabEnhancedSelects.add(select);
6768
-
6769
- const observer = new MutationObserver((mutations) => {
6770
- let selectedOption = null;
6771
- let selectedWasRemoved = false;
6772
- for (const mutation of mutations) {
6773
- if (
6774
- mutation.type !== "attributes" ||
6775
- mutation.attributeName !== "selected" ||
6776
- mutation.target.tagName !== "FIG-SELECT-OPTION"
6777
- ) {
6778
- continue;
6779
- }
6780
- if (figLabBooleanAttribute(mutation.target, "selected")) {
6781
- selectedOption = mutation.target;
6782
- } else if (
6783
- String(mutation.target.value ?? "") ===
6784
- (select.getAttribute("value") ?? "")
6785
- ) {
6786
- selectedWasRemoved = true;
6787
- }
6788
- }
6789
- if (selectedOption) {
6790
- const optionValue = String(selectedOption.value ?? "");
6791
- if ((select.getAttribute("value") ?? "") !== optionValue) {
6792
- figLabSyncSelectedOption(select, selectedOption);
6793
- }
6794
- } else if (selectedWasRemoved) {
6795
- figLabSyncSelectedOption(select, null);
6796
- }
6797
- });
6798
- observer.observe(select, {
6799
- attributes: true,
6800
- subtree: true,
6801
- attributeFilter: ["selected"],
6802
- });
6803
- }
6804
-
6805
- function figLabInstallSelectReconnectFallback(select) {
6806
- if (figLabReconnectFallbacks.has(select)) return;
6807
- const trigger = select.shadowRoot?.querySelector(".fig-select-trigger");
6808
- if (!trigger) return;
6809
- figLabReconnectFallbacks.add(select);
6810
-
6811
- trigger.addEventListener("click", () => {
6812
- if (figLabBooleanAttribute(select, "disabled")) return;
6813
- select.open = !select.open;
6814
- });
6815
- select.addEventListener("click", (event) => {
6816
- const option = event
6817
- .composedPath()
6818
- .find((node) => node?.tagName === "FIG-SELECT-OPTION");
6819
- if (
6820
- !option ||
6821
- !select.contains(option) ||
6822
- figLabBooleanAttribute(option, "disabled")
6823
- ) {
6824
- return;
6825
- }
6826
- figLabSyncSelectedOption(select, option);
6827
- select.open = false;
6828
- });
6829
- }
6830
-
6831
- function figLabMarkDisconnectedSelectTree(root) {
6832
- if (root instanceof Element && root.matches("fig-select")) {
6833
- figLabDisconnectedSelects.add(root);
6834
- }
6835
- root
6836
- .querySelectorAll?.("fig-select")
6837
- .forEach((select) => figLabDisconnectedSelects.add(select));
6838
- }
6839
-
6840
- function figLabEnhanceSelectTree(root) {
6841
- if (root instanceof Element && root.matches("fig-select")) {
6842
- figLabEnhanceSelect(root);
6843
- }
6844
- root
6845
- .querySelectorAll?.("fig-select")
6846
- .forEach((select) => figLabEnhanceSelect(select));
6847
- }
6848
-
6849
- figLabEnhanceSelectTree(document);
6850
- new MutationObserver((mutations) => {
6851
- for (const mutation of mutations) {
6852
- for (const node of mutation.removedNodes) {
6853
- if (node instanceof Element) figLabMarkDisconnectedSelectTree(node);
6854
- }
6855
- for (const node of mutation.addedNodes) {
6856
- if (!(node instanceof Element)) continue;
6857
- figLabEnhanceSelectTree(node);
6858
- const selects = node.matches("fig-select")
6859
- ? [node]
6860
- : [...node.querySelectorAll("fig-select")];
6861
- for (const select of selects) {
6862
- if (figLabDisconnectedSelects.has(select)) {
6863
- figLabInstallSelectReconnectFallback(select);
6864
- }
6865
- }
6866
- }
6867
- }
6868
- }).observe(document.documentElement, { childList: true, subtree: true });