@rogieking/figui3 7.1.0 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/fig-lab.css CHANGED
@@ -656,7 +656,7 @@ propskit-select {
656
656
  width: calc(50% - var(--spacer-2-5));
657
657
  }
658
658
 
659
- fig-select {
659
+ :is(fig-select, fig-dropdown) {
660
660
  padding-right: var(--spacer-1);
661
661
 
662
662
  &::after {
@@ -667,6 +667,10 @@ propskit-select {
667
667
  fig-select::part(trigger) {
668
668
  padding-left: var(--spacer-2-5);
669
669
  }
670
+
671
+ fig-dropdown > select {
672
+ padding-left: var(--spacer-2-5);
673
+ }
670
674
  }
671
675
  }
672
676
 
@@ -721,7 +725,7 @@ propskit-select {
721
725
  }
722
726
  }
723
727
 
724
- fig-select {
728
+ :is(fig-select, fig-dropdown) {
725
729
  position: relative;
726
730
  display: flex;
727
731
  flex: 1 1 auto;
@@ -732,13 +736,28 @@ propskit-select {
732
736
  border-radius: var(--radius-medium);
733
737
  background: transparent;
734
738
  box-shadow: none;
739
+ }
735
740
 
741
+ fig-select {
736
742
  --fig-select-trigger-width: 100%;
737
743
 
738
744
  &::part(label) {
739
745
  text-align: right;
740
746
  }
741
747
  }
748
+
749
+ /* fig-dropdown fallback: match fig-select trigger layout in the field. */
750
+ fig-dropdown > select {
751
+ width: 100%;
752
+ height: 100%;
753
+ min-width: 0;
754
+ padding: 0 var(--spacer-4) 0 var(--spacer-2);
755
+ text-align: right;
756
+ text-align-last: right;
757
+ color: inherit;
758
+ background: transparent;
759
+ box-shadow: none;
760
+ }
742
761
  }
743
762
  }
744
763
 
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,8 +1327,11 @@ 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.
1330
+ this.#usesFigSelect = PropskitSelect.#canUseFigSelect();
1331
+ const select = document.createElement(
1332
+ this.#usesFigSelect ? "fig-select" : "fig-dropdown",
1333
+ );
1334
+ // Match menu/control width to the full-surface field.
1288
1335
  select.setAttribute("full", "");
1289
1336
  field.append(label, select);
1290
1337
  this.#field = field;
@@ -1339,13 +1386,47 @@ 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
+ // Keep fig-dropdown's internal <select>; only replace slotted options.
1402
+ for (const child of [...this.#select.children]) {
1403
+ if (child.localName === "option" || child.localName === "optgroup") {
1404
+ child.remove();
1405
+ }
1406
+ }
1407
+ for (const entry of parsed) {
1408
+ const option = document.createElement("option");
1409
+ option.value = PropskitSelect.#optionEntryValue(entry);
1410
+ option.textContent = PropskitSelect.#optionEntryLabel(entry);
1411
+ this.#select.appendChild(option);
1412
+ }
1413
+ // Re-apply value after options land; fig-dropdown clones via async slotchange.
1414
+ const value = this.getAttribute("value");
1415
+ if (value != null) {
1416
+ const applyValue = () => {
1417
+ this.#select?.setAttribute("value", value);
1418
+ if (this.#select && "value" in this.#select) {
1419
+ this.#select.value = value;
1420
+ }
1421
+ };
1422
+ applyValue();
1423
+ queueMicrotask(applyValue);
1424
+ }
1425
+ }
1426
+
1347
1427
  #syncSelectAttributes() {
1348
1428
  if (!this.#select) return;
1429
+ if (!this.#usesFigSelect) this.#syncDropdownOptions();
1349
1430
  const selectAttrs = this.#getForwardedSelectAttrNames().sort((a, b) => {
1350
1431
  // Build options before applying value so fig-select can resolve selection.
1351
1432
  if (a === "options") return -1;
@@ -1370,13 +1451,15 @@ class PropskitSelect extends HTMLElement {
1370
1451
  if (!this.#select) return;
1371
1452
  this.#boundHandleInput ??= this.#forwardSelectEvent.bind(this, "input");
1372
1453
  this.#boundHandleChange ??= this.#forwardSelectEvent.bind(this, "change");
1373
- this.#boundHandleOptionHover ??= this.#forwardSelectEvent.bind(
1374
- this,
1375
- "optionhover",
1376
- );
1377
1454
  this.#select.addEventListener("input", this.#boundHandleInput);
1378
1455
  this.#select.addEventListener("change", this.#boundHandleChange);
1379
- this.#select.addEventListener("optionhover", this.#boundHandleOptionHover);
1456
+ if (this.#usesFigSelect) {
1457
+ this.#boundHandleOptionHover ??= this.#forwardSelectEvent.bind(
1458
+ this,
1459
+ "optionhover",
1460
+ );
1461
+ this.#select.addEventListener("optionhover", this.#boundHandleOptionHover);
1462
+ }
1380
1463
  }
1381
1464
 
1382
1465
  #unbindSelectEvents() {
@@ -1415,7 +1498,7 @@ class PropskitSelect extends HTMLElement {
1415
1498
  }
1416
1499
 
1417
1500
  #isSelectMenuOpen() {
1418
- if (!this.#select) return false;
1501
+ if (!this.#select || !this.#usesFigSelect) return false;
1419
1502
  if (this.#select.open) return true;
1420
1503
  const popup = this.#select.shadowRoot?.querySelector(
1421
1504
  'dialog[is="fig-popup"]',
@@ -1426,11 +1509,12 @@ class PropskitSelect extends HTMLElement {
1426
1509
  #handlePointerDown(event) {
1427
1510
  if (!(event.target instanceof Element)) return;
1428
1511
  if (event.target.closest("fig-menu")) return;
1429
- // fig-select owns its trigger/option clicks.
1430
- if (event.target.closest("fig-select")) {
1512
+ // Native dropdown / fig-select own their own trigger clicks.
1513
+ if (event.target.closest("fig-select, fig-dropdown")) {
1431
1514
  this.#closeGesture = false;
1432
1515
  return;
1433
1516
  }
1517
+ if (!this.#usesFigSelect) return;
1434
1518
  // Light-dismiss closes on pointerdown; remember so click doesn't reopen.
1435
1519
  this.#closeGesture = this.#isSelectMenuOpen();
1436
1520
  }
@@ -1439,7 +1523,10 @@ class PropskitSelect extends HTMLElement {
1439
1523
  if (event.target instanceof Element && event.target.closest("fig-menu")) {
1440
1524
  return;
1441
1525
  }
1442
- if (event.target instanceof Element && event.target.closest("fig-select")) {
1526
+ if (
1527
+ event.target instanceof Element &&
1528
+ event.target.closest("fig-select, fig-dropdown")
1529
+ ) {
1443
1530
  this.#closeGesture = false;
1444
1531
  return;
1445
1532
  }
@@ -1448,6 +1535,7 @@ class PropskitSelect extends HTMLElement {
1448
1535
  return;
1449
1536
  }
1450
1537
  this.#select.focus();
1538
+ if (!this.#usesFigSelect) return;
1451
1539
 
1452
1540
  if (this.#closeGesture || this.#isSelectMenuOpen()) {
1453
1541
  this.#closeGesture = false;
@@ -6701,168 +6789,3 @@ class FigReorder extends HTMLElement {
6701
6789
  }
6702
6790
 
6703
6791
  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 });