@rogieking/figui3 8.9.10 → 8.9.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 +184 -0
- package/.cursor/skills/fig-editor/reference.md +46 -0
- package/.cursor/skills/fig-lab/SKILL.md +159 -0
- package/.cursor/skills/fig-lab/reference.md +67 -0
- package/.cursor/skills/figui3/SKILL.md +160 -188
- package/.cursor/skills/figui3/reference.md +91 -0
- package/.cursor/skills/propkit/SKILL.md +66 -185
- package/README.md +66 -1
- package/components.css +109 -30
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +1 -1
- package/dist/fig.css +1 -1
- package/dist/fig.js +162 -3
- package/fig-editor.css +11 -91
- package/fig-editor.js +5 -0
- package/fig.js +525 -122
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -204,6 +204,195 @@ function createFigOverflowButtons({
|
|
|
204
204
|
};
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/* Shared by light-DOM fig-select-options and shadow fig-menu .fig-menu-options.
|
|
208
|
+
Adopted on document and on fig-menu's shadow so hover/icon/overflow stay one source. */
|
|
209
|
+
const FIG_VERTICAL_OVERFLOW_CSS = `
|
|
210
|
+
fig-select-options,
|
|
211
|
+
.fig-menu-options {
|
|
212
|
+
--fig-vertical-overflow-size: var(--spacer-4);
|
|
213
|
+
box-sizing: border-box;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: column;
|
|
216
|
+
gap: 0;
|
|
217
|
+
width: 100%;
|
|
218
|
+
max-width: 100%;
|
|
219
|
+
max-height: calc(100vh - 1rem);
|
|
220
|
+
padding: 0;
|
|
221
|
+
overflow: hidden auto;
|
|
222
|
+
scrollbar-width: none;
|
|
223
|
+
}
|
|
224
|
+
fig-select-options::-webkit-scrollbar,
|
|
225
|
+
.fig-menu-options::-webkit-scrollbar {
|
|
226
|
+
display: none;
|
|
227
|
+
}
|
|
228
|
+
fig-select-options > :not(.fig-overflow),
|
|
229
|
+
.fig-menu-options > :not(.fig-overflow) {
|
|
230
|
+
flex-shrink: 0;
|
|
231
|
+
}
|
|
232
|
+
fig-select-options > :nth-child(1 of :not(.fig-overflow)),
|
|
233
|
+
.fig-menu-options > :nth-child(1 of :not(.fig-overflow)) {
|
|
234
|
+
margin-top: var(--spacer-2);
|
|
235
|
+
}
|
|
236
|
+
fig-select-options > :not(.fig-overflow):last-child,
|
|
237
|
+
.fig-menu-options > :not(.fig-overflow):last-child,
|
|
238
|
+
fig-select-options > :not(.fig-overflow):has(+ .fig-overflow-end),
|
|
239
|
+
.fig-menu-options > :not(.fig-overflow):has(+ .fig-overflow-end) {
|
|
240
|
+
margin-bottom: var(--spacer-2);
|
|
241
|
+
}
|
|
242
|
+
fig-select-options > .fig-overflow,
|
|
243
|
+
.fig-menu-options > .fig-overflow {
|
|
244
|
+
all: unset;
|
|
245
|
+
box-sizing: border-box;
|
|
246
|
+
position: sticky;
|
|
247
|
+
left: 0;
|
|
248
|
+
z-index: 3;
|
|
249
|
+
flex: 0 0 var(--fig-vertical-overflow-size);
|
|
250
|
+
display: flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
justify-content: center;
|
|
253
|
+
width: 100%;
|
|
254
|
+
height: var(--fig-vertical-overflow-size);
|
|
255
|
+
margin: 0;
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
opacity: 0;
|
|
258
|
+
pointer-events: none;
|
|
259
|
+
color: var(--figma-color-text-menu);
|
|
260
|
+
background: var(--figma-color-bg-menu) !important;
|
|
261
|
+
}
|
|
262
|
+
fig-select-options > .fig-overflow::before,
|
|
263
|
+
.fig-menu-options > .fig-overflow::before {
|
|
264
|
+
content: "";
|
|
265
|
+
position: absolute;
|
|
266
|
+
inset: 0;
|
|
267
|
+
z-index: -1;
|
|
268
|
+
border-radius: 0;
|
|
269
|
+
background: var(--figma-color-bg-menu) !important;
|
|
270
|
+
}
|
|
271
|
+
fig-select-options > .fig-overflow:hover,
|
|
272
|
+
.fig-menu-options > .fig-overflow:hover {
|
|
273
|
+
color: var(--figma-color-text-menu);
|
|
274
|
+
}
|
|
275
|
+
fig-select-options > .fig-overflow:hover::before,
|
|
276
|
+
.fig-menu-options > .fig-overflow:hover::before {
|
|
277
|
+
background: light-dark(
|
|
278
|
+
rgba(0, 0, 0, 0.06),
|
|
279
|
+
rgba(255, 255, 255, 0.08)
|
|
280
|
+
) !important;
|
|
281
|
+
}
|
|
282
|
+
fig-select-options > .fig-overflow .fig-overflow-chevron,
|
|
283
|
+
.fig-menu-options > .fig-overflow .fig-overflow-chevron {
|
|
284
|
+
position: relative;
|
|
285
|
+
}
|
|
286
|
+
fig-select-options > .fig-overflow:active .fig-overflow-chevron,
|
|
287
|
+
.fig-menu-options > .fig-overflow:active .fig-overflow-chevron {
|
|
288
|
+
transform: translateY(1px);
|
|
289
|
+
}
|
|
290
|
+
fig-select-options.overflow-start > .fig-overflow-start,
|
|
291
|
+
.fig-menu-options.overflow-start > .fig-overflow-start,
|
|
292
|
+
fig-select-options.overflow-end > .fig-overflow-end,
|
|
293
|
+
.fig-menu-options.overflow-end > .fig-overflow-end {
|
|
294
|
+
opacity: 1;
|
|
295
|
+
pointer-events: auto;
|
|
296
|
+
}
|
|
297
|
+
fig-select-options > .fig-overflow-start,
|
|
298
|
+
.fig-menu-options > .fig-overflow-start {
|
|
299
|
+
order: -1;
|
|
300
|
+
top: 0;
|
|
301
|
+
margin-block-end: calc(-1 * var(--fig-vertical-overflow-size));
|
|
302
|
+
}
|
|
303
|
+
fig-select-options > .fig-overflow-start::before,
|
|
304
|
+
.fig-menu-options > .fig-overflow-start::before {
|
|
305
|
+
border-radius: var(--radius-large) var(--radius-large) 0 0;
|
|
306
|
+
}
|
|
307
|
+
fig-select-options > .fig-overflow-start .fig-overflow-chevron,
|
|
308
|
+
.fig-menu-options > .fig-overflow-start .fig-overflow-chevron {
|
|
309
|
+
rotate: 180deg;
|
|
310
|
+
}
|
|
311
|
+
fig-select-options > .fig-overflow-end,
|
|
312
|
+
.fig-menu-options > .fig-overflow-end {
|
|
313
|
+
order: 1;
|
|
314
|
+
bottom: 0;
|
|
315
|
+
margin-block-start: calc(-1 * var(--fig-vertical-overflow-size));
|
|
316
|
+
}
|
|
317
|
+
fig-select-options > .fig-overflow-end::before,
|
|
318
|
+
.fig-menu-options > .fig-overflow-end::before {
|
|
319
|
+
border-radius: 0 0 var(--radius-large) var(--radius-large);
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
322
|
+
|
|
323
|
+
const FIG_SHADOW_ICON_CSS = `
|
|
324
|
+
fig-icon {
|
|
325
|
+
contain: strict;
|
|
326
|
+
--size: var(--spacer-4);
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
width: var(--size);
|
|
329
|
+
height: var(--size);
|
|
330
|
+
flex-shrink: 0;
|
|
331
|
+
background: currentColor;
|
|
332
|
+
mask-image: var(--icon);
|
|
333
|
+
mask-size: contain;
|
|
334
|
+
mask-repeat: no-repeat;
|
|
335
|
+
mask-position: center;
|
|
336
|
+
-webkit-mask-image: var(--icon);
|
|
337
|
+
-webkit-mask-size: contain;
|
|
338
|
+
-webkit-mask-repeat: no-repeat;
|
|
339
|
+
-webkit-mask-position: center;
|
|
340
|
+
}
|
|
341
|
+
fig-icon[size="small"] {
|
|
342
|
+
--size: var(--spacer-3);
|
|
343
|
+
}
|
|
344
|
+
`;
|
|
345
|
+
|
|
346
|
+
const figVerticalOverflowSheet =
|
|
347
|
+
typeof CSSStyleSheet === "function" &&
|
|
348
|
+
typeof CSSStyleSheet.prototype.replaceSync === "function"
|
|
349
|
+
? new CSSStyleSheet()
|
|
350
|
+
: null;
|
|
351
|
+
figVerticalOverflowSheet?.replaceSync(FIG_VERTICAL_OVERFLOW_CSS);
|
|
352
|
+
|
|
353
|
+
const figShadowIconSheet =
|
|
354
|
+
typeof CSSStyleSheet === "function" &&
|
|
355
|
+
typeof CSSStyleSheet.prototype.replaceSync === "function"
|
|
356
|
+
? new CSSStyleSheet()
|
|
357
|
+
: null;
|
|
358
|
+
figShadowIconSheet?.replaceSync(FIG_SHADOW_ICON_CSS);
|
|
359
|
+
|
|
360
|
+
function figAdoptConstructedSheet(root, sheet, cssText, marker) {
|
|
361
|
+
if (!root || !cssText) return;
|
|
362
|
+
if (sheet && "adoptedStyleSheets" in root) {
|
|
363
|
+
if ([...root.adoptedStyleSheets].includes(sheet)) return;
|
|
364
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const host = root === document ? document.documentElement : root;
|
|
368
|
+
if (host.querySelector?.(`style[data-fig-${marker}]`)) return;
|
|
369
|
+
const style = document.createElement("style");
|
|
370
|
+
style.setAttribute(`data-fig-${marker}`, "");
|
|
371
|
+
style.textContent = cssText;
|
|
372
|
+
host.prepend(style);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function figAdoptVerticalOverflowStyles(root, options = {}) {
|
|
376
|
+
figAdoptConstructedSheet(
|
|
377
|
+
root,
|
|
378
|
+
figVerticalOverflowSheet,
|
|
379
|
+
FIG_VERTICAL_OVERFLOW_CSS,
|
|
380
|
+
"vertical-overflow",
|
|
381
|
+
);
|
|
382
|
+
if (options.icons) {
|
|
383
|
+
figAdoptConstructedSheet(
|
|
384
|
+
root,
|
|
385
|
+
figShadowIconSheet,
|
|
386
|
+
FIG_SHADOW_ICON_CSS,
|
|
387
|
+
"shadow-icon",
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (typeof document !== "undefined") {
|
|
393
|
+
figAdoptVerticalOverflowStyles(document);
|
|
394
|
+
}
|
|
395
|
+
|
|
207
396
|
function figSyncOverflowState(host, scrollEl, axis = "x", threshold = 2) {
|
|
208
397
|
if (!host || !scrollEl) return false;
|
|
209
398
|
const isHorizontal = axis === "x";
|
|
@@ -673,6 +862,7 @@ figDefineElement("fig-button", FigButton);
|
|
|
673
862
|
* A custom dropdown/select element.
|
|
674
863
|
* @attr {string} type - The dropdown type: "select" (default) or "dropdown"
|
|
675
864
|
* @attr {string} value - The currently selected value
|
|
865
|
+
* @attr {string} variant - Visual style. Use `ghost` for a borderless control.
|
|
676
866
|
*/
|
|
677
867
|
class FigDropdown extends HTMLElement {
|
|
678
868
|
#label = "Menu";
|
|
@@ -871,7 +1061,7 @@ class FigDropdown extends HTMLElement {
|
|
|
871
1061
|
this.setAttribute("value", value);
|
|
872
1062
|
}
|
|
873
1063
|
static get observedAttributes() {
|
|
874
|
-
return ["value", "type", "label", "disabled"];
|
|
1064
|
+
return ["value", "type", "label", "disabled", "variant"];
|
|
875
1065
|
}
|
|
876
1066
|
#syncDisabled() {
|
|
877
1067
|
const disabled =
|
|
@@ -1663,6 +1853,27 @@ class FigTruncate extends HTMLElement {
|
|
|
1663
1853
|
}
|
|
1664
1854
|
figDefineElement("fig-truncate", FigTruncate);
|
|
1665
1855
|
|
|
1856
|
+
function figEnsureAutoDialogHeader(host) {
|
|
1857
|
+
if (host.querySelector("fig-header[dialog-header]")) return;
|
|
1858
|
+
const header = document.createElement("fig-header");
|
|
1859
|
+
header.setAttribute("dialog-header", "");
|
|
1860
|
+
header.setAttribute("data-auto", "");
|
|
1861
|
+
const h3 = document.createElement("h3");
|
|
1862
|
+
h3.textContent = host.getAttribute("title") || "Dialog";
|
|
1863
|
+
const tooltip = document.createElement("fig-tooltip");
|
|
1864
|
+
tooltip.setAttribute("text", "Close");
|
|
1865
|
+
const btn = document.createElement("fig-button");
|
|
1866
|
+
btn.setAttribute("variant", "ghost");
|
|
1867
|
+
btn.setAttribute("icon", "");
|
|
1868
|
+
btn.setAttribute("aria-label", "Close dialog");
|
|
1869
|
+
btn.setAttribute("close-dialog", "");
|
|
1870
|
+
btn.appendChild(createFigIcon("close"));
|
|
1871
|
+
tooltip.appendChild(btn);
|
|
1872
|
+
header.appendChild(h3);
|
|
1873
|
+
header.appendChild(tooltip);
|
|
1874
|
+
host.prepend(header);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1666
1877
|
/* Dialog */
|
|
1667
1878
|
/**
|
|
1668
1879
|
* A custom dialog element for modal and non-modal dialogs.
|
|
@@ -1972,24 +2183,7 @@ class FigDialog extends HTMLDialogElement {
|
|
|
1972
2183
|
}
|
|
1973
2184
|
|
|
1974
2185
|
_ensureHeader() {
|
|
1975
|
-
|
|
1976
|
-
const header = document.createElement("fig-header");
|
|
1977
|
-
header.setAttribute("dialog-header", "");
|
|
1978
|
-
header.setAttribute("data-auto", "");
|
|
1979
|
-
const h3 = document.createElement("h3");
|
|
1980
|
-
h3.textContent = this.getAttribute("title") || "Dialog";
|
|
1981
|
-
const tooltip = document.createElement("fig-tooltip");
|
|
1982
|
-
tooltip.setAttribute("text", "Close");
|
|
1983
|
-
const btn = document.createElement("fig-button");
|
|
1984
|
-
btn.setAttribute("variant", "ghost");
|
|
1985
|
-
btn.setAttribute("icon", "");
|
|
1986
|
-
btn.setAttribute("aria-label", "Close dialog");
|
|
1987
|
-
btn.setAttribute("close-dialog", "");
|
|
1988
|
-
btn.appendChild(createFigIcon("close"));
|
|
1989
|
-
tooltip.appendChild(btn);
|
|
1990
|
-
header.appendChild(h3);
|
|
1991
|
-
header.appendChild(tooltip);
|
|
1992
|
-
this.prepend(header);
|
|
2186
|
+
figEnsureAutoDialogHeader(this);
|
|
1993
2187
|
}
|
|
1994
2188
|
|
|
1995
2189
|
_addCloseListeners() {
|
|
@@ -2587,6 +2781,7 @@ figDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
|
|
|
2587
2781
|
* @attr {string} offset - Horizontal and vertical offset as "x y" (default: "0 0").
|
|
2588
2782
|
* @attr {string} variant - Visual variant. Use variant="popover" to show an anchor beak.
|
|
2589
2783
|
* @attr {string} theme - Visual theme: "light", "dark", or "menu".
|
|
2784
|
+
* @attr {string} title - Title text for the auto-generated header (same as fig-dialog).
|
|
2590
2785
|
* @attr {boolean|string} open - Open when present and not "false".
|
|
2591
2786
|
*/
|
|
2592
2787
|
class FigPopup extends HTMLDialogElement {
|
|
@@ -2634,6 +2829,9 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2634
2829
|
this._boundPointerUp = this.handlePointerUp.bind(this);
|
|
2635
2830
|
this._boundDocumentKeydown = this.handleDocumentKeydown.bind(this);
|
|
2636
2831
|
this._boundNativeClose = this.handleNativeClose.bind(this);
|
|
2832
|
+
this._boundAutoClose = () => {
|
|
2833
|
+
this.open = false;
|
|
2834
|
+
};
|
|
2637
2835
|
}
|
|
2638
2836
|
|
|
2639
2837
|
ensureInitialized() {
|
|
@@ -2693,6 +2891,11 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2693
2891
|
if (typeof this._boundNativeClose !== "function") {
|
|
2694
2892
|
this._boundNativeClose = this.handleNativeClose.bind(this);
|
|
2695
2893
|
}
|
|
2894
|
+
if (typeof this._boundAutoClose !== "function") {
|
|
2895
|
+
this._boundAutoClose = () => {
|
|
2896
|
+
this.open = false;
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2696
2899
|
}
|
|
2697
2900
|
|
|
2698
2901
|
static get observedAttributes() {
|
|
@@ -2707,6 +2910,7 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2707
2910
|
"handle",
|
|
2708
2911
|
"autoresize",
|
|
2709
2912
|
"viewport-margin",
|
|
2913
|
+
"title",
|
|
2710
2914
|
];
|
|
2711
2915
|
}
|
|
2712
2916
|
|
|
@@ -2777,6 +2981,8 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2777
2981
|
this.drag =
|
|
2778
2982
|
this.hasAttribute("drag") && this.getAttribute("drag") !== "false";
|
|
2779
2983
|
|
|
2984
|
+
this._ensureHeader();
|
|
2985
|
+
|
|
2780
2986
|
this.removeEventListener("close", this._boundNativeClose);
|
|
2781
2987
|
this.addEventListener("close", this._boundNativeClose);
|
|
2782
2988
|
|
|
@@ -2835,6 +3041,13 @@ class FigPopup extends HTMLDialogElement {
|
|
|
2835
3041
|
return;
|
|
2836
3042
|
}
|
|
2837
3043
|
|
|
3044
|
+
if (name === "title") {
|
|
3045
|
+
this._ensureHeader();
|
|
3046
|
+
const autoHeader = this.querySelector("fig-header[data-auto] h3");
|
|
3047
|
+
if (autoHeader) autoHeader.textContent = newValue || "Dialog";
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
|
|
2838
3051
|
if (this.open) {
|
|
2839
3052
|
this.queueReposition();
|
|
2840
3053
|
this.setupObservers();
|
|
@@ -3109,6 +3322,20 @@ class FigPopup extends HTMLDialogElement {
|
|
|
3109
3322
|
this._lastAnchorRect = null;
|
|
3110
3323
|
}
|
|
3111
3324
|
|
|
3325
|
+
_ensureHeader() {
|
|
3326
|
+
if (
|
|
3327
|
+
!this.hasAttribute("title") ||
|
|
3328
|
+
this.getAttribute("variant") === "tooltip"
|
|
3329
|
+
) {
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
figEnsureAutoDialogHeader(this);
|
|
3333
|
+
this.querySelectorAll("fig-button[close-dialog]").forEach((button) => {
|
|
3334
|
+
button.removeEventListener("click", this._boundAutoClose);
|
|
3335
|
+
button.addEventListener("click", this._boundAutoClose);
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3112
3339
|
handleOutsidePointerDown(event) {
|
|
3113
3340
|
if (
|
|
3114
3341
|
!this.open ||
|
|
@@ -18728,10 +18955,15 @@ figDefineElement("fig-menu-item", FigMenuItem);
|
|
|
18728
18955
|
* @attr {string} label - Optional group label shown in secondary text under the line.
|
|
18729
18956
|
* @attr {string} direction - Orientation: "vertical" for a tall rule; default is horizontal.
|
|
18730
18957
|
* @attr {boolean} borderless - Hides the separator line when present or "true".
|
|
18958
|
+
* @attr {boolean} sticky - Stick to the top of the nearest scrollport while scrolling.
|
|
18731
18959
|
*/
|
|
18732
18960
|
class FigSeparator extends HTMLElement {
|
|
18961
|
+
#scrollRoot = null;
|
|
18962
|
+
#resizeObserver = null;
|
|
18963
|
+
#boundSyncStuck = this.#syncStuck.bind(this);
|
|
18964
|
+
|
|
18733
18965
|
static get observedAttributes() {
|
|
18734
|
-
return ["label", "direction"];
|
|
18966
|
+
return ["label", "direction", "sticky"];
|
|
18735
18967
|
}
|
|
18736
18968
|
|
|
18737
18969
|
get label() {
|
|
@@ -18745,11 +18977,17 @@ class FigSeparator extends HTMLElement {
|
|
|
18745
18977
|
|
|
18746
18978
|
connectedCallback() {
|
|
18747
18979
|
this.#sync();
|
|
18980
|
+
this.#bindStuck();
|
|
18981
|
+
}
|
|
18982
|
+
|
|
18983
|
+
disconnectedCallback() {
|
|
18984
|
+
this.#unbindStuck();
|
|
18748
18985
|
}
|
|
18749
18986
|
|
|
18750
18987
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
18751
18988
|
if (oldValue === newValue) return;
|
|
18752
18989
|
this.#sync();
|
|
18990
|
+
if (name === "sticky") this.#bindStuck();
|
|
18753
18991
|
}
|
|
18754
18992
|
|
|
18755
18993
|
#sync() {
|
|
@@ -18766,36 +19004,106 @@ class FigSeparator extends HTMLElement {
|
|
|
18766
19004
|
if (label) this.setAttribute("aria-label", label);
|
|
18767
19005
|
else this.removeAttribute("aria-label");
|
|
18768
19006
|
}
|
|
19007
|
+
|
|
19008
|
+
#isSticky() {
|
|
19009
|
+
return this.hasAttribute("sticky") && this.getAttribute("sticky") !== "false";
|
|
19010
|
+
}
|
|
19011
|
+
|
|
19012
|
+
#bindStuck() {
|
|
19013
|
+
this.#unbindStuck();
|
|
19014
|
+
if (!this.isConnected || !this.#isSticky()) {
|
|
19015
|
+
this.removeAttribute("stuck");
|
|
19016
|
+
return;
|
|
19017
|
+
}
|
|
19018
|
+
const root = figNearestScrollRoot(this);
|
|
19019
|
+
this.#scrollRoot = root;
|
|
19020
|
+
root?.addEventListener("scroll", this.#boundSyncStuck, { passive: true });
|
|
19021
|
+
this.#resizeObserver = new ResizeObserver(this.#boundSyncStuck);
|
|
19022
|
+
this.#resizeObserver.observe(this);
|
|
19023
|
+
if (root instanceof Element) this.#resizeObserver.observe(root);
|
|
19024
|
+
requestAnimationFrame(this.#boundSyncStuck);
|
|
19025
|
+
}
|
|
19026
|
+
|
|
19027
|
+
#unbindStuck() {
|
|
19028
|
+
this.#scrollRoot?.removeEventListener?.("scroll", this.#boundSyncStuck);
|
|
19029
|
+
this.#scrollRoot = null;
|
|
19030
|
+
this.#resizeObserver?.disconnect();
|
|
19031
|
+
this.#resizeObserver = null;
|
|
19032
|
+
}
|
|
19033
|
+
|
|
19034
|
+
#syncStuck() {
|
|
19035
|
+
if (!this.#isSticky() || !this.#scrollRoot) {
|
|
19036
|
+
this.removeAttribute("stuck");
|
|
19037
|
+
return;
|
|
19038
|
+
}
|
|
19039
|
+
const stickyTop = parseFloat(getComputedStyle(this).top) || 0;
|
|
19040
|
+
const root = this.#scrollRoot;
|
|
19041
|
+
const rootRect =
|
|
19042
|
+
root === document.scrollingElement || root === document.documentElement
|
|
19043
|
+
? { top: 0 }
|
|
19044
|
+
: root.getBoundingClientRect();
|
|
19045
|
+
const paddingTop =
|
|
19046
|
+
root instanceof Element ? parseFloat(getComputedStyle(root).paddingTop) || 0 : 0;
|
|
19047
|
+
const line = rootRect.top + paddingTop + stickyTop;
|
|
19048
|
+
this.toggleAttribute("stuck", this.getBoundingClientRect().top <= line + 1);
|
|
19049
|
+
}
|
|
18769
19050
|
}
|
|
18770
19051
|
figDefineElement("fig-separator", FigSeparator);
|
|
18771
19052
|
|
|
19053
|
+
function figNearestScrollRoot(el) {
|
|
19054
|
+
let node = el.parentElement;
|
|
19055
|
+
while (node) {
|
|
19056
|
+
const style = getComputedStyle(node);
|
|
19057
|
+
const y = style.overflowY || style.overflow;
|
|
19058
|
+
if (y && y !== "visible" && y !== "clip") return node;
|
|
19059
|
+
node = node.parentElement;
|
|
19060
|
+
}
|
|
19061
|
+
return document.scrollingElement;
|
|
19062
|
+
}
|
|
19063
|
+
|
|
18772
19064
|
// Backwards-compatible alias. Custom element constructors cannot be registered
|
|
18773
19065
|
// under multiple tag names, so the legacy tag uses an otherwise empty subclass.
|
|
18774
19066
|
class FigMenuSeparator extends FigSeparator {}
|
|
18775
19067
|
figDefineElement("fig-menu-separator", FigMenuSeparator);
|
|
18776
19068
|
|
|
18777
|
-
const FIG_MENU_CHILD_SELECTOR =
|
|
18778
|
-
":scope > fig-menu-item, :scope > fig-separator, :scope > fig-menu-separator";
|
|
18779
19069
|
const FIG_MENU_CHILD_TAGS = new Set([
|
|
18780
19070
|
"FIG-MENU-ITEM",
|
|
18781
19071
|
"FIG-SEPARATOR",
|
|
18782
19072
|
"FIG-MENU-SEPARATOR",
|
|
18783
19073
|
]);
|
|
19074
|
+
const FIG_MENU_TRIGGER_SLOT = "trigger";
|
|
19075
|
+
|
|
19076
|
+
function figIsMenuChild(node) {
|
|
19077
|
+
return node?.nodeType === 1 && FIG_MENU_CHILD_TAGS.has(node.tagName);
|
|
19078
|
+
}
|
|
19079
|
+
|
|
19080
|
+
function figMenuTriggerHost(menu, trigger) {
|
|
19081
|
+
if (!menu || !trigger) return null;
|
|
19082
|
+
let host = trigger;
|
|
19083
|
+
while (host.parentElement && host.parentElement !== menu) {
|
|
19084
|
+
host = host.parentElement;
|
|
19085
|
+
}
|
|
19086
|
+
return host.parentElement === menu ? host : null;
|
|
19087
|
+
}
|
|
18784
19088
|
|
|
18785
19089
|
class FigMenu extends HTMLElement {
|
|
18786
19090
|
#popup = null;
|
|
18787
19091
|
#panel = null;
|
|
19092
|
+
#itemSlot = null;
|
|
18788
19093
|
#trigger = null;
|
|
19094
|
+
#triggerHost = null;
|
|
18789
19095
|
#virtualAnchor = null;
|
|
18790
19096
|
#observer = null;
|
|
18791
19097
|
#resizeObserver = null;
|
|
18792
19098
|
#navStart = null;
|
|
18793
19099
|
#navEnd = null;
|
|
19100
|
+
#initialized = false;
|
|
18794
19101
|
#boundTriggerClick;
|
|
18795
19102
|
#boundTriggerContextMenu;
|
|
18796
|
-
#
|
|
19103
|
+
#boundHostClick;
|
|
18797
19104
|
#boundMenuKeydown;
|
|
18798
19105
|
#boundPopupClose;
|
|
19106
|
+
#boundSlotChange;
|
|
18799
19107
|
#boundSyncOverflow = this.#syncOverflow.bind(this);
|
|
18800
19108
|
#focusedIndex = -1;
|
|
18801
19109
|
|
|
@@ -18807,9 +19115,11 @@ class FigMenu extends HTMLElement {
|
|
|
18807
19115
|
super();
|
|
18808
19116
|
this.#boundTriggerClick = this.#handleTriggerClick.bind(this);
|
|
18809
19117
|
this.#boundTriggerContextMenu = this.#handleTriggerContextMenu.bind(this);
|
|
18810
|
-
this.#
|
|
19118
|
+
this.#boundHostClick = this.#handleHostClick.bind(this);
|
|
18811
19119
|
this.#boundMenuKeydown = this.#handleMenuKeydown.bind(this);
|
|
18812
19120
|
this.#boundPopupClose = this.#handlePopupClose.bind(this);
|
|
19121
|
+
this.#boundSlotChange = this.#syncOverflow.bind(this);
|
|
19122
|
+
this.#initialize();
|
|
18813
19123
|
}
|
|
18814
19124
|
|
|
18815
19125
|
get value() {
|
|
@@ -18835,8 +19145,6 @@ class FigMenu extends HTMLElement {
|
|
|
18835
19145
|
|
|
18836
19146
|
connectedCallback() {
|
|
18837
19147
|
this.#detectTrigger();
|
|
18838
|
-
this.#createPopup();
|
|
18839
|
-
this.#moveItemsToPopup();
|
|
18840
19148
|
this.#setupListeners();
|
|
18841
19149
|
this.#setupOverflow();
|
|
18842
19150
|
this.#setupObserver();
|
|
@@ -18855,16 +19163,6 @@ class FigMenu extends HTMLElement {
|
|
|
18855
19163
|
this.#observer.disconnect();
|
|
18856
19164
|
this.#observer = null;
|
|
18857
19165
|
}
|
|
18858
|
-
if (this.#popup) {
|
|
18859
|
-
this.#popup.removeEventListener("close", this.#boundPopupClose);
|
|
18860
|
-
const items = Array.from(
|
|
18861
|
-
this.#panel?.querySelectorAll(FIG_MENU_CHILD_SELECTOR) ?? [],
|
|
18862
|
-
);
|
|
18863
|
-
for (const item of items) this.insertBefore(item, this.#popup);
|
|
18864
|
-
this.#popup.remove();
|
|
18865
|
-
this.#popup = null;
|
|
18866
|
-
this.#panel = null;
|
|
18867
|
-
}
|
|
18868
19166
|
}
|
|
18869
19167
|
|
|
18870
19168
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
@@ -18897,112 +19195,164 @@ class FigMenu extends HTMLElement {
|
|
|
18897
19195
|
}
|
|
18898
19196
|
}
|
|
18899
19197
|
|
|
18900
|
-
#
|
|
18901
|
-
this.#
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
|
|
19198
|
+
#initialize() {
|
|
19199
|
+
if (this.#initialized) return;
|
|
19200
|
+
this.#initialized = true;
|
|
19201
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
19202
|
+
figAdoptVerticalOverflowStyles(shadow, { icons: true });
|
|
19203
|
+
const style = document.createElement("style");
|
|
19204
|
+
style.textContent = `
|
|
19205
|
+
:host {
|
|
19206
|
+
display: contents;
|
|
19207
|
+
}
|
|
19208
|
+
/* Listbox chrome comes from document fig-menu::part(listbox).
|
|
19209
|
+
Stay in this shadow so item slots keep working — unlike tooltips,
|
|
19210
|
+
we cannot portal this popup to the overlay root.
|
|
19211
|
+
Overflow/hover/icon chrome lives in figVerticalOverflowSheet. */
|
|
19212
|
+
dialog[is="fig-popup"] {
|
|
19213
|
+
flex-direction: column;
|
|
19214
|
+
overflow: hidden;
|
|
19215
|
+
padding: 0;
|
|
19216
|
+
}
|
|
19217
|
+
dialog[is="fig-popup"][open] {
|
|
19218
|
+
display: flex;
|
|
19219
|
+
}
|
|
19220
|
+
.fig-menu-items {
|
|
19221
|
+
display: flex;
|
|
19222
|
+
flex-direction: column;
|
|
19223
|
+
flex: 0 0 auto;
|
|
19224
|
+
}
|
|
19225
|
+
.fig-menu-items > ::slotted(fig-menu-item),
|
|
19226
|
+
.fig-menu-items > ::slotted(fig-separator),
|
|
19227
|
+
.fig-menu-items > ::slotted(fig-menu-separator) {
|
|
19228
|
+
flex-shrink: 0;
|
|
19229
|
+
}
|
|
19230
|
+
`;
|
|
19231
|
+
|
|
19232
|
+
const triggerSlot = document.createElement("slot");
|
|
19233
|
+
triggerSlot.setAttribute("name", FIG_MENU_TRIGGER_SLOT);
|
|
19234
|
+
|
|
19235
|
+
const popup = document.createElement("dialog", { is: "fig-popup" });
|
|
19236
|
+
popup.setAttribute("is", "fig-popup");
|
|
19237
|
+
popup.classList.add("fig-menu-popup");
|
|
19238
|
+
popup.setAttribute("part", "listbox");
|
|
19239
|
+
popup.setAttribute("theme", "menu");
|
|
19240
|
+
popup.setAttribute("role", "menu");
|
|
19241
|
+
popup.id = figUniqueId();
|
|
18922
19242
|
if ("popover" in HTMLElement.prototype) {
|
|
18923
|
-
|
|
19243
|
+
popup.setAttribute("popover", "manual");
|
|
18924
19244
|
}
|
|
18925
19245
|
|
|
19246
|
+
const panel = document.createElement("div");
|
|
19247
|
+
panel.className = "fig-menu-options";
|
|
19248
|
+
panel.setAttribute("part", "options");
|
|
19249
|
+
panel.setAttribute("role", "presentation");
|
|
19250
|
+
|
|
19251
|
+
const items = document.createElement("div");
|
|
19252
|
+
items.className = "fig-menu-items";
|
|
19253
|
+
const itemSlot = document.createElement("slot");
|
|
19254
|
+
items.appendChild(itemSlot);
|
|
19255
|
+
panel.appendChild(items);
|
|
19256
|
+
popup.appendChild(panel);
|
|
19257
|
+
|
|
19258
|
+
shadow.append(style, triggerSlot, popup);
|
|
19259
|
+
|
|
19260
|
+
this.#popup = popup;
|
|
19261
|
+
this.#panel = panel;
|
|
19262
|
+
this.#itemSlot = itemSlot;
|
|
19263
|
+
this.#syncPopupAttrs();
|
|
19264
|
+
popup.addEventListener("close", this.#boundPopupClose);
|
|
19265
|
+
itemSlot.addEventListener("slotchange", this.#boundSlotChange);
|
|
19266
|
+
}
|
|
19267
|
+
|
|
19268
|
+
#syncPopupAttrs() {
|
|
19269
|
+
if (!this.#popup) return;
|
|
18926
19270
|
const position = this.getAttribute("position") || "bottom left";
|
|
18927
19271
|
this.#popup.setAttribute("position", position);
|
|
18928
|
-
|
|
18929
19272
|
const offset = this.getAttribute("offset");
|
|
18930
19273
|
if (offset) this.#popup.setAttribute("offset", offset);
|
|
18931
|
-
|
|
19274
|
+
else this.#popup.removeAttribute("offset");
|
|
18932
19275
|
const closedby = this.getAttribute("closedby");
|
|
18933
19276
|
if (closedby) this.#popup.setAttribute("closedby", closedby);
|
|
19277
|
+
else this.#popup.removeAttribute("closedby");
|
|
19278
|
+
}
|
|
18934
19279
|
|
|
18935
|
-
|
|
18936
|
-
|
|
19280
|
+
#detectTrigger() {
|
|
19281
|
+
this.#trigger =
|
|
19282
|
+
this.querySelector("[fig-menu-trigger]") ||
|
|
19283
|
+
this.querySelector(
|
|
19284
|
+
`:scope > :not(fig-menu-item):not(fig-separator):not(fig-menu-separator):not([slot="${FIG_MENU_TRIGGER_SLOT}"])`,
|
|
19285
|
+
) ||
|
|
19286
|
+
this.querySelector(`[slot="${FIG_MENU_TRIGGER_SLOT}"]`);
|
|
19287
|
+
this.#assignTriggerSlot();
|
|
19288
|
+
}
|
|
19289
|
+
|
|
19290
|
+
#assignTriggerSlot() {
|
|
19291
|
+
const host = figMenuTriggerHost(this, this.#trigger);
|
|
19292
|
+
this.#triggerHost = host;
|
|
19293
|
+
if (!host) return;
|
|
19294
|
+
if (host.getAttribute("slot") !== FIG_MENU_TRIGGER_SLOT) {
|
|
19295
|
+
host.setAttribute("slot", FIG_MENU_TRIGGER_SLOT);
|
|
18937
19296
|
}
|
|
19297
|
+
}
|
|
18938
19298
|
|
|
18939
|
-
|
|
18940
|
-
this
|
|
18941
|
-
this
|
|
18942
|
-
this.#popup.appendChild(this.#panel);
|
|
18943
|
-
this.#popup.addEventListener("close", this.#boundPopupClose);
|
|
18944
|
-
this.appendChild(this.#popup);
|
|
19299
|
+
#usesContextMenuTrigger() {
|
|
19300
|
+
const trigger = (this.getAttribute("trigger") || "").toLowerCase();
|
|
19301
|
+
return trigger === "contextmenu" || this.hasAttribute("contextmenu");
|
|
18945
19302
|
}
|
|
18946
19303
|
|
|
18947
|
-
#
|
|
18948
|
-
if (!this.#
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
19304
|
+
#bindTrigger() {
|
|
19305
|
+
if (!this.#trigger || !this.#popup) return;
|
|
19306
|
+
this.#trigger.addEventListener("click", this.#boundTriggerClick);
|
|
19307
|
+
this.#trigger.addEventListener("contextmenu", this.#boundTriggerContextMenu);
|
|
19308
|
+
this.#trigger.setAttribute("aria-haspopup", "menu");
|
|
19309
|
+
this.#trigger.setAttribute("aria-expanded", this.open ? "true" : "false");
|
|
19310
|
+
this.#trigger.setAttribute("aria-controls", this.#popup.id);
|
|
19311
|
+
this.#popup.anchor = this.#trigger;
|
|
19312
|
+
}
|
|
19313
|
+
|
|
19314
|
+
#unbindTrigger() {
|
|
19315
|
+
if (!this.#trigger) return;
|
|
19316
|
+
this.#trigger.removeEventListener("click", this.#boundTriggerClick);
|
|
19317
|
+
this.#trigger.removeEventListener("contextmenu", this.#boundTriggerContextMenu);
|
|
18953
19318
|
}
|
|
18954
19319
|
|
|
18955
19320
|
#setupListeners() {
|
|
18956
19321
|
this.addEventListener("keydown", this.#boundMenuKeydown);
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
this.#trigger.setAttribute("aria-haspopup", "menu");
|
|
18961
|
-
this.#trigger.setAttribute("aria-expanded", "false");
|
|
18962
|
-
this.#trigger.setAttribute("aria-controls", this.#popup.getAttribute("id"));
|
|
18963
|
-
}
|
|
18964
|
-
if (this.#popup) {
|
|
18965
|
-
this.#popup.addEventListener("click", this.#boundPopupClick);
|
|
18966
|
-
this.#popup.addEventListener("keydown", this.#boundMenuKeydown);
|
|
18967
|
-
}
|
|
19322
|
+
this.addEventListener("click", this.#boundHostClick);
|
|
19323
|
+
this.#bindTrigger();
|
|
19324
|
+
this.#popup?.addEventListener("keydown", this.#boundMenuKeydown);
|
|
18968
19325
|
}
|
|
18969
19326
|
|
|
18970
19327
|
#teardownListeners() {
|
|
18971
19328
|
this.removeEventListener("keydown", this.#boundMenuKeydown);
|
|
18972
|
-
|
|
18973
|
-
|
|
18974
|
-
|
|
18975
|
-
}
|
|
18976
|
-
if (this.#popup) {
|
|
18977
|
-
this.#popup.removeEventListener("click", this.#boundPopupClick);
|
|
18978
|
-
this.#popup.removeEventListener("keydown", this.#boundMenuKeydown);
|
|
18979
|
-
}
|
|
19329
|
+
this.removeEventListener("click", this.#boundHostClick);
|
|
19330
|
+
this.#unbindTrigger();
|
|
19331
|
+
this.#popup?.removeEventListener("keydown", this.#boundMenuKeydown);
|
|
18980
19332
|
}
|
|
18981
19333
|
|
|
18982
19334
|
#setupObserver() {
|
|
19335
|
+
if (this.#observer) return;
|
|
18983
19336
|
this.#observer = new MutationObserver((mutations) => {
|
|
19337
|
+
let triggerChanged = false;
|
|
18984
19338
|
for (const mutation of mutations) {
|
|
18985
19339
|
for (const node of mutation.addedNodes) {
|
|
18986
|
-
if (node.nodeType !== 1
|
|
18987
|
-
if (
|
|
18988
|
-
|
|
18989
|
-
|
|
18990
|
-
|
|
18991
|
-
|
|
18992
|
-
|
|
18993
|
-
this.#detectTrigger();
|
|
18994
|
-
if (this.#trigger) {
|
|
18995
|
-
this.#trigger.addEventListener("click", this.#boundTriggerClick);
|
|
18996
|
-
this.#trigger.addEventListener("contextmenu", this.#boundTriggerContextMenu);
|
|
18997
|
-
this.#trigger.setAttribute("aria-haspopup", "menu");
|
|
18998
|
-
this.#trigger.setAttribute("aria-expanded", "false");
|
|
18999
|
-
this.#trigger.setAttribute("aria-controls", this.#popup.getAttribute("id"));
|
|
19000
|
-
this.#popup.anchor = this.#trigger;
|
|
19001
|
-
this.#syncDisabled();
|
|
19002
|
-
}
|
|
19340
|
+
if (node.nodeType !== 1) continue;
|
|
19341
|
+
if (figIsMenuChild(node)) continue;
|
|
19342
|
+
if (node.parentElement === this) triggerChanged = true;
|
|
19343
|
+
}
|
|
19344
|
+
for (const node of mutation.removedNodes) {
|
|
19345
|
+
if (node === this.#trigger || node === this.#triggerHost) {
|
|
19346
|
+
triggerChanged = true;
|
|
19003
19347
|
}
|
|
19004
19348
|
}
|
|
19005
19349
|
}
|
|
19350
|
+
if (triggerChanged || !this.#trigger) {
|
|
19351
|
+
this.#unbindTrigger();
|
|
19352
|
+
this.#detectTrigger();
|
|
19353
|
+
this.#bindTrigger();
|
|
19354
|
+
this.#syncDisabled();
|
|
19355
|
+
}
|
|
19006
19356
|
this.#syncOverflow();
|
|
19007
19357
|
});
|
|
19008
19358
|
this.#observer.observe(this, { childList: true });
|
|
@@ -19027,8 +19377,25 @@ class FigMenu extends HTMLElement {
|
|
|
19027
19377
|
this.#removeNavButtons();
|
|
19028
19378
|
}
|
|
19029
19379
|
|
|
19380
|
+
#markFirstSeparatorBorderless() {
|
|
19381
|
+
const first = Array.from(this.children).find((child) => figIsMenuChild(child));
|
|
19382
|
+
if (first && (first.tagName === "FIG-SEPARATOR" || first.tagName === "FIG-MENU-SEPARATOR")) {
|
|
19383
|
+
first.setAttribute("borderless", "");
|
|
19384
|
+
}
|
|
19385
|
+
}
|
|
19386
|
+
|
|
19030
19387
|
#syncOverflow() {
|
|
19031
|
-
|
|
19388
|
+
this.#markFirstSeparatorBorderless();
|
|
19389
|
+
const scrollable = figSyncOverflowState(this.#panel, this.#panel, "y");
|
|
19390
|
+
this.classList.toggle(
|
|
19391
|
+
"overflow-start",
|
|
19392
|
+
Boolean(this.#panel?.classList.contains("overflow-start")),
|
|
19393
|
+
);
|
|
19394
|
+
this.classList.toggle(
|
|
19395
|
+
"overflow-end",
|
|
19396
|
+
Boolean(this.#panel?.classList.contains("overflow-end")),
|
|
19397
|
+
);
|
|
19398
|
+
return scrollable;
|
|
19032
19399
|
}
|
|
19033
19400
|
|
|
19034
19401
|
#ensureNavButtons() {
|
|
@@ -19060,13 +19427,14 @@ class FigMenu extends HTMLElement {
|
|
|
19060
19427
|
this.#navStart = null;
|
|
19061
19428
|
this.#navEnd = null;
|
|
19062
19429
|
this.#panel?.classList.remove("overflow-start", "overflow-end");
|
|
19430
|
+
this.classList.remove("overflow-start", "overflow-end");
|
|
19063
19431
|
}
|
|
19064
19432
|
|
|
19065
19433
|
#getItems() {
|
|
19066
|
-
|
|
19067
|
-
return Array.from(this.#panel.querySelectorAll("fig-menu-item")).filter(
|
|
19434
|
+
return Array.from(this.querySelectorAll(":scope > fig-menu-item")).filter(
|
|
19068
19435
|
(item) =>
|
|
19069
|
-
!item.
|
|
19436
|
+
!item.hidden &&
|
|
19437
|
+
(!item.hasAttribute("disabled") || item.getAttribute("disabled") === "false"),
|
|
19070
19438
|
);
|
|
19071
19439
|
}
|
|
19072
19440
|
|
|
@@ -19090,7 +19458,31 @@ class FigMenu extends HTMLElement {
|
|
|
19090
19458
|
this.#focusedIndex = wrapped;
|
|
19091
19459
|
const item = items[wrapped];
|
|
19092
19460
|
item.focus();
|
|
19093
|
-
|
|
19461
|
+
this.#scrollItemIntoView(item);
|
|
19462
|
+
}
|
|
19463
|
+
|
|
19464
|
+
#scrollItemIntoView(item) {
|
|
19465
|
+
const panel = this.#panel;
|
|
19466
|
+
if (!panel || !item) return;
|
|
19467
|
+
requestAnimationFrame(() => {
|
|
19468
|
+
if (!panel.isConnected || !item.isConnected) return;
|
|
19469
|
+
const scrollSize = panel.scrollHeight;
|
|
19470
|
+
const clientSize = panel.clientHeight;
|
|
19471
|
+
if (scrollSize <= clientSize + 1) {
|
|
19472
|
+
this.#syncOverflow();
|
|
19473
|
+
return;
|
|
19474
|
+
}
|
|
19475
|
+
const itemRect = item.getBoundingClientRect();
|
|
19476
|
+
const hostRect = panel.getBoundingClientRect();
|
|
19477
|
+
const elementStart = itemRect.top - hostRect.top + panel.scrollTop;
|
|
19478
|
+
const maxScroll = scrollSize - clientSize;
|
|
19479
|
+
const nextScroll = Math.max(
|
|
19480
|
+
0,
|
|
19481
|
+
Math.min(elementStart + itemRect.height / 2 - clientSize / 2, maxScroll),
|
|
19482
|
+
);
|
|
19483
|
+
panel.scrollTo({ top: nextScroll, behavior: "auto" });
|
|
19484
|
+
this.#syncOverflow();
|
|
19485
|
+
});
|
|
19094
19486
|
}
|
|
19095
19487
|
|
|
19096
19488
|
#syncDisabled() {
|
|
@@ -19170,19 +19562,30 @@ class FigMenu extends HTMLElement {
|
|
|
19170
19562
|
fallbackTimer = window.setTimeout(openMenu, 180);
|
|
19171
19563
|
}
|
|
19172
19564
|
|
|
19173
|
-
#
|
|
19565
|
+
#handleHostClick(e) {
|
|
19174
19566
|
if (this.#isDisabled()) return;
|
|
19175
|
-
|
|
19176
|
-
|
|
19177
|
-
|
|
19178
|
-
|
|
19567
|
+
if (this.#trigger?.contains(e.target) || this.#triggerHost?.contains(e.target)) {
|
|
19568
|
+
return;
|
|
19569
|
+
}
|
|
19570
|
+
const item = e.target.closest?.("fig-menu-item");
|
|
19571
|
+
// Direct children only — nested fig-menu inside an item has its own host.
|
|
19572
|
+
if (!item || item.parentElement !== this) return;
|
|
19573
|
+
if (item.hasAttribute("disabled") && item.getAttribute("disabled") !== "false") {
|
|
19574
|
+
return;
|
|
19575
|
+
}
|
|
19576
|
+
e.stopPropagation();
|
|
19179
19577
|
this.#selectItem(item);
|
|
19180
19578
|
}
|
|
19181
19579
|
|
|
19182
19580
|
#handleMenuKeydown(e) {
|
|
19183
19581
|
if (this.#isDisabled()) return;
|
|
19184
19582
|
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
19185
|
-
|
|
19583
|
+
// Slotted item keydown crosses the shadow slot, so the popup listener
|
|
19584
|
+
// already handles it. Skip the host to avoid double-stepping.
|
|
19585
|
+
if (e.currentTarget === this) {
|
|
19586
|
+
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
19587
|
+
if (this.#popup && path.includes(this.#popup)) return;
|
|
19588
|
+
}
|
|
19186
19589
|
if (!this.open || !this.#popup?.matches?.(":open")) {
|
|
19187
19590
|
if (
|
|
19188
19591
|
this.#trigger?.contains(e.target) &&
|