@rogieking/figui3 9.0.11 → 9.0.13
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 +3 -3
- package/dist/fig.css +1 -1
- package/dist/fig.js +5 -1
- package/fig-editor.js +308 -56
- package/fig.js +8 -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,6 +950,60 @@ class FigSelect extends HTMLElement {
|
|
|
920
950
|
return { top: 8, right: 8, bottom: 8, left: 8 };
|
|
921
951
|
}
|
|
922
952
|
|
|
953
|
+
#resolveExplicitMenuAnchor() {
|
|
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 null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
#resolveMenuAnchor() {
|
|
984
|
+
return this.#resolveExplicitMenuAnchor() || this.#button;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
#hasMenuAnchor() {
|
|
988
|
+
return Boolean(this.#resolveExplicitMenuAnchor());
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
#usesMenuAnchorOverlayGeometry() {
|
|
992
|
+
return !this.#usesExplicitPosition() && this.#hasMenuAnchor();
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
#syncMenuAnchor() {
|
|
996
|
+
if (!this.#popup) return;
|
|
997
|
+
this.#popup.anchor = this.#resolveMenuAnchor();
|
|
998
|
+
this.#syncPopupWidth();
|
|
999
|
+
if (!this.open) return;
|
|
1000
|
+
this.#freezeMenuPosition = false;
|
|
1001
|
+
this.#frozenLabelRect = null;
|
|
1002
|
+
this.#frozenViewport = null;
|
|
1003
|
+
this.#popup.setupObservers?.();
|
|
1004
|
+
this.#popup.queueReposition?.();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
923
1007
|
#getTriggerAlignmentRect() {
|
|
924
1008
|
const labelRect = this.#labelEl?.getBoundingClientRect();
|
|
925
1009
|
if (labelRect?.width && labelRect?.height) return labelRect;
|
|
@@ -933,7 +1017,11 @@ class FigSelect extends HTMLElement {
|
|
|
933
1017
|
}
|
|
934
1018
|
|
|
935
1019
|
#readLabelRectSnapshot() {
|
|
936
|
-
const
|
|
1020
|
+
const anchor = this.#usesMenuAnchorOverlayGeometry()
|
|
1021
|
+
? this.#resolveExplicitMenuAnchor()
|
|
1022
|
+
: null;
|
|
1023
|
+
const rect = anchor?.getBoundingClientRect() ??
|
|
1024
|
+
this.#getTriggerAlignmentRect();
|
|
937
1025
|
if (!rect) return null;
|
|
938
1026
|
return {
|
|
939
1027
|
x: rect.x,
|
|
@@ -995,10 +1083,99 @@ class FigSelect extends HTMLElement {
|
|
|
995
1083
|
this.#frozenViewport = this.#readViewportSnapshot();
|
|
996
1084
|
}
|
|
997
1085
|
|
|
1086
|
+
#usesExplicitPosition() {
|
|
1087
|
+
const value = this.getAttribute("position");
|
|
1088
|
+
return Boolean(value && value.trim());
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
#clearOverlayPositionStyles() {
|
|
1092
|
+
if (!this.#popup) return;
|
|
1093
|
+
this.#popup.style.removeProperty("left");
|
|
1094
|
+
this.#popup.style.removeProperty("top");
|
|
1095
|
+
this.#popup.style.removeProperty("right");
|
|
1096
|
+
this.#popup.style.removeProperty("bottom");
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
#placeOpenMenu() {
|
|
1100
|
+
if (this.#usesExplicitPosition()) {
|
|
1101
|
+
this.#freezeMenuPosition = false;
|
|
1102
|
+
this.#frozenLabelRect = null;
|
|
1103
|
+
this.#frozenViewport = null;
|
|
1104
|
+
this.#syncPopupWidth();
|
|
1105
|
+
this.#clearOverlayPositionStyles();
|
|
1106
|
+
if (this.#positionExplicitMenuAnchor()) return;
|
|
1107
|
+
this.#originalPositionPopup?.();
|
|
1108
|
+
this.#clampMenuToViewport();
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
this.#positionPopupOverSelected();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
#positionExplicitMenuAnchor() {
|
|
1115
|
+
const popup = this.#popup;
|
|
1116
|
+
const menuAnchor = this.#resolveExplicitMenuAnchor();
|
|
1117
|
+
if (
|
|
1118
|
+
!popup ||
|
|
1119
|
+
!menuAnchor ||
|
|
1120
|
+
typeof popup.parsePosition !== "function" ||
|
|
1121
|
+
typeof popup.parseOffset !== "function" ||
|
|
1122
|
+
typeof popup.computeCoords !== "function"
|
|
1123
|
+
) {
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const popupRect = popup.getBoundingClientRect();
|
|
1128
|
+
const anchorRect = menuAnchor.getBoundingClientRect();
|
|
1129
|
+
if (!popupRect.width || !popupRect.height) return false;
|
|
1130
|
+
const { vertical, horizontal, shorthand } = popup.parsePosition();
|
|
1131
|
+
const offset = popup.parseOffset();
|
|
1132
|
+
let { left, top } = popup.computeCoords(
|
|
1133
|
+
anchorRect,
|
|
1134
|
+
popupRect,
|
|
1135
|
+
vertical,
|
|
1136
|
+
horizontal,
|
|
1137
|
+
offset,
|
|
1138
|
+
shorthand,
|
|
1139
|
+
);
|
|
1140
|
+
const margins = this.#getViewportMargins();
|
|
1141
|
+
if (typeof popup.clampToViewport === "function") {
|
|
1142
|
+
({ left, top } = popup.clampToViewport(
|
|
1143
|
+
{ left, top },
|
|
1144
|
+
popupRect,
|
|
1145
|
+
margins,
|
|
1146
|
+
));
|
|
1147
|
+
}
|
|
1148
|
+
popup.style.setProperty("right", "auto", "important");
|
|
1149
|
+
popup.style.setProperty("bottom", "auto", "important");
|
|
1150
|
+
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
1151
|
+
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
#clampMenuToViewport() {
|
|
1156
|
+
const popup = this.#popup;
|
|
1157
|
+
if (!popup || typeof popup.clampToViewport !== "function") return;
|
|
1158
|
+
const popupRect = popup.getBoundingClientRect();
|
|
1159
|
+
if (!popupRect.width || !popupRect.height) return;
|
|
1160
|
+
const margins = this.#getViewportMargins();
|
|
1161
|
+
const { left, top } = popup.clampToViewport(
|
|
1162
|
+
{ left: popupRect.left, top: popupRect.top },
|
|
1163
|
+
popupRect,
|
|
1164
|
+
margins,
|
|
1165
|
+
);
|
|
1166
|
+
popup.style.setProperty("right", "auto", "important");
|
|
1167
|
+
popup.style.setProperty("bottom", "auto", "important");
|
|
1168
|
+
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
1169
|
+
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
1170
|
+
}
|
|
1171
|
+
|
|
998
1172
|
#positionPopupOverSelected() {
|
|
999
1173
|
// Content ResizeObserver / overflow scroll re-enter here; keep the
|
|
1000
1174
|
// open-time alignment unless the trigger or viewport actually changed.
|
|
1001
|
-
if (this.#shouldSkipFrozenPositionPass())
|
|
1175
|
+
if (this.#shouldSkipFrozenPositionPass()) {
|
|
1176
|
+
this.#clampMenuToViewport();
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1002
1179
|
|
|
1003
1180
|
const popup = this.#popup;
|
|
1004
1181
|
const label = this.#labelEl;
|
|
@@ -1016,30 +1193,46 @@ class FigSelect extends HTMLElement {
|
|
|
1016
1193
|
return;
|
|
1017
1194
|
}
|
|
1018
1195
|
|
|
1196
|
+
const menuAnchor = this.#usesMenuAnchorOverlayGeometry()
|
|
1197
|
+
? this.#resolveExplicitMenuAnchor()
|
|
1198
|
+
: null;
|
|
1199
|
+
if (menuAnchor) this.#syncPopupWidth();
|
|
1200
|
+
|
|
1019
1201
|
// Lay out with the default positioning first so option metrics are valid.
|
|
1020
1202
|
this.#originalPositionPopup?.();
|
|
1021
1203
|
|
|
1022
1204
|
const popupRect = popup.getBoundingClientRect();
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
if (
|
|
1026
|
-
!popupRect.width ||
|
|
1027
|
-
!popupRect.height ||
|
|
1028
|
-
!labelRect.width ||
|
|
1029
|
-
!optionTextRect
|
|
1030
|
-
) {
|
|
1205
|
+
const selectedOptionRect = selected.getBoundingClientRect();
|
|
1206
|
+
if (!popupRect.width || !popupRect.height || !selectedOptionRect.height) {
|
|
1031
1207
|
return;
|
|
1032
1208
|
}
|
|
1033
1209
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1210
|
+
let left;
|
|
1211
|
+
let top;
|
|
1212
|
+
let alignmentRect;
|
|
1213
|
+
if (menuAnchor) {
|
|
1214
|
+
const anchorRect = menuAnchor.getBoundingClientRect();
|
|
1215
|
+
alignmentRect = anchorRect;
|
|
1216
|
+
left = anchorRect.left + (anchorRect.width - popupRect.width) / 2;
|
|
1217
|
+
top =
|
|
1218
|
+
anchorRect.top +
|
|
1219
|
+
(anchorRect.height - selectedOptionRect.height) / 2 -
|
|
1220
|
+
(selectedOptionRect.top - popupRect.top);
|
|
1221
|
+
} else {
|
|
1222
|
+
const labelRect = this.#getTriggerAlignmentRect();
|
|
1223
|
+
const optionTextRect = this.#getOptionTextRect(selected);
|
|
1224
|
+
if (!labelRect?.width || !optionTextRect) return;
|
|
1225
|
+
alignmentRect = labelRect;
|
|
1226
|
+
const selectedOffsetX = optionTextRect.left - popupRect.left;
|
|
1227
|
+
const selectedOffsetY = optionTextRect.top - popupRect.top;
|
|
1228
|
+
const full = figEditorBooleanAttribute(this, "full");
|
|
1229
|
+
// [full]: pin menu to host width/edges. Otherwise overlay selected
|
|
1230
|
+
// option text on the trigger label (blend-mode style).
|
|
1231
|
+
left = full
|
|
1232
|
+
? this.getBoundingClientRect().left
|
|
1233
|
+
: labelRect.left - selectedOffsetX;
|
|
1234
|
+
top = labelRect.top - selectedOffsetY;
|
|
1235
|
+
}
|
|
1043
1236
|
|
|
1044
1237
|
// Keep the whole menu in-view when aligning over the selected option
|
|
1045
1238
|
// would otherwise push it past a viewport edge (corners / far sides).
|
|
@@ -1062,15 +1255,24 @@ class FigSelect extends HTMLElement {
|
|
|
1062
1255
|
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
1063
1256
|
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
1064
1257
|
|
|
1065
|
-
//
|
|
1258
|
+
// If the panel scrolls, keep the selected row centered over a menu anchor.
|
|
1259
|
+
// Standard selects retain text-to-trigger alignment.
|
|
1066
1260
|
const panel = this.#getPanel();
|
|
1067
|
-
const
|
|
1261
|
+
const alignedOptionRect = selected.getBoundingClientRect();
|
|
1262
|
+
const alignedTextRect = menuAnchor
|
|
1263
|
+
? null
|
|
1264
|
+
: this.#getOptionTextRect(selected);
|
|
1068
1265
|
if (
|
|
1069
|
-
alignedTextRect &&
|
|
1070
1266
|
panel &&
|
|
1071
1267
|
panel.scrollHeight > panel.clientHeight + 1
|
|
1072
1268
|
) {
|
|
1073
|
-
const deltaY =
|
|
1269
|
+
const deltaY = menuAnchor
|
|
1270
|
+
? alignedOptionRect.top +
|
|
1271
|
+
alignedOptionRect.height / 2 -
|
|
1272
|
+
(alignmentRect.top + alignmentRect.height / 2)
|
|
1273
|
+
: alignedTextRect
|
|
1274
|
+
? alignedTextRect.top - alignmentRect.top
|
|
1275
|
+
: 0;
|
|
1074
1276
|
if (Math.abs(deltaY) > 0.5) {
|
|
1075
1277
|
panel.scrollTop += deltaY;
|
|
1076
1278
|
}
|
|
@@ -1395,9 +1597,7 @@ class FigSelect extends HTMLElement {
|
|
|
1395
1597
|
e.preventDefault();
|
|
1396
1598
|
e.stopPropagation();
|
|
1397
1599
|
const nextOpen = !this.open;
|
|
1398
|
-
if (nextOpen
|
|
1399
|
-
this.#popup.anchor = this.#button;
|
|
1400
|
-
}
|
|
1600
|
+
if (nextOpen) this.#syncMenuAnchor();
|
|
1401
1601
|
this.open = nextOpen;
|
|
1402
1602
|
}
|
|
1403
1603
|
|
|
@@ -1448,7 +1648,7 @@ class FigSelect extends HTMLElement {
|
|
|
1448
1648
|
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")
|
|
1449
1649
|
) {
|
|
1450
1650
|
e.preventDefault();
|
|
1451
|
-
|
|
1651
|
+
this.#syncMenuAnchor();
|
|
1452
1652
|
this.open = true;
|
|
1453
1653
|
requestAnimationFrame(() => {
|
|
1454
1654
|
const options = this.#getOptions({ enabledOnly: true });
|
|
@@ -1554,21 +1754,66 @@ class FigSelect extends HTMLElement {
|
|
|
1554
1754
|
const hostWidth = Math.ceil(this.getBoundingClientRect().width);
|
|
1555
1755
|
const triggerWidth = Math.ceil(this.#button.getBoundingClientRect().width);
|
|
1556
1756
|
const anchorWidth = Math.max(hostWidth, triggerWidth, 96);
|
|
1757
|
+
const menuAnchor = this.#hasMenuAnchor()
|
|
1758
|
+
? this.#resolveExplicitMenuAnchor()
|
|
1759
|
+
: null;
|
|
1557
1760
|
|
|
1558
1761
|
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
1559
1762
|
// Menus size to their options while remaining at least as wide as the trigger.
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1763
|
+
const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
|
|
1764
|
+
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
|
1765
|
+
const rem =
|
|
1766
|
+
parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
1767
|
+
const maxWidth = Math.max(
|
|
1768
|
+
0,
|
|
1769
|
+
Math.min(Math.floor(20 * rem), Math.floor(viewportWidth - 16)),
|
|
1566
1770
|
);
|
|
1771
|
+
const maxHeight = Math.max(0, Math.floor(viewportHeight - 16));
|
|
1772
|
+
this.#popup.style.setProperty("box-sizing", "border-box", "important");
|
|
1773
|
+
this.#popup.style.setProperty("padding", "0", "important");
|
|
1774
|
+
this.#popup.style.setProperty("border", "0", "important");
|
|
1775
|
+
this.#popup.style.setProperty("margin", "0", "important");
|
|
1776
|
+
if (menuAnchor) {
|
|
1777
|
+
const requestedWidth = menuAnchor.getBoundingClientRect().width;
|
|
1778
|
+
// Match a normal anchor exactly. If that would make the menu narrower
|
|
1779
|
+
// than its usable trigger/content width, keep the wider menu. Default
|
|
1780
|
+
// overlay centers it; explicit position applies its requested alignment.
|
|
1781
|
+
this.#popup.style.setProperty("width", "max-content", "important");
|
|
1782
|
+
this.#popup.style.setProperty("min-width", "0", "important");
|
|
1783
|
+
this.#popup.style.setProperty("max-width", `${maxWidth}px`, "important");
|
|
1784
|
+
const intrinsicWidth = this.#popup.getBoundingClientRect().width;
|
|
1785
|
+
const minimumWidth = Math.min(
|
|
1786
|
+
maxWidth,
|
|
1787
|
+
Math.max(anchorWidth, intrinsicWidth),
|
|
1788
|
+
);
|
|
1789
|
+
const width = Math.max(requestedWidth, minimumWidth);
|
|
1790
|
+
this.#popup.style.setProperty("width", `${width}px`, "important");
|
|
1791
|
+
this.#popup.style.setProperty("min-width", `${width}px`, "important");
|
|
1792
|
+
this.#popup.style.setProperty("max-width", `${width}px`, "important");
|
|
1793
|
+
} else {
|
|
1794
|
+
this.#popup.style.setProperty(
|
|
1795
|
+
"width",
|
|
1796
|
+
"max-content",
|
|
1797
|
+
"important",
|
|
1798
|
+
);
|
|
1799
|
+
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
1800
|
+
this.#popup.style.setProperty("max-width", `${maxWidth}px`, "important");
|
|
1801
|
+
}
|
|
1802
|
+
this.#popup.style.setProperty("max-height", `${maxHeight}px`, "important");
|
|
1803
|
+
this.#popup.style.setProperty("height", "max-content", "important");
|
|
1804
|
+
this.#popup.style.setProperty("overflow", "hidden", "important");
|
|
1805
|
+
const used = this.#popup.getBoundingClientRect();
|
|
1806
|
+
if (used.height > maxHeight) {
|
|
1807
|
+
this.#popup.style.setProperty("height", `${maxHeight}px`, "important");
|
|
1808
|
+
}
|
|
1809
|
+
if (!menuAnchor && used.width > maxWidth) {
|
|
1810
|
+
this.#popup.style.setProperty("width", `${maxWidth}px`, "important");
|
|
1811
|
+
}
|
|
1567
1812
|
}
|
|
1568
1813
|
|
|
1569
1814
|
#openList() {
|
|
1570
1815
|
if (!this.#popup || figEditorBooleanAttribute(this, "disabled")) return;
|
|
1571
|
-
|
|
1816
|
+
this.#syncMenuAnchor();
|
|
1572
1817
|
this.#installPopupPositioning();
|
|
1573
1818
|
this.#freezeMenuPosition = false;
|
|
1574
1819
|
this.#frozenLabelRect = null;
|
|
@@ -1580,26 +1825,33 @@ class FigSelect extends HTMLElement {
|
|
|
1580
1825
|
this.#button?.setAttribute("aria-expanded", "true");
|
|
1581
1826
|
this.#focusedIndex = -1;
|
|
1582
1827
|
requestAnimationFrame(() => {
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1828
|
+
requestAnimationFrame(() => {
|
|
1829
|
+
this.#syncPopupWidth();
|
|
1830
|
+
this.#placeOpenMenu();
|
|
1831
|
+
this.#syncPopupWidth();
|
|
1832
|
+
this.#clampMenuToViewport();
|
|
1833
|
+
const panel = this.#getPanel();
|
|
1834
|
+
const options = this.#getEnabledOptions();
|
|
1835
|
+
const selectedIndex = options.findIndex(
|
|
1836
|
+
(opt) => this.#optionValue(opt) === this.value,
|
|
1837
|
+
);
|
|
1838
|
+
if (selectedIndex >= 0) {
|
|
1839
|
+
this.#focusOptionAt(selectedIndex);
|
|
1840
|
+
} else if (
|
|
1841
|
+
this.#button?.hasAttribute("data-focus-visible") ||
|
|
1842
|
+
this.#button?.matches?.(":focus-visible")
|
|
1843
|
+
) {
|
|
1844
|
+
this.#focusOptionAt(0);
|
|
1845
|
+
}
|
|
1846
|
+
panel?.syncOverflow?.();
|
|
1847
|
+
// Freeze overlay after layout settles so later positionPopup passes
|
|
1848
|
+
// don't undo scroll. Still clamp if content grows past the viewport.
|
|
1849
|
+
// Explicit position stays live so the popup can follow the anchor.
|
|
1850
|
+
if (!this.#usesExplicitPosition()) {
|
|
1851
|
+
this.#freezeMenuPosition = true;
|
|
1852
|
+
this.#rememberFrozenGeometry();
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1603
1855
|
});
|
|
1604
1856
|
}
|
|
1605
1857
|
|
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
|
|
package/package.json
CHANGED