@rogieking/figui3 9.0.10 → 9.0.12
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/fig-editor/SKILL.md +1 -1
- package/.cursor/skills/fig-editor/components.md +2 -1
- package/.cursor/skills/fig-editor/reference.md +3 -2
- package/README.md +9 -1
- package/components.css +5 -4
- package/dist/components.css +1 -1
- package/dist/fig-editor.js +2 -2
- package/dist/fig.css +1 -1
- package/dist/fig.js +6 -2
- package/fig-editor.js +182 -32
- package/fig.js +17 -2
- package/package.json +1 -1
package/fig-editor.js
CHANGED
|
@@ -495,6 +495,8 @@ figEditorDefineElement("fig-select-options", FigSelectOptions);
|
|
|
495
495
|
* A dropdown-styled select.
|
|
496
496
|
* @attr {string} variant - Visual style. Use `ghost` for a borderless control.
|
|
497
497
|
* @attr {string} size - Control size. Use `large` for a 32px-tall control.
|
|
498
|
+
* @attr {string} menu-anchor - CSS selector for an alternate menu anchor.
|
|
499
|
+
* @property {Element|string|null} menuAnchor - Alternate menu anchor element or selector.
|
|
498
500
|
* @event optionfocus - Fires with the focused option value during open-menu keyboard navigation.
|
|
499
501
|
*/
|
|
500
502
|
class FigSelect extends HTMLElement {
|
|
@@ -521,6 +523,7 @@ class FigSelect extends HTMLElement {
|
|
|
521
523
|
#frozenLabelRect = null;
|
|
522
524
|
#frozenViewport = null;
|
|
523
525
|
#syncingOptions = false;
|
|
526
|
+
#menuAnchorRef = null;
|
|
524
527
|
#boundTriggerClick = this.#handleTriggerClick.bind(this);
|
|
525
528
|
#boundOptionClick = this.#handleOptionClick.bind(this);
|
|
526
529
|
#boundOptionPointerOver = this.#handleOptionPointerOver.bind(this);
|
|
@@ -540,6 +543,7 @@ class FigSelect extends HTMLElement {
|
|
|
540
543
|
"open",
|
|
541
544
|
"variant",
|
|
542
545
|
"size",
|
|
546
|
+
"menu-anchor",
|
|
543
547
|
"aria-label",
|
|
544
548
|
];
|
|
545
549
|
}
|
|
@@ -562,6 +566,26 @@ class FigSelect extends HTMLElement {
|
|
|
562
566
|
else this.removeAttribute("open");
|
|
563
567
|
}
|
|
564
568
|
|
|
569
|
+
get menuAnchor() {
|
|
570
|
+
return this.#menuAnchorRef ?? this.getAttribute("menu-anchor");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
set menuAnchor(value) {
|
|
574
|
+
if (value instanceof Element) {
|
|
575
|
+
if (this.hasAttribute("menu-anchor")) {
|
|
576
|
+
this.removeAttribute("menu-anchor");
|
|
577
|
+
}
|
|
578
|
+
this.#menuAnchorRef = value;
|
|
579
|
+
} else if (typeof value === "string" && value.trim()) {
|
|
580
|
+
this.#menuAnchorRef = null;
|
|
581
|
+
this.setAttribute("menu-anchor", value);
|
|
582
|
+
} else {
|
|
583
|
+
this.#menuAnchorRef = null;
|
|
584
|
+
this.removeAttribute("menu-anchor");
|
|
585
|
+
}
|
|
586
|
+
this.#syncMenuAnchor();
|
|
587
|
+
}
|
|
588
|
+
|
|
565
589
|
connectedCallback() {
|
|
566
590
|
if (!this.#initialized) this.#initialize();
|
|
567
591
|
this.#ensurePanelSlotAttrs();
|
|
@@ -606,8 +630,14 @@ class FigSelect extends HTMLElement {
|
|
|
606
630
|
else this.#openList();
|
|
607
631
|
return;
|
|
608
632
|
}
|
|
633
|
+
if (name === "menu-anchor") {
|
|
634
|
+
this.#menuAnchorRef = null;
|
|
635
|
+
this.#syncMenuAnchor();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
609
638
|
if (name === "position" || name === "offset" || name === "closedby") {
|
|
610
639
|
this.#syncPopupAttrs();
|
|
640
|
+
if (this.open) this.#placeOpenMenu();
|
|
611
641
|
}
|
|
612
642
|
}
|
|
613
643
|
|
|
@@ -875,7 +905,7 @@ class FigSelect extends HTMLElement {
|
|
|
875
905
|
this.#labelEl = labelEl;
|
|
876
906
|
this.#popup = popup;
|
|
877
907
|
this.#panelSlot = panelSlot;
|
|
878
|
-
|
|
908
|
+
this.#syncMenuAnchor();
|
|
879
909
|
|
|
880
910
|
this.#ensurePanelSlotAttrs();
|
|
881
911
|
this.#installPopupPositioning();
|
|
@@ -897,7 +927,7 @@ class FigSelect extends HTMLElement {
|
|
|
897
927
|
this.#originalPositionPopup?.();
|
|
898
928
|
return;
|
|
899
929
|
}
|
|
900
|
-
this.#
|
|
930
|
+
this.#placeOpenMenu();
|
|
901
931
|
};
|
|
902
932
|
this.#popupPositionPatched = true;
|
|
903
933
|
}
|
|
@@ -920,7 +950,53 @@ class FigSelect extends HTMLElement {
|
|
|
920
950
|
return { top: 8, right: 8, bottom: 8, left: 8 };
|
|
921
951
|
}
|
|
922
952
|
|
|
953
|
+
#resolveMenuAnchor() {
|
|
954
|
+
if (this.#menuAnchorRef?.isConnected) return this.#menuAnchorRef;
|
|
955
|
+
|
|
956
|
+
const selector = this.getAttribute("menu-anchor");
|
|
957
|
+
if (selector) {
|
|
958
|
+
try {
|
|
959
|
+
const localMatch = this.parentElement?.querySelector(selector);
|
|
960
|
+
if (
|
|
961
|
+
localMatch &&
|
|
962
|
+
localMatch !== this &&
|
|
963
|
+
!this.contains(localMatch)
|
|
964
|
+
) {
|
|
965
|
+
return localMatch;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const root = this.getRootNode();
|
|
969
|
+
const rootMatch = root?.querySelector?.(selector);
|
|
970
|
+
if (
|
|
971
|
+
rootMatch &&
|
|
972
|
+
rootMatch !== this &&
|
|
973
|
+
!this.contains(rootMatch)
|
|
974
|
+
) {
|
|
975
|
+
return rootMatch;
|
|
976
|
+
}
|
|
977
|
+
} catch {}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return this.#button;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
#syncMenuAnchor() {
|
|
984
|
+
if (!this.#popup) return;
|
|
985
|
+
this.#popup.anchor = this.#resolveMenuAnchor();
|
|
986
|
+
if (!this.open) return;
|
|
987
|
+
this.#freezeMenuPosition = false;
|
|
988
|
+
this.#frozenLabelRect = null;
|
|
989
|
+
this.#frozenViewport = null;
|
|
990
|
+
this.#popup.queueReposition?.();
|
|
991
|
+
}
|
|
992
|
+
|
|
923
993
|
#getTriggerAlignmentRect() {
|
|
994
|
+
const menuAnchor = this.#resolveMenuAnchor();
|
|
995
|
+
if (menuAnchor && menuAnchor !== this.#button) {
|
|
996
|
+
const anchorRect = menuAnchor.getBoundingClientRect();
|
|
997
|
+
if (anchorRect?.width && anchorRect?.height) return anchorRect;
|
|
998
|
+
}
|
|
999
|
+
|
|
924
1000
|
const labelRect = this.#labelEl?.getBoundingClientRect();
|
|
925
1001
|
if (labelRect?.width && labelRect?.height) return labelRect;
|
|
926
1002
|
const customTriggerRect = this.querySelector(
|
|
@@ -995,10 +1071,56 @@ class FigSelect extends HTMLElement {
|
|
|
995
1071
|
this.#frozenViewport = this.#readViewportSnapshot();
|
|
996
1072
|
}
|
|
997
1073
|
|
|
1074
|
+
#usesExplicitPosition() {
|
|
1075
|
+
const value = this.getAttribute("position");
|
|
1076
|
+
return Boolean(value && value.trim());
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
#clearOverlayPositionStyles() {
|
|
1080
|
+
if (!this.#popup) return;
|
|
1081
|
+
this.#popup.style.removeProperty("left");
|
|
1082
|
+
this.#popup.style.removeProperty("top");
|
|
1083
|
+
this.#popup.style.removeProperty("right");
|
|
1084
|
+
this.#popup.style.removeProperty("bottom");
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
#placeOpenMenu() {
|
|
1088
|
+
if (this.#usesExplicitPosition()) {
|
|
1089
|
+
this.#freezeMenuPosition = false;
|
|
1090
|
+
this.#frozenLabelRect = null;
|
|
1091
|
+
this.#frozenViewport = null;
|
|
1092
|
+
this.#clearOverlayPositionStyles();
|
|
1093
|
+
this.#originalPositionPopup?.();
|
|
1094
|
+
this.#clampMenuToViewport();
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
this.#positionPopupOverSelected();
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
#clampMenuToViewport() {
|
|
1101
|
+
const popup = this.#popup;
|
|
1102
|
+
if (!popup || typeof popup.clampToViewport !== "function") return;
|
|
1103
|
+
const popupRect = popup.getBoundingClientRect();
|
|
1104
|
+
if (!popupRect.width || !popupRect.height) return;
|
|
1105
|
+
const margins = this.#getViewportMargins();
|
|
1106
|
+
const { left, top } = popup.clampToViewport(
|
|
1107
|
+
{ left: popupRect.left, top: popupRect.top },
|
|
1108
|
+
popupRect,
|
|
1109
|
+
margins,
|
|
1110
|
+
);
|
|
1111
|
+
popup.style.setProperty("right", "auto", "important");
|
|
1112
|
+
popup.style.setProperty("bottom", "auto", "important");
|
|
1113
|
+
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
1114
|
+
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
1115
|
+
}
|
|
1116
|
+
|
|
998
1117
|
#positionPopupOverSelected() {
|
|
999
1118
|
// Content ResizeObserver / overflow scroll re-enter here; keep the
|
|
1000
1119
|
// open-time alignment unless the trigger or viewport actually changed.
|
|
1001
|
-
if (this.#shouldSkipFrozenPositionPass())
|
|
1120
|
+
if (this.#shouldSkipFrozenPositionPass()) {
|
|
1121
|
+
this.#clampMenuToViewport();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1002
1124
|
|
|
1003
1125
|
const popup = this.#popup;
|
|
1004
1126
|
const label = this.#labelEl;
|
|
@@ -1395,9 +1517,7 @@ class FigSelect extends HTMLElement {
|
|
|
1395
1517
|
e.preventDefault();
|
|
1396
1518
|
e.stopPropagation();
|
|
1397
1519
|
const nextOpen = !this.open;
|
|
1398
|
-
if (nextOpen
|
|
1399
|
-
this.#popup.anchor = this.#button;
|
|
1400
|
-
}
|
|
1520
|
+
if (nextOpen) this.#syncMenuAnchor();
|
|
1401
1521
|
this.open = nextOpen;
|
|
1402
1522
|
}
|
|
1403
1523
|
|
|
@@ -1448,7 +1568,7 @@ class FigSelect extends HTMLElement {
|
|
|
1448
1568
|
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")
|
|
1449
1569
|
) {
|
|
1450
1570
|
e.preventDefault();
|
|
1451
|
-
|
|
1571
|
+
this.#syncMenuAnchor();
|
|
1452
1572
|
this.open = true;
|
|
1453
1573
|
requestAnimationFrame(() => {
|
|
1454
1574
|
const options = this.#getOptions({ enabledOnly: true });
|
|
@@ -1557,18 +1677,41 @@ class FigSelect extends HTMLElement {
|
|
|
1557
1677
|
|
|
1558
1678
|
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
1559
1679
|
// Menus size to their options while remaining at least as wide as the trigger.
|
|
1560
|
-
|
|
1561
|
-
|
|
1680
|
+
const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
|
|
1681
|
+
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
|
1682
|
+
const rem =
|
|
1683
|
+
parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
1684
|
+
const maxWidth = Math.max(
|
|
1685
|
+
0,
|
|
1686
|
+
Math.min(Math.floor(20 * rem), Math.floor(viewportWidth - 16)),
|
|
1687
|
+
);
|
|
1688
|
+
const maxHeight = Math.max(0, Math.floor(viewportHeight - 16));
|
|
1689
|
+
this.#popup.style.setProperty("box-sizing", "border-box", "important");
|
|
1690
|
+
this.#popup.style.setProperty("padding", "0", "important");
|
|
1691
|
+
this.#popup.style.setProperty("border", "0", "important");
|
|
1692
|
+
this.#popup.style.setProperty("margin", "0", "important");
|
|
1562
1693
|
this.#popup.style.setProperty(
|
|
1563
|
-
"
|
|
1564
|
-
|
|
1694
|
+
"width",
|
|
1695
|
+
`min(max-content, ${maxWidth}px)`,
|
|
1565
1696
|
"important",
|
|
1566
1697
|
);
|
|
1698
|
+
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
1699
|
+
this.#popup.style.setProperty("max-width", `${maxWidth}px`, "important");
|
|
1700
|
+
this.#popup.style.setProperty("max-height", `${maxHeight}px`, "important");
|
|
1701
|
+
this.#popup.style.setProperty("height", "auto", "important");
|
|
1702
|
+
this.#popup.style.setProperty("overflow", "hidden", "important");
|
|
1703
|
+
const used = this.#popup.getBoundingClientRect();
|
|
1704
|
+
if (used.height > maxHeight) {
|
|
1705
|
+
this.#popup.style.setProperty("height", `${maxHeight}px`, "important");
|
|
1706
|
+
}
|
|
1707
|
+
if (used.width > maxWidth) {
|
|
1708
|
+
this.#popup.style.setProperty("width", `${maxWidth}px`, "important");
|
|
1709
|
+
}
|
|
1567
1710
|
}
|
|
1568
1711
|
|
|
1569
1712
|
#openList() {
|
|
1570
1713
|
if (!this.#popup || figEditorBooleanAttribute(this, "disabled")) return;
|
|
1571
|
-
|
|
1714
|
+
this.#syncMenuAnchor();
|
|
1572
1715
|
this.#installPopupPositioning();
|
|
1573
1716
|
this.#freezeMenuPosition = false;
|
|
1574
1717
|
this.#frozenLabelRect = null;
|
|
@@ -1580,26 +1723,33 @@ class FigSelect extends HTMLElement {
|
|
|
1580
1723
|
this.#button?.setAttribute("aria-expanded", "true");
|
|
1581
1724
|
this.#focusedIndex = -1;
|
|
1582
1725
|
requestAnimationFrame(() => {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1726
|
+
requestAnimationFrame(() => {
|
|
1727
|
+
this.#syncPopupWidth();
|
|
1728
|
+
this.#placeOpenMenu();
|
|
1729
|
+
this.#syncPopupWidth();
|
|
1730
|
+
this.#clampMenuToViewport();
|
|
1731
|
+
const panel = this.#getPanel();
|
|
1732
|
+
const options = this.#getEnabledOptions();
|
|
1733
|
+
const selectedIndex = options.findIndex(
|
|
1734
|
+
(opt) => this.#optionValue(opt) === this.value,
|
|
1735
|
+
);
|
|
1736
|
+
if (selectedIndex >= 0) {
|
|
1737
|
+
this.#focusOptionAt(selectedIndex);
|
|
1738
|
+
} else if (
|
|
1739
|
+
this.#button?.hasAttribute("data-focus-visible") ||
|
|
1740
|
+
this.#button?.matches?.(":focus-visible")
|
|
1741
|
+
) {
|
|
1742
|
+
this.#focusOptionAt(0);
|
|
1743
|
+
}
|
|
1744
|
+
panel?.syncOverflow?.();
|
|
1745
|
+
// Freeze overlay after layout settles so later positionPopup passes
|
|
1746
|
+
// don't undo scroll. Still clamp if content grows past the viewport.
|
|
1747
|
+
// Explicit position stays live so the popup can follow the anchor.
|
|
1748
|
+
if (!this.#usesExplicitPosition()) {
|
|
1749
|
+
this.#freezeMenuPosition = true;
|
|
1750
|
+
this.#rememberFrozenGeometry();
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1603
1753
|
});
|
|
1604
1754
|
}
|
|
1605
1755
|
|
package/fig.js
CHANGED
|
@@ -318,6 +318,10 @@ fig-select-options > .fig-overflow-end::before,
|
|
|
318
318
|
.fig-menu-options > .fig-overflow-end::before {
|
|
319
319
|
border-radius: 0 0 var(--radius-large) var(--radius-large);
|
|
320
320
|
}
|
|
321
|
+
fig-select-options.overflow-start > :is(fig-separator, fig-menu-separator)[sticky]:not([sticky="false"]),
|
|
322
|
+
.fig-menu-options.overflow-start > :is(fig-separator, fig-menu-separator)[sticky]:not([sticky="false"]) {
|
|
323
|
+
top: var(--fig-vertical-overflow-size, var(--spacer-4)) !important;
|
|
324
|
+
}
|
|
321
325
|
`;
|
|
322
326
|
|
|
323
327
|
const FIG_SHADOW_ICON_CSS = `
|
|
@@ -3933,10 +3937,12 @@ class FigPopup extends HTMLDialogElement {
|
|
|
3933
3937
|
const bounds = this.getViewportBounds(m);
|
|
3934
3938
|
const maxLeft = bounds.maxRight - popupRect.width;
|
|
3935
3939
|
const maxTop = bounds.maxBottom - popupRect.height;
|
|
3940
|
+
const clampLeftMax = Math.max(bounds.minLeft, maxLeft);
|
|
3941
|
+
const clampTopMax = Math.max(bounds.minTop, maxTop);
|
|
3936
3942
|
|
|
3937
3943
|
return {
|
|
3938
|
-
left: Math.min(
|
|
3939
|
-
top: Math.min(
|
|
3944
|
+
left: Math.min(clampLeftMax, Math.max(bounds.minLeft, coords.left)),
|
|
3945
|
+
top: Math.min(clampTopMax, Math.max(bounds.minTop, coords.top)),
|
|
3940
3946
|
};
|
|
3941
3947
|
}
|
|
3942
3948
|
|
|
@@ -18464,6 +18470,12 @@ class FigChooser extends HTMLElement {
|
|
|
18464
18470
|
}
|
|
18465
18471
|
};
|
|
18466
18472
|
|
|
18473
|
+
const onDragStart = (e) => {
|
|
18474
|
+
if (this.#dragState?.active) {
|
|
18475
|
+
e.preventDefault();
|
|
18476
|
+
}
|
|
18477
|
+
};
|
|
18478
|
+
|
|
18467
18479
|
this.#dragState = {
|
|
18468
18480
|
active: false,
|
|
18469
18481
|
didDrag: false,
|
|
@@ -18478,6 +18490,7 @@ class FigChooser extends HTMLElement {
|
|
|
18478
18490
|
onPointerUp,
|
|
18479
18491
|
onClick,
|
|
18480
18492
|
onPointerUpCapture,
|
|
18493
|
+
onDragStart,
|
|
18481
18494
|
};
|
|
18482
18495
|
|
|
18483
18496
|
this.addEventListener("pointerdown", onPointerDown);
|
|
@@ -18485,6 +18498,7 @@ class FigChooser extends HTMLElement {
|
|
|
18485
18498
|
window.addEventListener("pointerup", onPointerUp);
|
|
18486
18499
|
this.addEventListener("pointerup", onPointerUpCapture, true);
|
|
18487
18500
|
this.addEventListener("click", onClick, true);
|
|
18501
|
+
this.addEventListener("dragstart", onDragStart);
|
|
18488
18502
|
}
|
|
18489
18503
|
|
|
18490
18504
|
#teardownDrag() {
|
|
@@ -18498,6 +18512,7 @@ class FigChooser extends HTMLElement {
|
|
|
18498
18512
|
true,
|
|
18499
18513
|
);
|
|
18500
18514
|
this.removeEventListener("click", this.#dragState.onClick, true);
|
|
18515
|
+
this.removeEventListener("dragstart", this.#dragState.onDragStart);
|
|
18501
18516
|
this.style.cursor = "";
|
|
18502
18517
|
this.style.userSelect = "";
|
|
18503
18518
|
this.#dragState = null;
|
package/package.json
CHANGED