@rogieking/figui3 6.21.1 → 6.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/skills/figui3/SKILL.md +1 -15
- package/.cursor/skills/propkit/SKILL.md +2 -2
- package/README.md +6 -8
- package/components.css +344 -201
- package/dist/components.css +1 -1
- package/dist/fig-editor.css +1 -1
- package/dist/fig-editor.js +30 -33
- package/dist/fig-lab.css +1 -1
- package/dist/fig-lab.js +32 -103
- package/dist/fig.css +1 -1
- package/dist/fig.js +130 -41
- package/dist/propskit.css +1 -1
- package/fig-editor.js +186 -67
- package/fig-lab.css +32 -307
- package/fig-lab.js +727 -1278
- package/fig.js +1645 -288
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -105,9 +105,12 @@ function createFigOverflowButtons({
|
|
|
105
105
|
startClass = "",
|
|
106
106
|
endClass = "",
|
|
107
107
|
chevronClass = "",
|
|
108
|
+
startLabel = "Scroll back",
|
|
109
|
+
endLabel = "Scroll forward",
|
|
108
110
|
} = {}) {
|
|
109
111
|
const makeButton = (direction, onPointerDown) => {
|
|
110
112
|
const button = document.createElement("button");
|
|
113
|
+
button.type = "button";
|
|
111
114
|
button.className = [
|
|
112
115
|
"fig-overflow",
|
|
113
116
|
`fig-overflow-${direction}`,
|
|
@@ -118,7 +121,10 @@ function createFigOverflowButtons({
|
|
|
118
121
|
button.dataset.figOverflow = direction;
|
|
119
122
|
if (owner) button.setAttribute(`data-fig-${owner}-nav`, direction);
|
|
120
123
|
button.setAttribute("tabindex", "-1");
|
|
121
|
-
button.setAttribute(
|
|
124
|
+
button.setAttribute(
|
|
125
|
+
"aria-label",
|
|
126
|
+
direction === "start" ? startLabel : endLabel,
|
|
127
|
+
);
|
|
122
128
|
button.appendChild(
|
|
123
129
|
createFigIcon("chevron", {
|
|
124
130
|
size: "small",
|
|
@@ -164,6 +170,48 @@ function figScrollOverflowPage(scrollEl, axis = "x", direction = 1) {
|
|
|
164
170
|
});
|
|
165
171
|
}
|
|
166
172
|
|
|
173
|
+
function figScrollElementToCenter(
|
|
174
|
+
scrollEl,
|
|
175
|
+
element,
|
|
176
|
+
axis = "y",
|
|
177
|
+
behavior = "auto",
|
|
178
|
+
) {
|
|
179
|
+
if (!scrollEl || !element || !scrollEl.contains(element)) return;
|
|
180
|
+
requestAnimationFrame(() => {
|
|
181
|
+
if (!scrollEl.isConnected || !element.isConnected) return;
|
|
182
|
+
const isHorizontal = axis === "x";
|
|
183
|
+
const scrollSize = isHorizontal
|
|
184
|
+
? scrollEl.scrollWidth
|
|
185
|
+
: scrollEl.scrollHeight;
|
|
186
|
+
const clientSize = isHorizontal
|
|
187
|
+
? scrollEl.clientWidth
|
|
188
|
+
: scrollEl.clientHeight;
|
|
189
|
+
if (scrollSize <= clientSize + 1) {
|
|
190
|
+
figSyncOverflowState(scrollEl, scrollEl, axis);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const elementRect = element.getBoundingClientRect();
|
|
194
|
+
const hostRect = scrollEl.getBoundingClientRect();
|
|
195
|
+
const currentScroll = isHorizontal
|
|
196
|
+
? scrollEl.scrollLeft
|
|
197
|
+
: scrollEl.scrollTop;
|
|
198
|
+
const elementStart =
|
|
199
|
+
(isHorizontal ? elementRect.left - hostRect.left : elementRect.top - hostRect.top) +
|
|
200
|
+
currentScroll;
|
|
201
|
+
const elementSize = isHorizontal ? elementRect.width : elementRect.height;
|
|
202
|
+
const maxScroll = scrollSize - clientSize;
|
|
203
|
+
const nextScroll = Math.max(
|
|
204
|
+
0,
|
|
205
|
+
Math.min(elementStart + elementSize / 2 - clientSize / 2, maxScroll),
|
|
206
|
+
);
|
|
207
|
+
scrollEl.scrollTo({
|
|
208
|
+
[isHorizontal ? "left" : "top"]: nextScroll,
|
|
209
|
+
behavior,
|
|
210
|
+
});
|
|
211
|
+
figSyncOverflowState(scrollEl, scrollEl, axis);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
167
215
|
function hasFigFillPicker() {
|
|
168
216
|
return typeof customElements !== "undefined" && !!customElements.get("fig-fill-picker");
|
|
169
217
|
}
|
|
@@ -330,9 +378,11 @@ function figSupportsPopover() {
|
|
|
330
378
|
class FigButton extends HTMLElement {
|
|
331
379
|
type;
|
|
332
380
|
#selected;
|
|
381
|
+
#slottedDisabledStates = new WeakMap();
|
|
333
382
|
#a11yAttributes = ["aria-label", "aria-labelledby", "aria-describedby", "title"];
|
|
334
383
|
#boundHandleControlKeydown = this.#handleControlKeydown.bind(this);
|
|
335
384
|
#boundHandleClick = this.#handleClick.bind(this);
|
|
385
|
+
#boundHandleSlotChange = () => this.#syncSlottedControlDisabled();
|
|
336
386
|
#boundHandleFocus = () => {
|
|
337
387
|
if (this.button?.matches(":focus-visible")) {
|
|
338
388
|
this.setAttribute("data-focus-visible", "");
|
|
@@ -398,6 +448,9 @@ class FigButton extends HTMLElement {
|
|
|
398
448
|
this.button.addEventListener("blur", this.#boundHandleBlur);
|
|
399
449
|
}
|
|
400
450
|
|
|
451
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
452
|
+
slot?.removeEventListener("slotchange", this.#boundHandleSlotChange);
|
|
453
|
+
slot?.addEventListener("slotchange", this.#boundHandleSlotChange);
|
|
401
454
|
this.removeEventListener("keydown", this.#boundHandleControlKeydown);
|
|
402
455
|
this.addEventListener("keydown", this.#boundHandleControlKeydown);
|
|
403
456
|
|
|
@@ -514,9 +567,30 @@ class FigButton extends HTMLElement {
|
|
|
514
567
|
this.button.type = "button";
|
|
515
568
|
this.button.setAttribute("type", "button");
|
|
516
569
|
}
|
|
570
|
+
this.#syncSlottedControlDisabled();
|
|
517
571
|
this.#syncA11yAttributes();
|
|
518
572
|
this.#syncPressedState();
|
|
519
573
|
}
|
|
574
|
+
#syncSlottedControlDisabled() {
|
|
575
|
+
const control = this.#getSlottedControl();
|
|
576
|
+
if (!control) return;
|
|
577
|
+
const disabled = this.#isDisabled();
|
|
578
|
+
if (disabled) {
|
|
579
|
+
if (!this.#slottedDisabledStates.has(control)) {
|
|
580
|
+
this.#slottedDisabledStates.set(
|
|
581
|
+
control,
|
|
582
|
+
control.hasAttribute("disabled") &&
|
|
583
|
+
control.getAttribute("disabled") !== "false",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
control.setAttribute("disabled", "");
|
|
587
|
+
} else if (this.#slottedDisabledStates.has(control)) {
|
|
588
|
+
const wasDisabled = this.#slottedDisabledStates.get(control);
|
|
589
|
+
this.#slottedDisabledStates.delete(control);
|
|
590
|
+
if (wasDisabled) control.setAttribute("disabled", "");
|
|
591
|
+
else control.removeAttribute("disabled");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
520
594
|
static get observedAttributes() {
|
|
521
595
|
return [
|
|
522
596
|
"disabled",
|
|
@@ -555,6 +629,9 @@ class FigButton extends HTMLElement {
|
|
|
555
629
|
}
|
|
556
630
|
disconnectedCallback() {
|
|
557
631
|
this.removeEventListener("keydown", this.#boundHandleControlKeydown);
|
|
632
|
+
this.shadowRoot
|
|
633
|
+
?.querySelector("slot")
|
|
634
|
+
?.removeEventListener("slotchange", this.#boundHandleSlotChange);
|
|
558
635
|
}
|
|
559
636
|
}
|
|
560
637
|
figDefineElement("fig-button", FigButton);
|
|
@@ -570,8 +647,6 @@ class FigDropdown extends HTMLElement {
|
|
|
570
647
|
#boundHandleSelectInput;
|
|
571
648
|
#boundHandleSelectChange;
|
|
572
649
|
#boundHandleSelectKeydown;
|
|
573
|
-
#selectedContentEnabled = false;
|
|
574
|
-
#selectedContentEl = null;
|
|
575
650
|
|
|
576
651
|
get label() {
|
|
577
652
|
return this.#label;
|
|
@@ -591,54 +666,6 @@ class FigDropdown extends HTMLElement {
|
|
|
591
666
|
this.#boundSlotChange = this.slotChange.bind(this);
|
|
592
667
|
}
|
|
593
668
|
|
|
594
|
-
#supportsSelectedContent() {
|
|
595
|
-
if (typeof CSS === "undefined" || typeof CSS.supports !== "function")
|
|
596
|
-
return false;
|
|
597
|
-
try {
|
|
598
|
-
return (
|
|
599
|
-
CSS.supports("appearance: base-select") &&
|
|
600
|
-
CSS.supports("selector(::picker(select))")
|
|
601
|
-
);
|
|
602
|
-
} catch {
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
#enableSelectedContentIfNeeded() {
|
|
608
|
-
const experimental = this.getAttribute("experimental") || "";
|
|
609
|
-
const wantsModern = experimental
|
|
610
|
-
.split(/\s+/)
|
|
611
|
-
.filter(Boolean)
|
|
612
|
-
.includes("modern");
|
|
613
|
-
|
|
614
|
-
if (!wantsModern || !this.#supportsSelectedContent()) {
|
|
615
|
-
this.#selectedContentEnabled = false;
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const button = document.createElement("button");
|
|
620
|
-
button.setAttribute("type", "button");
|
|
621
|
-
button.setAttribute("aria-hidden", "true");
|
|
622
|
-
const selected = document.createElement("selectedcontent");
|
|
623
|
-
button.appendChild(selected);
|
|
624
|
-
this.select.appendChild(button);
|
|
625
|
-
this.#selectedContentEnabled = true;
|
|
626
|
-
this.#selectedContentEl = selected;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
#syncSelectedContent() {
|
|
630
|
-
if (!this.#selectedContentEl) return;
|
|
631
|
-
const selectedOption = this.select.selectedOptions?.[0];
|
|
632
|
-
if (!selectedOption) {
|
|
633
|
-
this.#selectedContentEl.textContent = "";
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
// Fallback mirror for browsers that don't auto-project selectedcontent reliably.
|
|
637
|
-
this.#selectedContentEl.replaceChildren(
|
|
638
|
-
...Array.from(selectedOption.childNodes, (node) => node.cloneNode(true)),
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
669
|
#addEventListeners() {
|
|
643
670
|
this.select.addEventListener("input", this.#boundHandleSelectInput);
|
|
644
671
|
this.select.addEventListener("change", this.#boundHandleSelectChange);
|
|
@@ -703,8 +730,6 @@ class FigDropdown extends HTMLElement {
|
|
|
703
730
|
this.select.firstChild.remove();
|
|
704
731
|
}
|
|
705
732
|
|
|
706
|
-
this.#enableSelectedContentIfNeeded();
|
|
707
|
-
|
|
708
733
|
if (this.type === "dropdown") {
|
|
709
734
|
const hiddenOption = document.createElement("option");
|
|
710
735
|
hiddenOption.setAttribute("hidden", "true");
|
|
@@ -720,7 +745,6 @@ class FigDropdown extends HTMLElement {
|
|
|
720
745
|
if (selectedValue !== null) {
|
|
721
746
|
this.#syncSelectedValue(selectedValue);
|
|
722
747
|
}
|
|
723
|
-
this.#syncSelectedContent();
|
|
724
748
|
if (this.type === "dropdown") {
|
|
725
749
|
this.select.selectedIndex = -1;
|
|
726
750
|
}
|
|
@@ -743,7 +767,6 @@ class FigDropdown extends HTMLElement {
|
|
|
743
767
|
this.#selectedValue = selectedValue;
|
|
744
768
|
}
|
|
745
769
|
this.setAttribute("value", selectedValue);
|
|
746
|
-
this.#syncSelectedContent();
|
|
747
770
|
this.dispatchEvent(
|
|
748
771
|
new CustomEvent("input", {
|
|
749
772
|
detail: selectedValue,
|
|
@@ -771,7 +794,6 @@ class FigDropdown extends HTMLElement {
|
|
|
771
794
|
if (this.type === "dropdown") {
|
|
772
795
|
this.select.selectedIndex = -1;
|
|
773
796
|
}
|
|
774
|
-
this.#syncSelectedContent();
|
|
775
797
|
this.dispatchEvent(
|
|
776
798
|
new CustomEvent("change", {
|
|
777
799
|
detail: selectedValue,
|
|
@@ -784,7 +806,6 @@ class FigDropdown extends HTMLElement {
|
|
|
784
806
|
#handleSelectKeydown(e) {
|
|
785
807
|
if (this.closest('fig-button[type="select"]')) return;
|
|
786
808
|
if (e.key !== "Enter" || e.defaultPrevented) return;
|
|
787
|
-
if (this.#selectedContentEnabled && this.select.matches(":open")) return;
|
|
788
809
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
789
810
|
if (this.select.disabled || this.select.multiple) return;
|
|
790
811
|
if (typeof this.select.showPicker !== "function") return;
|
|
@@ -817,7 +838,7 @@ class FigDropdown extends HTMLElement {
|
|
|
817
838
|
this.setAttribute("value", value);
|
|
818
839
|
}
|
|
819
840
|
static get observedAttributes() {
|
|
820
|
-
return ["value", "type", "
|
|
841
|
+
return ["value", "type", "label", "disabled"];
|
|
821
842
|
}
|
|
822
843
|
#syncDisabled() {
|
|
823
844
|
const disabled =
|
|
@@ -830,7 +851,6 @@ class FigDropdown extends HTMLElement {
|
|
|
830
851
|
return;
|
|
831
852
|
}
|
|
832
853
|
if (this.select) this.select.value = value ?? "";
|
|
833
|
-
this.#syncSelectedContent();
|
|
834
854
|
}
|
|
835
855
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
836
856
|
if (name === "value") {
|
|
@@ -840,9 +860,6 @@ class FigDropdown extends HTMLElement {
|
|
|
840
860
|
this.type = newValue || "select";
|
|
841
861
|
if (this.isConnected) this.slotChange();
|
|
842
862
|
}
|
|
843
|
-
if (name === "experimental") {
|
|
844
|
-
this.slotChange();
|
|
845
|
-
}
|
|
846
863
|
if (name === "label") {
|
|
847
864
|
this.#label = newValue || "Menu";
|
|
848
865
|
this.select.setAttribute("aria-label", this.#label);
|
|
@@ -887,6 +904,8 @@ class FigTooltip extends HTMLElement {
|
|
|
887
904
|
#boundHidePopupOutsideClick;
|
|
888
905
|
#boundShowDelayedPopup;
|
|
889
906
|
#boundHandlePointerLeave;
|
|
907
|
+
#boundHandleFocus;
|
|
908
|
+
#boundHandleBlur;
|
|
890
909
|
#boundHandleTouchStart;
|
|
891
910
|
#boundHandleTouchMove;
|
|
892
911
|
#boundHandleTouchEnd;
|
|
@@ -896,6 +915,7 @@ class FigTooltip extends HTMLElement {
|
|
|
896
915
|
#parentDialog = null;
|
|
897
916
|
#triggerEl = null;
|
|
898
917
|
#childObserver = null;
|
|
918
|
+
#suppressFocusOpen = false;
|
|
899
919
|
#touchTimeout;
|
|
900
920
|
#isTouching = false;
|
|
901
921
|
constructor() {
|
|
@@ -908,6 +928,13 @@ class FigTooltip extends HTMLElement {
|
|
|
908
928
|
this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
|
|
909
929
|
this.#boundShowDelayedPopup = this.showDelayedPopup.bind(this);
|
|
910
930
|
this.#boundHandlePointerLeave = this.#handlePointerLeave.bind(this);
|
|
931
|
+
this.#boundHandleFocus = () => {
|
|
932
|
+
if (!this.#suppressFocusOpen) this.showDelayedPopup();
|
|
933
|
+
};
|
|
934
|
+
this.#boundHandleBlur = () => {
|
|
935
|
+
this.#suppressFocusOpen = false;
|
|
936
|
+
this.hidePopup();
|
|
937
|
+
};
|
|
911
938
|
this.#boundHandleTouchStart = this.#handleTouchStart.bind(this);
|
|
912
939
|
this.#boundHandleTouchMove = this.#handleTouchMove.bind(this);
|
|
913
940
|
this.#boundHandleTouchEnd = this.#handleTouchEnd.bind(this);
|
|
@@ -1002,6 +1029,8 @@ class FigTooltip extends HTMLElement {
|
|
|
1002
1029
|
trigger.addEventListener("touchcancel", this.#boundHandleTouchCancel, {
|
|
1003
1030
|
passive: true,
|
|
1004
1031
|
});
|
|
1032
|
+
trigger.addEventListener("focus", this.#boundHandleFocus);
|
|
1033
|
+
trigger.addEventListener("blur", this.#boundHandleBlur);
|
|
1005
1034
|
} else if (this.action === "click") {
|
|
1006
1035
|
trigger.addEventListener("click", this.#boundShowDelayedPopup);
|
|
1007
1036
|
trigger.addEventListener("touchstart", this.#boundShowDelayedPopup, {
|
|
@@ -1020,6 +1049,8 @@ class FigTooltip extends HTMLElement {
|
|
|
1020
1049
|
trigger.removeEventListener("touchmove", this.#boundHandleTouchMove);
|
|
1021
1050
|
trigger.removeEventListener("touchend", this.#boundHandleTouchEnd);
|
|
1022
1051
|
trigger.removeEventListener("touchcancel", this.#boundHandleTouchCancel);
|
|
1052
|
+
trigger.removeEventListener("focus", this.#boundHandleFocus);
|
|
1053
|
+
trigger.removeEventListener("blur", this.#boundHandleBlur);
|
|
1023
1054
|
} else if (this.action === "click") {
|
|
1024
1055
|
trigger.removeEventListener("click", this.#boundShowDelayedPopup);
|
|
1025
1056
|
trigger.removeEventListener("touchstart", this.#boundShowDelayedPopup);
|
|
@@ -1421,6 +1452,16 @@ class FigTooltip extends HTMLElement {
|
|
|
1421
1452
|
if (!(node instanceof FigTooltip)) continue;
|
|
1422
1453
|
if (node.action !== "hover") continue;
|
|
1423
1454
|
if (node.#showPersisted) continue;
|
|
1455
|
+
node.#suppressFocusOpen = true;
|
|
1456
|
+
setTimeout(() => {
|
|
1457
|
+
const trigger = node.#triggerEl;
|
|
1458
|
+
if (
|
|
1459
|
+
trigger !== document.activeElement &&
|
|
1460
|
+
!trigger?.matches?.(":focus, :focus-within")
|
|
1461
|
+
) {
|
|
1462
|
+
node.#suppressFocusOpen = false;
|
|
1463
|
+
}
|
|
1464
|
+
}, 0);
|
|
1424
1465
|
if (node.isOpen || node.timeout) node.hidePopup();
|
|
1425
1466
|
}
|
|
1426
1467
|
for (const anchor of Array.from(FigTooltip.#programmaticAnchors)) {
|
|
@@ -5051,77 +5092,1209 @@ class FigOptions extends HTMLElement {
|
|
|
5051
5092
|
}
|
|
5052
5093
|
}
|
|
5053
5094
|
|
|
5054
|
-
#syncValueToChild() {
|
|
5055
|
-
if (!this.#childControl || this.#suppressEvents) return;
|
|
5056
|
-
const val = this.getAttribute("value") || "";
|
|
5057
|
-
this.#childControl.value = val;
|
|
5058
|
-
}
|
|
5059
|
-
|
|
5060
|
-
#syncAttrToChild(attr) {
|
|
5061
|
-
if (!this.#childControl) return;
|
|
5062
|
-
if (this.hasAttribute(attr)) {
|
|
5063
|
-
this.#childControl.setAttribute(attr, this.getAttribute(attr) || "");
|
|
5064
|
-
} else {
|
|
5065
|
-
this.#childControl.removeAttribute(attr);
|
|
5066
|
-
}
|
|
5095
|
+
#syncValueToChild() {
|
|
5096
|
+
if (!this.#childControl || this.#suppressEvents) return;
|
|
5097
|
+
const val = this.getAttribute("value") || "";
|
|
5098
|
+
this.#childControl.value = val;
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
#syncAttrToChild(attr) {
|
|
5102
|
+
if (!this.#childControl) return;
|
|
5103
|
+
if (this.hasAttribute(attr)) {
|
|
5104
|
+
this.#childControl.setAttribute(attr, this.getAttribute(attr) || "");
|
|
5105
|
+
} else {
|
|
5106
|
+
this.#childControl.removeAttribute(attr);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
#startResizeObserver() {
|
|
5111
|
+
this.#resizeObserver?.disconnect();
|
|
5112
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
5113
|
+
this.#checkOverflow();
|
|
5114
|
+
});
|
|
5115
|
+
this.#resizeObserver.observe(this);
|
|
5116
|
+
}
|
|
5117
|
+
|
|
5118
|
+
#isSegmentTruncated(seg) {
|
|
5119
|
+
const range = document.createRange();
|
|
5120
|
+
range.selectNodeContents(seg);
|
|
5121
|
+
const textWidth = range.getBoundingClientRect().width;
|
|
5122
|
+
const segRect = seg.getBoundingClientRect();
|
|
5123
|
+
const segWidth = segRect.width;
|
|
5124
|
+
const cs = getComputedStyle(seg);
|
|
5125
|
+
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
5126
|
+
const padR = parseFloat(cs.paddingRight) || 0;
|
|
5127
|
+
const contentWidth = segWidth - padL - padR;
|
|
5128
|
+
return textWidth > contentWidth + 0.5;
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
#anySegmentTruncated() {
|
|
5132
|
+
const segments = this.querySelectorAll("fig-segment");
|
|
5133
|
+
for (const seg of segments) {
|
|
5134
|
+
if (this.#isSegmentTruncated(seg)) return true;
|
|
5135
|
+
}
|
|
5136
|
+
return false;
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
#checkOverflow() {
|
|
5140
|
+
if (this.#parsedOptions.length <= 1) return;
|
|
5141
|
+
|
|
5142
|
+
if (this.#currentMode === "segments") {
|
|
5143
|
+
const sc = this.#childControl;
|
|
5144
|
+
const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
|
|
5145
|
+
if (containerOverflow || this.#anySegmentTruncated()) {
|
|
5146
|
+
this.#naturalWidth = this.clientWidth;
|
|
5147
|
+
this.#renderDropdown();
|
|
5148
|
+
}
|
|
5149
|
+
} else {
|
|
5150
|
+
if (this.#naturalWidth > 0 && this.clientWidth >= this.#naturalWidth) {
|
|
5151
|
+
this.#renderSegments();
|
|
5152
|
+
requestAnimationFrame(() => {
|
|
5153
|
+
requestAnimationFrame(() => {
|
|
5154
|
+
const sc = this.#childControl;
|
|
5155
|
+
const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
|
|
5156
|
+
if (containerOverflow || this.#anySegmentTruncated()) {
|
|
5157
|
+
this.#renderDropdown();
|
|
5158
|
+
}
|
|
5159
|
+
});
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
figDefineElement("fig-options", FigOptions);
|
|
5166
|
+
|
|
5167
|
+
/* Select — dropdown-styled trigger + fig-popup listbox */
|
|
5168
|
+
/** Parse options attr — same formats as fig-options / propskit-select. */
|
|
5169
|
+
function figSelectParseOptionsAttribute(raw) {
|
|
5170
|
+
const text = raw || "";
|
|
5171
|
+
if (text.startsWith("[")) {
|
|
5172
|
+
try {
|
|
5173
|
+
const parsed = JSON.parse(text);
|
|
5174
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
5175
|
+
} catch {
|
|
5176
|
+
/* fall through */
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
const delimiter = text.includes("\n") ? "\n" : ",";
|
|
5180
|
+
return text
|
|
5181
|
+
.split(delimiter)
|
|
5182
|
+
.map((s) => s.trim())
|
|
5183
|
+
.filter(Boolean);
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
function figSelectOptionEntryValue(opt) {
|
|
5187
|
+
if (opt && typeof opt === "object") {
|
|
5188
|
+
return String(opt.value ?? opt.label ?? "");
|
|
5189
|
+
}
|
|
5190
|
+
return String(opt ?? "");
|
|
5191
|
+
}
|
|
5192
|
+
|
|
5193
|
+
function figSelectOptionEntryLabel(opt) {
|
|
5194
|
+
if (opt && typeof opt === "object") {
|
|
5195
|
+
return String(opt.label ?? opt.value ?? "");
|
|
5196
|
+
}
|
|
5197
|
+
return String(opt ?? "");
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5200
|
+
/**
|
|
5201
|
+
* A selectable option for fig-select.
|
|
5202
|
+
* Supports light-DOM slots: `slot="prepend"` (leading) and `slot="append"` (trailing).
|
|
5203
|
+
* Use the `label` attribute for the closed-trigger label when option content is rich.
|
|
5204
|
+
*
|
|
5205
|
+
* @attr {string} value - Option value
|
|
5206
|
+
* @attr {string} label - Optional display label for the select trigger
|
|
5207
|
+
* @attr {boolean} disabled - Whether the option is disabled
|
|
5208
|
+
* @attr {boolean} selected - Whether the option is selected
|
|
5209
|
+
*/
|
|
5210
|
+
class FigSelectOption extends HTMLElement {
|
|
5211
|
+
static get observedAttributes() {
|
|
5212
|
+
return ["value", "disabled", "selected", "label"];
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
get value() {
|
|
5216
|
+
const attr = this.getAttribute("value");
|
|
5217
|
+
if (attr !== null) return attr;
|
|
5218
|
+
return (this.textContent || "").trim();
|
|
5219
|
+
}
|
|
5220
|
+
|
|
5221
|
+
set value(val) {
|
|
5222
|
+
if (val === null || val === undefined) {
|
|
5223
|
+
this.removeAttribute("value");
|
|
5224
|
+
} else {
|
|
5225
|
+
this.setAttribute("value", String(val));
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
|
|
5229
|
+
get disabled() {
|
|
5230
|
+
return figBooleanAttribute(this, "disabled");
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
set disabled(val) {
|
|
5234
|
+
if (val) this.setAttribute("disabled", "");
|
|
5235
|
+
else this.removeAttribute("disabled");
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
get selected() {
|
|
5239
|
+
return figBooleanAttribute(this, "selected");
|
|
5240
|
+
}
|
|
5241
|
+
|
|
5242
|
+
set selected(val) {
|
|
5243
|
+
if (val) this.setAttribute("selected", "");
|
|
5244
|
+
else this.removeAttribute("selected");
|
|
5245
|
+
}
|
|
5246
|
+
|
|
5247
|
+
connectedCallback() {
|
|
5248
|
+
if (!this.hasAttribute("role")) this.setAttribute("role", "option");
|
|
5249
|
+
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5250
|
+
this.#syncDisabled();
|
|
5251
|
+
}
|
|
5252
|
+
|
|
5253
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5254
|
+
if (oldValue === newValue) return;
|
|
5255
|
+
if (name === "disabled") this.#syncDisabled();
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
#syncDisabled() {
|
|
5259
|
+
const disabled = this.disabled;
|
|
5260
|
+
if (disabled) {
|
|
5261
|
+
this.setAttribute("aria-disabled", "true");
|
|
5262
|
+
this.setAttribute("tabindex", "-1");
|
|
5263
|
+
} else {
|
|
5264
|
+
this.removeAttribute("aria-disabled");
|
|
5265
|
+
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
figDefineElement("fig-select-option", FigSelectOption);
|
|
5270
|
+
|
|
5271
|
+
/** Light-DOM panel wrapper projected into fig-select's popup; owns overflow buttons. */
|
|
5272
|
+
class FigSelectOptions extends HTMLElement {
|
|
5273
|
+
#navStart = null;
|
|
5274
|
+
#navEnd = null;
|
|
5275
|
+
#resizeObserver = null;
|
|
5276
|
+
#boundSyncOverflow = this.syncOverflow.bind(this);
|
|
5277
|
+
|
|
5278
|
+
connectedCallback() {
|
|
5279
|
+
if (!this.hasAttribute("slot")) this.setAttribute("slot", "panel");
|
|
5280
|
+
this.#unwrapLegacyChooser();
|
|
5281
|
+
this.#ensureNavButtons();
|
|
5282
|
+
this.addEventListener("scroll", this.#boundSyncOverflow, { passive: true });
|
|
5283
|
+
this.#resizeObserver?.disconnect();
|
|
5284
|
+
this.#resizeObserver = new ResizeObserver(() => this.syncOverflow());
|
|
5285
|
+
this.#resizeObserver.observe(this);
|
|
5286
|
+
requestAnimationFrame(() => this.syncOverflow());
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5289
|
+
disconnectedCallback() {
|
|
5290
|
+
this.removeEventListener("scroll", this.#boundSyncOverflow);
|
|
5291
|
+
this.#resizeObserver?.disconnect();
|
|
5292
|
+
this.#resizeObserver = null;
|
|
5293
|
+
this.#removeNavButtons();
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
syncOverflow() {
|
|
5297
|
+
return figSyncOverflowState(this, this, "y");
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
scrollToOption(option, behavior = "auto") {
|
|
5301
|
+
figScrollElementToCenter(this, option, "y", behavior);
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
#unwrapLegacyChooser() {
|
|
5305
|
+
const chooser = this.querySelector(":scope > fig-chooser");
|
|
5306
|
+
if (!chooser) return;
|
|
5307
|
+
while (chooser.firstChild) {
|
|
5308
|
+
this.insertBefore(chooser.firstChild, chooser);
|
|
5309
|
+
}
|
|
5310
|
+
chooser.remove();
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
#ensureNavButtons() {
|
|
5314
|
+
if (
|
|
5315
|
+
this.#navStart &&
|
|
5316
|
+
this.#navEnd &&
|
|
5317
|
+
this.contains(this.#navStart) &&
|
|
5318
|
+
this.contains(this.#navEnd)
|
|
5319
|
+
) {
|
|
5320
|
+
return;
|
|
5321
|
+
}
|
|
5322
|
+
this.#removeNavButtons();
|
|
5323
|
+
const buttons = createFigOverflowButtons({
|
|
5324
|
+
owner: "select",
|
|
5325
|
+
startLabel: "Scroll up",
|
|
5326
|
+
endLabel: "Scroll down",
|
|
5327
|
+
onStart: () => figScrollOverflowPage(this, "y", -1),
|
|
5328
|
+
onEnd: () => figScrollOverflowPage(this, "y", 1),
|
|
5329
|
+
});
|
|
5330
|
+
this.#navStart = buttons.start;
|
|
5331
|
+
this.#navEnd = buttons.end;
|
|
5332
|
+
this.prepend(this.#navStart);
|
|
5333
|
+
this.append(this.#navEnd);
|
|
5334
|
+
}
|
|
5335
|
+
|
|
5336
|
+
#removeNavButtons() {
|
|
5337
|
+
this.#navStart?.remove();
|
|
5338
|
+
this.#navEnd?.remove();
|
|
5339
|
+
this.#navStart = null;
|
|
5340
|
+
this.#navEnd = null;
|
|
5341
|
+
this.classList.remove("overflow-start", "overflow-end");
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
figDefineElement("fig-select-options", FigSelectOptions);
|
|
5345
|
+
|
|
5346
|
+
class FigSelect extends HTMLElement {
|
|
5347
|
+
#button = null;
|
|
5348
|
+
#popup = null;
|
|
5349
|
+
#prependEl = null;
|
|
5350
|
+
#labelEl = null;
|
|
5351
|
+
#panelSlot = null;
|
|
5352
|
+
#observer = null;
|
|
5353
|
+
#initialized = false;
|
|
5354
|
+
#focusedIndex = -1;
|
|
5355
|
+
#syncingValue = false;
|
|
5356
|
+
#popupPositionPatched = false;
|
|
5357
|
+
#originalPositionPopup = null;
|
|
5358
|
+
/**
|
|
5359
|
+
* After open align, ignore content/scroll-driven positionPopup passes so
|
|
5360
|
+
* overflow paging isn't yanked back. Still realign when the trigger moves
|
|
5361
|
+
* or the viewport size changes (window resize, layout shift, page scroll).
|
|
5362
|
+
*/
|
|
5363
|
+
#freezeMenuPosition = false;
|
|
5364
|
+
#frozenLabelRect = null;
|
|
5365
|
+
#frozenViewport = null;
|
|
5366
|
+
#syncingOptions = false;
|
|
5367
|
+
#boundTriggerClick = this.#handleTriggerClick.bind(this);
|
|
5368
|
+
#boundOptionClick = this.#handleOptionClick.bind(this);
|
|
5369
|
+
#boundKeydown = this.#handleKeydown.bind(this);
|
|
5370
|
+
#boundPopupClose = this.#handlePopupClose.bind(this);
|
|
5371
|
+
#boundSlotChange = this.#handleSlotChange.bind(this);
|
|
5372
|
+
|
|
5373
|
+
static get observedAttributes() {
|
|
5374
|
+
return [
|
|
5375
|
+
"value",
|
|
5376
|
+
"disabled",
|
|
5377
|
+
"label",
|
|
5378
|
+
"options",
|
|
5379
|
+
"position",
|
|
5380
|
+
"offset",
|
|
5381
|
+
"closedby",
|
|
5382
|
+
"open",
|
|
5383
|
+
];
|
|
5384
|
+
}
|
|
5385
|
+
|
|
5386
|
+
get value() {
|
|
5387
|
+
return this.getAttribute("value") ?? "";
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5390
|
+
set value(val) {
|
|
5391
|
+
if (val === null || val === undefined) this.removeAttribute("value");
|
|
5392
|
+
else this.setAttribute("value", String(val));
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
get open() {
|
|
5396
|
+
return figBooleanAttribute(this, "open");
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
set open(val) {
|
|
5400
|
+
if (val) this.setAttribute("open", "");
|
|
5401
|
+
else this.removeAttribute("open");
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
connectedCallback() {
|
|
5405
|
+
if (!this.#initialized) this.#initialize();
|
|
5406
|
+
this.#ensurePanelSlotAttrs();
|
|
5407
|
+
this.#syncOptionsFromAttribute();
|
|
5408
|
+
this.#syncDisabled();
|
|
5409
|
+
this.#syncPopupAttrs();
|
|
5410
|
+
this.#syncValue();
|
|
5411
|
+
this.#setupObserver();
|
|
5412
|
+
if (this.open) this.#openList();
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
disconnectedCallback() {
|
|
5416
|
+
this.#teardownListeners();
|
|
5417
|
+
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
5418
|
+
this.#observer?.disconnect();
|
|
5419
|
+
this.#observer = null;
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5422
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5423
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
5424
|
+
if (name === "options") {
|
|
5425
|
+
this.#syncOptionsFromAttribute();
|
|
5426
|
+
this.#syncValue();
|
|
5427
|
+
return;
|
|
5428
|
+
}
|
|
5429
|
+
if (name === "value" || name === "label") {
|
|
5430
|
+
this.#syncValue();
|
|
5431
|
+
return;
|
|
5432
|
+
}
|
|
5433
|
+
if (name === "disabled") {
|
|
5434
|
+
this.#syncDisabled();
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
5437
|
+
if (name === "open") {
|
|
5438
|
+
if (newValue === null || newValue === "false") this.#closeList();
|
|
5439
|
+
else this.#openList();
|
|
5440
|
+
return;
|
|
5441
|
+
}
|
|
5442
|
+
if (name === "position" || name === "offset" || name === "closedby") {
|
|
5443
|
+
this.#syncPopupAttrs();
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
focus(options) {
|
|
5448
|
+
this.#button?.focus(options);
|
|
5449
|
+
}
|
|
5450
|
+
|
|
5451
|
+
blur() {
|
|
5452
|
+
this.#button?.blur();
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
#isMenuChild(node) {
|
|
5456
|
+
return (
|
|
5457
|
+
node?.nodeType === 1 &&
|
|
5458
|
+
(node.tagName === "FIG-SELECT-OPTION" ||
|
|
5459
|
+
node.tagName === "FIG-MENU-SEPARATOR" ||
|
|
5460
|
+
node.tagName === "FIG-SELECT-OPTIONS")
|
|
5461
|
+
);
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5464
|
+
#ensurePanelSlotAttrs() {
|
|
5465
|
+
for (const panel of this.querySelectorAll(":scope > fig-select-options")) {
|
|
5466
|
+
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
|
|
5470
|
+
#getPanel() {
|
|
5471
|
+
const assigned = this.#panelSlot?.assignedElements({ flatten: true }) ?? [];
|
|
5472
|
+
const fromSlot = assigned.find(
|
|
5473
|
+
(el) => el.tagName === "FIG-SELECT-OPTIONS",
|
|
5474
|
+
);
|
|
5475
|
+
if (fromSlot) return fromSlot;
|
|
5476
|
+
return this.querySelector(":scope > fig-select-options");
|
|
5477
|
+
}
|
|
5478
|
+
|
|
5479
|
+
#hasAuthoredOptions() {
|
|
5480
|
+
return Boolean(
|
|
5481
|
+
this.querySelector(
|
|
5482
|
+
":scope > fig-select-option:not([data-fig-generated]), :scope > fig-select-options > fig-select-option:not([data-fig-generated])",
|
|
5483
|
+
),
|
|
5484
|
+
);
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
#ensureOptionsPanel() {
|
|
5488
|
+
let panel = this.#getPanel();
|
|
5489
|
+
if (panel) {
|
|
5490
|
+
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5491
|
+
return panel;
|
|
5492
|
+
}
|
|
5493
|
+
panel = document.createElement("fig-select-options");
|
|
5494
|
+
panel.setAttribute("slot", "panel");
|
|
5495
|
+
panel.setAttribute("data-fig-generated", "");
|
|
5496
|
+
this.appendChild(panel);
|
|
5497
|
+
return panel;
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5500
|
+
/**
|
|
5501
|
+
* When no authored fig-select-option exists, build panel/options from the
|
|
5502
|
+
* options attribute (comma / newline / JSON — same as fig-options).
|
|
5503
|
+
*/
|
|
5504
|
+
#syncOptionsFromAttribute() {
|
|
5505
|
+
if (this.#hasAuthoredOptions()) return;
|
|
5506
|
+
|
|
5507
|
+
const hasOptionsAttr = this.hasAttribute("options");
|
|
5508
|
+
const panel = hasOptionsAttr
|
|
5509
|
+
? this.#ensureOptionsPanel()
|
|
5510
|
+
: this.#getPanel();
|
|
5511
|
+
if (!panel) return;
|
|
5512
|
+
|
|
5513
|
+
this.#syncingOptions = true;
|
|
5514
|
+
try {
|
|
5515
|
+
for (const opt of panel.querySelectorAll(
|
|
5516
|
+
":scope > fig-select-option[data-fig-generated]",
|
|
5517
|
+
)) {
|
|
5518
|
+
opt.remove();
|
|
5519
|
+
}
|
|
5520
|
+
|
|
5521
|
+
if (!hasOptionsAttr) return;
|
|
5522
|
+
|
|
5523
|
+
const parsed = figSelectParseOptionsAttribute(this.getAttribute("options"));
|
|
5524
|
+
const endBtn = panel.querySelector(":scope > .fig-overflow-end");
|
|
5525
|
+
for (const entry of parsed) {
|
|
5526
|
+
const el = document.createElement("fig-select-option");
|
|
5527
|
+
el.setAttribute("data-fig-generated", "");
|
|
5528
|
+
el.setAttribute("value", figSelectOptionEntryValue(entry));
|
|
5529
|
+
el.textContent = figSelectOptionEntryLabel(entry);
|
|
5530
|
+
if (endBtn) panel.insertBefore(el, endBtn);
|
|
5531
|
+
else panel.appendChild(el);
|
|
5532
|
+
}
|
|
5533
|
+
} finally {
|
|
5534
|
+
this.#syncingOptions = false;
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
#initialize() {
|
|
5539
|
+
this.#initialized = true;
|
|
5540
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
5541
|
+
shadow.innerHTML = `
|
|
5542
|
+
<style>
|
|
5543
|
+
:host {
|
|
5544
|
+
display: inline-flex;
|
|
5545
|
+
position: relative;
|
|
5546
|
+
align-items: center;
|
|
5547
|
+
min-width: 0;
|
|
5548
|
+
}
|
|
5549
|
+
:host([full]:not([full="false"])) {
|
|
5550
|
+
display: flex;
|
|
5551
|
+
width: 100%;
|
|
5552
|
+
}
|
|
5553
|
+
.fig-select-trigger {
|
|
5554
|
+
display: flex;
|
|
5555
|
+
align-items: center;
|
|
5556
|
+
justify-content: flex-start;
|
|
5557
|
+
flex: 1;
|
|
5558
|
+
min-width: 0;
|
|
5559
|
+
width: var(--fig-select-trigger-width, 100%);
|
|
5560
|
+
height: 100%;
|
|
5561
|
+
margin: 0;
|
|
5562
|
+
padding: 0 var(--spacer-4, 1rem) 0 var(--spacer-2, 0.5rem);
|
|
5563
|
+
border: 0;
|
|
5564
|
+
border-radius: inherit;
|
|
5565
|
+
background: transparent;
|
|
5566
|
+
box-shadow: none;
|
|
5567
|
+
color: inherit;
|
|
5568
|
+
font: inherit;
|
|
5569
|
+
font-weight: inherit;
|
|
5570
|
+
text-align: left;
|
|
5571
|
+
white-space: nowrap;
|
|
5572
|
+
overflow: hidden;
|
|
5573
|
+
text-overflow: ellipsis;
|
|
5574
|
+
cursor: default;
|
|
5575
|
+
}
|
|
5576
|
+
.fig-select-trigger:has(.fig-select-prepend:not(:empty)) {
|
|
5577
|
+
padding-left: 0;
|
|
5578
|
+
}
|
|
5579
|
+
.fig-select-trigger:hover,
|
|
5580
|
+
.fig-select-trigger:active,
|
|
5581
|
+
.fig-select-trigger:active:hover {
|
|
5582
|
+
background: transparent;
|
|
5583
|
+
box-shadow: none;
|
|
5584
|
+
color: inherit;
|
|
5585
|
+
}
|
|
5586
|
+
.fig-select-trigger:focus-visible,
|
|
5587
|
+
.fig-select-trigger[data-focus-visible] {
|
|
5588
|
+
outline: var(--figma-focus-outline);
|
|
5589
|
+
outline-offset: var(--figma-focus-outline-offset);
|
|
5590
|
+
}
|
|
5591
|
+
:host([disabled]:not([disabled="false"])) .fig-select-trigger,
|
|
5592
|
+
:host([disabled]:not([disabled="false"])) .fig-select-label {
|
|
5593
|
+
color: var(--figma-color-text-tertiary);
|
|
5594
|
+
}
|
|
5595
|
+
.fig-select-label {
|
|
5596
|
+
display: block;
|
|
5597
|
+
width: 100%;
|
|
5598
|
+
min-width: 0;
|
|
5599
|
+
overflow: hidden;
|
|
5600
|
+
text-overflow: ellipsis;
|
|
5601
|
+
white-space: nowrap;
|
|
5602
|
+
text-align: left;
|
|
5603
|
+
}
|
|
5604
|
+
.fig-select-prepend {
|
|
5605
|
+
display: inline-flex;
|
|
5606
|
+
flex: 0 0 auto;
|
|
5607
|
+
align-items: center;
|
|
5608
|
+
margin-right: var(--spacer-1, 0.25rem);
|
|
5609
|
+
pointer-events: none;
|
|
5610
|
+
}
|
|
5611
|
+
.fig-select-prepend:empty {
|
|
5612
|
+
display: none;
|
|
5613
|
+
}
|
|
5614
|
+
/* Listbox chrome from document fig-select::part(listbox).
|
|
5615
|
+
Overflow UI lives on slotted fig-select-options.
|
|
5616
|
+
Never set display except when open — closed <dialog> must stay display:none. */
|
|
5617
|
+
dialog[is="fig-popup"] {
|
|
5618
|
+
flex-direction: column;
|
|
5619
|
+
overflow: hidden;
|
|
5620
|
+
}
|
|
5621
|
+
dialog[is="fig-popup"][open] {
|
|
5622
|
+
display: flex;
|
|
5623
|
+
}
|
|
5624
|
+
::slotted(fig-select-options) {
|
|
5625
|
+
flex: 1 1 auto;
|
|
5626
|
+
min-height: 0;
|
|
5627
|
+
max-height: inherit;
|
|
5628
|
+
}
|
|
5629
|
+
</style>
|
|
5630
|
+
`;
|
|
5631
|
+
|
|
5632
|
+
const button = document.createElement("fig-button");
|
|
5633
|
+
button.className = "fig-select-trigger";
|
|
5634
|
+
button.setAttribute("part", "trigger");
|
|
5635
|
+
button.setAttribute("variant", "ghost");
|
|
5636
|
+
button.setAttribute("aria-haspopup", "listbox");
|
|
5637
|
+
button.setAttribute("aria-expanded", "false");
|
|
5638
|
+
|
|
5639
|
+
const prependEl = document.createElement("span");
|
|
5640
|
+
prependEl.className = "fig-select-prepend";
|
|
5641
|
+
prependEl.setAttribute("part", "prepend");
|
|
5642
|
+
prependEl.setAttribute("aria-hidden", "true");
|
|
5643
|
+
|
|
5644
|
+
const labelEl = document.createElement("span");
|
|
5645
|
+
labelEl.className = "fig-select-label";
|
|
5646
|
+
labelEl.setAttribute("part", "label");
|
|
5647
|
+
button.append(prependEl, labelEl);
|
|
5648
|
+
|
|
5649
|
+
const popup = document.createElement("dialog", { is: "fig-popup" });
|
|
5650
|
+
popup.setAttribute("is", "fig-popup");
|
|
5651
|
+
popup.setAttribute("part", "listbox");
|
|
5652
|
+
popup.setAttribute("theme", "menu");
|
|
5653
|
+
popup.setAttribute("role", "listbox");
|
|
5654
|
+
// Top-layer via popover so the menu escapes ancestor contain/overflow
|
|
5655
|
+
// (e.g. fig-fill-picker-dialog). Stays in shadow so option slots still work —
|
|
5656
|
+
// unlike tooltips, we cannot portal this popup to the overlay root.
|
|
5657
|
+
if ("popover" in HTMLElement.prototype) {
|
|
5658
|
+
popup.setAttribute("popover", "manual");
|
|
5659
|
+
}
|
|
5660
|
+
popup.id = figUniqueId();
|
|
5661
|
+
button.setAttribute("aria-controls", popup.id);
|
|
5662
|
+
|
|
5663
|
+
const panelSlot = document.createElement("slot");
|
|
5664
|
+
panelSlot.setAttribute("name", "panel");
|
|
5665
|
+
popup.appendChild(panelSlot);
|
|
5666
|
+
|
|
5667
|
+
shadow.append(button, popup);
|
|
5668
|
+
|
|
5669
|
+
this.#button = button;
|
|
5670
|
+
this.#prependEl = prependEl;
|
|
5671
|
+
this.#labelEl = labelEl;
|
|
5672
|
+
this.#popup = popup;
|
|
5673
|
+
this.#panelSlot = panelSlot;
|
|
5674
|
+
popup.anchor = button;
|
|
5675
|
+
|
|
5676
|
+
this.#ensurePanelSlotAttrs();
|
|
5677
|
+
this.#setupListeners();
|
|
5678
|
+
this.#installPopupPositioning();
|
|
5679
|
+
|
|
5680
|
+
if (!this.hasAttribute("value")) {
|
|
5681
|
+
const selected = this.#getOptions().find((opt) =>
|
|
5682
|
+
figBooleanAttribute(opt, "selected"),
|
|
5683
|
+
);
|
|
5684
|
+
if (selected) this.setAttribute("value", selected.value);
|
|
5685
|
+
}
|
|
5686
|
+
}
|
|
5687
|
+
|
|
5688
|
+
#installPopupPositioning() {
|
|
5689
|
+
if (!this.#popup || this.#popupPositionPatched) return;
|
|
5690
|
+
if (typeof this.#popup.positionPopup !== "function") return;
|
|
5691
|
+
this.#originalPositionPopup = this.#popup.positionPopup.bind(this.#popup);
|
|
5692
|
+
this.#popup.positionPopup = () => {
|
|
5693
|
+
if (!this.open) {
|
|
5694
|
+
this.#originalPositionPopup?.();
|
|
5695
|
+
return;
|
|
5696
|
+
}
|
|
5697
|
+
this.#positionPopupOverSelected();
|
|
5698
|
+
};
|
|
5699
|
+
this.#popupPositionPatched = true;
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
#getOptionTextRect(option) {
|
|
5703
|
+
if (!option) return null;
|
|
5704
|
+
const range = document.createRange();
|
|
5705
|
+
range.selectNodeContents(option);
|
|
5706
|
+
const rects = [...range.getClientRects()].filter(
|
|
5707
|
+
(rect) => rect.width > 0 && rect.height > 0,
|
|
5708
|
+
);
|
|
5709
|
+
if (rects.length) return rects[0];
|
|
5710
|
+
return option.getBoundingClientRect();
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
#getViewportMargins() {
|
|
5714
|
+
if (typeof this.#popup?.parseViewportMargins === "function") {
|
|
5715
|
+
return this.#popup.parseViewportMargins();
|
|
5716
|
+
}
|
|
5717
|
+
return { top: 8, right: 8, bottom: 8, left: 8 };
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
#readLabelRectSnapshot() {
|
|
5721
|
+
const rect = this.#labelEl?.getBoundingClientRect();
|
|
5722
|
+
if (!rect) return null;
|
|
5723
|
+
return {
|
|
5724
|
+
x: rect.x,
|
|
5725
|
+
y: rect.y,
|
|
5726
|
+
width: rect.width,
|
|
5727
|
+
height: rect.height,
|
|
5728
|
+
};
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5731
|
+
#readViewportSnapshot() {
|
|
5732
|
+
const vv = window.visualViewport;
|
|
5733
|
+
return {
|
|
5734
|
+
width: vv?.width ?? window.innerWidth,
|
|
5735
|
+
height: vv?.height ?? window.innerHeight,
|
|
5736
|
+
offsetLeft: vv?.offsetLeft ?? 0,
|
|
5737
|
+
offsetTop: vv?.offsetTop ?? 0,
|
|
5738
|
+
};
|
|
5739
|
+
}
|
|
5740
|
+
|
|
5741
|
+
#rectSnapshotChanged(prev, next, epsilon = 0.25) {
|
|
5742
|
+
if (!prev && !next) return false;
|
|
5743
|
+
if (!prev || !next) return true;
|
|
5744
|
+
return (
|
|
5745
|
+
Math.abs(prev.x - next.x) > epsilon ||
|
|
5746
|
+
Math.abs(prev.y - next.y) > epsilon ||
|
|
5747
|
+
Math.abs(prev.width - next.width) > epsilon ||
|
|
5748
|
+
Math.abs(prev.height - next.height) > epsilon
|
|
5749
|
+
);
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
#viewportSnapshotChanged(prev, next, epsilon = 0.25) {
|
|
5753
|
+
if (!prev && !next) return false;
|
|
5754
|
+
if (!prev || !next) return true;
|
|
5755
|
+
return (
|
|
5756
|
+
Math.abs(prev.width - next.width) > epsilon ||
|
|
5757
|
+
Math.abs(prev.height - next.height) > epsilon ||
|
|
5758
|
+
Math.abs(prev.offsetLeft - next.offsetLeft) > epsilon ||
|
|
5759
|
+
Math.abs(prev.offsetTop - next.offsetTop) > epsilon
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
|
|
5763
|
+
#shouldSkipFrozenPositionPass() {
|
|
5764
|
+
if (!this.#freezeMenuPosition) return false;
|
|
5765
|
+
const labelMoved = this.#rectSnapshotChanged(
|
|
5766
|
+
this.#frozenLabelRect,
|
|
5767
|
+
this.#readLabelRectSnapshot(),
|
|
5768
|
+
);
|
|
5769
|
+
const viewportChanged = this.#viewportSnapshotChanged(
|
|
5770
|
+
this.#frozenViewport,
|
|
5771
|
+
this.#readViewportSnapshot(),
|
|
5772
|
+
);
|
|
5773
|
+
// Skip only when neither the trigger nor the viewport moved — typical of
|
|
5774
|
+
// overflow scroll / content sync fighting the open-time alignment.
|
|
5775
|
+
return !labelMoved && !viewportChanged;
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
#rememberFrozenGeometry() {
|
|
5779
|
+
this.#frozenLabelRect = this.#readLabelRectSnapshot();
|
|
5780
|
+
this.#frozenViewport = this.#readViewportSnapshot();
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
#positionPopupOverSelected() {
|
|
5784
|
+
// Content ResizeObserver / overflow scroll re-enter here; keep the
|
|
5785
|
+
// open-time alignment unless the trigger or viewport actually changed.
|
|
5786
|
+
if (this.#shouldSkipFrozenPositionPass()) return;
|
|
5787
|
+
|
|
5788
|
+
const popup = this.#popup;
|
|
5789
|
+
const label = this.#labelEl;
|
|
5790
|
+
if (!popup || !label) {
|
|
5791
|
+
this.#originalPositionPopup?.();
|
|
5792
|
+
return;
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
const options = this.#getOptions();
|
|
5796
|
+
const selected =
|
|
5797
|
+
options.find((opt) => this.#optionValue(opt) === this.value) ||
|
|
5798
|
+
options[0];
|
|
5799
|
+
if (!selected) {
|
|
5800
|
+
this.#originalPositionPopup?.();
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
// Lay out with the default positioning first so option metrics are valid.
|
|
5805
|
+
this.#originalPositionPopup?.();
|
|
5806
|
+
|
|
5807
|
+
const popupRect = popup.getBoundingClientRect();
|
|
5808
|
+
const labelRect = label.getBoundingClientRect();
|
|
5809
|
+
const optionTextRect = this.#getOptionTextRect(selected);
|
|
5810
|
+
if (
|
|
5811
|
+
!popupRect.width ||
|
|
5812
|
+
!popupRect.height ||
|
|
5813
|
+
!labelRect.width ||
|
|
5814
|
+
!optionTextRect
|
|
5815
|
+
) {
|
|
5816
|
+
return;
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
const selectedOffsetX = optionTextRect.left - popupRect.left;
|
|
5820
|
+
const selectedOffsetY = optionTextRect.top - popupRect.top;
|
|
5821
|
+
const full = figBooleanAttribute(this, "full");
|
|
5822
|
+
// [full]: pin menu to host width/edges. Otherwise overlay selected
|
|
5823
|
+
// option text on the trigger label (blend-mode style).
|
|
5824
|
+
let left = full
|
|
5825
|
+
? this.getBoundingClientRect().left
|
|
5826
|
+
: labelRect.left - selectedOffsetX;
|
|
5827
|
+
let top = labelRect.top - selectedOffsetY;
|
|
5828
|
+
|
|
5829
|
+
// Keep the whole menu in-view when aligning over the selected option
|
|
5830
|
+
// would otherwise push it past a viewport edge (corners / far sides).
|
|
5831
|
+
const margins = this.#getViewportMargins();
|
|
5832
|
+
if (typeof popup.clampToViewport === "function") {
|
|
5833
|
+
({ left, top } = popup.clampToViewport({ left, top }, popupRect, margins));
|
|
5834
|
+
} else {
|
|
5835
|
+
const minLeft = margins.left;
|
|
5836
|
+
const minTop = margins.top;
|
|
5837
|
+
const maxLeft = window.innerWidth - popupRect.width - margins.right;
|
|
5838
|
+
const maxTop = window.innerHeight - popupRect.height - margins.bottom;
|
|
5839
|
+
left = Math.min(Math.max(left, minLeft), Math.max(minLeft, maxLeft));
|
|
5840
|
+
top = Math.min(Math.max(top, minTop), Math.max(minTop, maxTop));
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
// !important: fig-select::part(listbox) and dialog UA rules can otherwise
|
|
5844
|
+
// keep the menu at its static/anchor position past the viewport edge.
|
|
5845
|
+
popup.style.setProperty("right", "auto", "important");
|
|
5846
|
+
popup.style.setProperty("bottom", "auto", "important");
|
|
5847
|
+
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
5848
|
+
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
5849
|
+
|
|
5850
|
+
// Nudge the panel scroller so the selected label stays over the trigger.
|
|
5851
|
+
const panel = this.#getPanel();
|
|
5852
|
+
const alignedTextRect = this.#getOptionTextRect(selected);
|
|
5853
|
+
if (
|
|
5854
|
+
alignedTextRect &&
|
|
5855
|
+
panel &&
|
|
5856
|
+
panel.scrollHeight > panel.clientHeight + 1
|
|
5857
|
+
) {
|
|
5858
|
+
const deltaY = alignedTextRect.top - labelRect.top;
|
|
5859
|
+
if (Math.abs(deltaY) > 0.5) {
|
|
5860
|
+
panel.scrollTop += deltaY;
|
|
5861
|
+
}
|
|
5862
|
+
panel.syncOverflow?.();
|
|
5863
|
+
}
|
|
5864
|
+
|
|
5865
|
+
if (this.#freezeMenuPosition || this.open) {
|
|
5866
|
+
this.#rememberFrozenGeometry();
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5870
|
+
#setupListeners() {
|
|
5871
|
+
this.#button?.addEventListener("click", this.#boundTriggerClick);
|
|
5872
|
+
this.#button?.addEventListener("keydown", this.#boundKeydown);
|
|
5873
|
+
// Host click: slotted options stay in light DOM (not dialog.contains).
|
|
5874
|
+
this.addEventListener("click", this.#boundOptionClick);
|
|
5875
|
+
this.#popup?.addEventListener("keydown", this.#boundKeydown);
|
|
5876
|
+
this.#popup?.addEventListener("close", this.#boundPopupClose);
|
|
5877
|
+
this.#panelSlot?.addEventListener("slotchange", this.#boundSlotChange);
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
#teardownListeners() {
|
|
5881
|
+
this.#button?.removeEventListener("click", this.#boundTriggerClick);
|
|
5882
|
+
this.#button?.removeEventListener("keydown", this.#boundKeydown);
|
|
5883
|
+
this.removeEventListener("click", this.#boundOptionClick);
|
|
5884
|
+
this.#popup?.removeEventListener("keydown", this.#boundKeydown);
|
|
5885
|
+
this.#popup?.removeEventListener("close", this.#boundPopupClose);
|
|
5886
|
+
this.#panelSlot?.removeEventListener("slotchange", this.#boundSlotChange);
|
|
5887
|
+
}
|
|
5888
|
+
|
|
5889
|
+
#handleSlotChange() {
|
|
5890
|
+
this.#ensurePanelSlotAttrs();
|
|
5891
|
+
this.#syncValue();
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
#setupObserver() {
|
|
5895
|
+
if (this.#observer) return;
|
|
5896
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
5897
|
+
if (this.#syncingValue || this.#syncingOptions) return;
|
|
5898
|
+
let needsSync = false;
|
|
5899
|
+
for (const mutation of mutations) {
|
|
5900
|
+
if (mutation.type === "childList") {
|
|
5901
|
+
if (
|
|
5902
|
+
[...mutation.addedNodes].some((node) => this.#isMenuChild(node)) ||
|
|
5903
|
+
[...mutation.removedNodes].some((node) => this.#isMenuChild(node)) ||
|
|
5904
|
+
mutation.target?.closest?.("fig-select-option")
|
|
5905
|
+
) {
|
|
5906
|
+
needsSync = true;
|
|
5907
|
+
}
|
|
5908
|
+
}
|
|
5909
|
+
if (
|
|
5910
|
+
mutation.type === "attributes" &&
|
|
5911
|
+
mutation.target?.tagName === "FIG-SELECT-OPTION" &&
|
|
5912
|
+
(mutation.attributeName === "value" ||
|
|
5913
|
+
mutation.attributeName === "disabled" ||
|
|
5914
|
+
mutation.attributeName === "label")
|
|
5915
|
+
) {
|
|
5916
|
+
needsSync = true;
|
|
5917
|
+
}
|
|
5918
|
+
if (
|
|
5919
|
+
mutation.type === "characterData" &&
|
|
5920
|
+
mutation.target?.parentElement?.tagName === "FIG-SELECT-OPTION"
|
|
5921
|
+
) {
|
|
5922
|
+
needsSync = true;
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
if (needsSync) this.#syncValue();
|
|
5926
|
+
});
|
|
5927
|
+
this.#observer.observe(this, {
|
|
5928
|
+
childList: true,
|
|
5929
|
+
subtree: true,
|
|
5930
|
+
characterData: true,
|
|
5931
|
+
attributes: true,
|
|
5932
|
+
attributeFilter: ["value", "disabled", "selected", "label"],
|
|
5933
|
+
});
|
|
5934
|
+
}
|
|
5935
|
+
|
|
5936
|
+
#getOptions({ enabledOnly = false } = {}) {
|
|
5937
|
+
const panel = this.#getPanel();
|
|
5938
|
+
const options = panel
|
|
5939
|
+
? Array.from(panel.querySelectorAll(":scope > fig-select-option"))
|
|
5940
|
+
: [];
|
|
5941
|
+
if (!enabledOnly) return options;
|
|
5942
|
+
return options.filter((opt) => !figBooleanAttribute(opt, "disabled"));
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5945
|
+
#optionValue(option) {
|
|
5946
|
+
if (!option) return "";
|
|
5947
|
+
if (typeof option.value === "string") return option.value;
|
|
5948
|
+
const attr = option.getAttribute?.("value");
|
|
5949
|
+
if (attr != null) return attr;
|
|
5950
|
+
return (option.textContent || "").trim();
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
#optionLabel(option) {
|
|
5954
|
+
if (!option) return "";
|
|
5955
|
+
const labelAttr = option.getAttribute?.("label");
|
|
5956
|
+
if (labelAttr != null && labelAttr !== "") return labelAttr.trim();
|
|
5957
|
+
|
|
5958
|
+
// Ignore prepend/append slot content when deriving a label from children.
|
|
5959
|
+
const parts = [];
|
|
5960
|
+
for (const node of option.childNodes) {
|
|
5961
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
5962
|
+
const text = node.textContent?.trim();
|
|
5963
|
+
if (text) parts.push(text);
|
|
5964
|
+
continue;
|
|
5965
|
+
}
|
|
5966
|
+
if (!(node instanceof Element)) continue;
|
|
5967
|
+
const slot = node.getAttribute("slot");
|
|
5968
|
+
if (slot === "prepend" || slot === "append") continue;
|
|
5969
|
+
const text = node.textContent?.trim();
|
|
5970
|
+
if (text) parts.push(text);
|
|
5971
|
+
}
|
|
5972
|
+
if (parts.length) return parts.join(" ").trim();
|
|
5973
|
+
return (option.textContent || "").trim();
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
#syncPrepend(option) {
|
|
5977
|
+
if (!this.#prependEl) return;
|
|
5978
|
+
const source = option?.querySelector?.(':scope > [slot="prepend"]');
|
|
5979
|
+
this.#prependEl.replaceChildren(
|
|
5980
|
+
...Array.from(source?.childNodes ?? [], (node) => node.cloneNode(true)),
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
#syncPopupAttrs() {
|
|
5985
|
+
if (!this.#popup) return;
|
|
5986
|
+
this.#popup.setAttribute(
|
|
5987
|
+
"position",
|
|
5988
|
+
this.getAttribute("position") || "bottom left",
|
|
5989
|
+
);
|
|
5990
|
+
const offset = this.getAttribute("offset");
|
|
5991
|
+
if (offset) this.#popup.setAttribute("offset", offset);
|
|
5992
|
+
else this.#popup.removeAttribute("offset");
|
|
5993
|
+
const closedby = this.getAttribute("closedby");
|
|
5994
|
+
if (closedby) this.#popup.setAttribute("closedby", closedby);
|
|
5995
|
+
else this.#popup.removeAttribute("closedby");
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5998
|
+
#syncDisabled() {
|
|
5999
|
+
const disabled = figBooleanAttribute(this, "disabled");
|
|
6000
|
+
if (this.#button) {
|
|
6001
|
+
if (disabled) this.#button.setAttribute("disabled", "");
|
|
6002
|
+
else this.#button.removeAttribute("disabled");
|
|
6003
|
+
}
|
|
6004
|
+
if (disabled && this.open) this.open = false;
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
#pickFallbackOption(options) {
|
|
6008
|
+
if (!options.length) return null;
|
|
6009
|
+
const selected = options.find((opt) =>
|
|
6010
|
+
figBooleanAttribute(opt, "selected"),
|
|
6011
|
+
);
|
|
6012
|
+
if (selected && !figBooleanAttribute(selected, "disabled")) {
|
|
6013
|
+
return selected;
|
|
6014
|
+
}
|
|
6015
|
+
return (
|
|
6016
|
+
options.find((opt) => !figBooleanAttribute(opt, "disabled")) ||
|
|
6017
|
+
options[0] ||
|
|
6018
|
+
null
|
|
6019
|
+
);
|
|
6020
|
+
}
|
|
6021
|
+
|
|
6022
|
+
#emitValueEvents(value) {
|
|
6023
|
+
this.dispatchEvent(
|
|
6024
|
+
new CustomEvent("input", {
|
|
6025
|
+
detail: value,
|
|
6026
|
+
bubbles: true,
|
|
6027
|
+
composed: true,
|
|
6028
|
+
}),
|
|
6029
|
+
);
|
|
6030
|
+
this.dispatchEvent(
|
|
6031
|
+
new CustomEvent("change", {
|
|
6032
|
+
detail: value,
|
|
6033
|
+
bubbles: true,
|
|
6034
|
+
composed: true,
|
|
6035
|
+
}),
|
|
6036
|
+
);
|
|
6037
|
+
}
|
|
6038
|
+
|
|
6039
|
+
#syncValue() {
|
|
6040
|
+
if (this.#syncingValue) return;
|
|
6041
|
+
this.#syncingValue = true;
|
|
6042
|
+
try {
|
|
6043
|
+
const options = this.#getOptions();
|
|
6044
|
+
const hasValueAttr = this.hasAttribute("value");
|
|
6045
|
+
const previousValue = hasValueAttr ? this.getAttribute("value") : null;
|
|
6046
|
+
let match = hasValueAttr
|
|
6047
|
+
? options.find((opt) => this.#optionValue(opt) === previousValue)
|
|
6048
|
+
: null;
|
|
6049
|
+
let valueCorrected = false;
|
|
6050
|
+
|
|
6051
|
+
if (!match) {
|
|
6052
|
+
if (hasValueAttr) {
|
|
6053
|
+
// Options may not be built yet (options attr sync). Keep value until then.
|
|
6054
|
+
if (!options.length) {
|
|
6055
|
+
if (this.#labelEl) {
|
|
6056
|
+
this.#labelEl.textContent =
|
|
6057
|
+
previousValue || this.getAttribute("label") || "";
|
|
6058
|
+
}
|
|
6059
|
+
return;
|
|
6060
|
+
}
|
|
6061
|
+
// Value orphaned (option removed / value attr changed) — clamp or clear.
|
|
6062
|
+
match = this.#pickFallbackOption(options);
|
|
6063
|
+
if (match) {
|
|
6064
|
+
const nextValue = this.#optionValue(match);
|
|
6065
|
+
if (previousValue !== nextValue) {
|
|
6066
|
+
this.setAttribute("value", nextValue);
|
|
6067
|
+
valueCorrected = true;
|
|
6068
|
+
}
|
|
6069
|
+
} else {
|
|
6070
|
+
this.removeAttribute("value");
|
|
6071
|
+
valueCorrected = true;
|
|
6072
|
+
}
|
|
6073
|
+
} else {
|
|
6074
|
+
// No host value yet — honor a selected option if present.
|
|
6075
|
+
match = options.find((opt) =>
|
|
6076
|
+
figBooleanAttribute(opt, "selected"),
|
|
6077
|
+
);
|
|
6078
|
+
if (match) {
|
|
6079
|
+
this.setAttribute("value", this.#optionValue(match));
|
|
6080
|
+
valueCorrected = true;
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
|
|
6085
|
+
for (const opt of options) {
|
|
6086
|
+
const selected = opt === match;
|
|
6087
|
+
opt.setAttribute("aria-selected", selected ? "true" : "false");
|
|
6088
|
+
if (selected) opt.setAttribute("selected", "");
|
|
6089
|
+
else opt.removeAttribute("selected");
|
|
6090
|
+
}
|
|
6091
|
+
|
|
6092
|
+
const label =
|
|
6093
|
+
(match && this.#optionLabel(match)) || this.getAttribute("label") || "";
|
|
6094
|
+
if (this.#labelEl) this.#labelEl.textContent = label;
|
|
6095
|
+
this.#syncPrepend(match);
|
|
6096
|
+
|
|
6097
|
+
const ariaLabel = this.getAttribute("label") || "Select";
|
|
6098
|
+
this.#button?.setAttribute("aria-label", ariaLabel);
|
|
6099
|
+
|
|
6100
|
+
// Don't scrollToOption while open — reposition/sync would fight overflow paging.
|
|
6101
|
+
this.#getPanel()?.syncOverflow?.();
|
|
6102
|
+
|
|
6103
|
+
if (valueCorrected) {
|
|
6104
|
+
this.#emitValueEvents(this.getAttribute("value") ?? "");
|
|
6105
|
+
}
|
|
6106
|
+
} finally {
|
|
6107
|
+
this.#syncingValue = false;
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
|
|
6111
|
+
#handleTriggerClick(e) {
|
|
6112
|
+
if (figBooleanAttribute(this, "disabled")) return;
|
|
6113
|
+
e.preventDefault();
|
|
6114
|
+
e.stopPropagation();
|
|
6115
|
+
const nextOpen = !this.open;
|
|
6116
|
+
if (nextOpen && this.#popup && this.#button) {
|
|
6117
|
+
this.#popup.anchor = this.#button;
|
|
6118
|
+
}
|
|
6119
|
+
this.open = nextOpen;
|
|
6120
|
+
}
|
|
6121
|
+
|
|
6122
|
+
#handleOptionClick(e) {
|
|
6123
|
+
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
6124
|
+
const option = path.find(
|
|
6125
|
+
(node) => node?.tagName === "FIG-SELECT-OPTION",
|
|
6126
|
+
);
|
|
6127
|
+
if (!option || !this.contains(option)) return;
|
|
6128
|
+
if (figBooleanAttribute(option, "disabled")) return;
|
|
6129
|
+
// Do not stopPropagation — React light-DOM onClick must still fire.
|
|
6130
|
+
this.#selectOption(option);
|
|
6131
|
+
}
|
|
6132
|
+
|
|
6133
|
+
#handleKeydown(e) {
|
|
6134
|
+
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
6135
|
+
|
|
6136
|
+
const listOpen = this.open && (this.#popup?.matches?.(":open") ?? false);
|
|
6137
|
+
if (!listOpen) {
|
|
6138
|
+
if (
|
|
6139
|
+
this.#button?.contains(e.target) &&
|
|
6140
|
+
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")
|
|
6141
|
+
) {
|
|
6142
|
+
e.preventDefault();
|
|
6143
|
+
if (this.#popup && this.#button) this.#popup.anchor = this.#button;
|
|
6144
|
+
this.open = true;
|
|
6145
|
+
requestAnimationFrame(() => {
|
|
6146
|
+
const options = this.#getOptions({ enabledOnly: true });
|
|
6147
|
+
const selectedIndex = options.findIndex(
|
|
6148
|
+
(opt) => this.#optionValue(opt) === this.value,
|
|
6149
|
+
);
|
|
6150
|
+
this.#focusOptionAt(selectedIndex >= 0 ? selectedIndex : 0);
|
|
6151
|
+
});
|
|
6152
|
+
}
|
|
6153
|
+
return;
|
|
6154
|
+
}
|
|
6155
|
+
|
|
6156
|
+
const options = this.#getOptions({ enabledOnly: true });
|
|
6157
|
+
if (!options.length) return;
|
|
6158
|
+
|
|
6159
|
+
switch (e.key) {
|
|
6160
|
+
case "ArrowDown":
|
|
6161
|
+
e.preventDefault();
|
|
6162
|
+
this.#syncFocusedIndex();
|
|
6163
|
+
this.#focusOptionAt(this.#focusedIndex + 1);
|
|
6164
|
+
break;
|
|
6165
|
+
case "ArrowUp":
|
|
6166
|
+
e.preventDefault();
|
|
6167
|
+
this.#syncFocusedIndex();
|
|
6168
|
+
this.#focusOptionAt(this.#focusedIndex - 1);
|
|
6169
|
+
break;
|
|
6170
|
+
case "Home":
|
|
6171
|
+
e.preventDefault();
|
|
6172
|
+
this.#focusOptionAt(0);
|
|
6173
|
+
break;
|
|
6174
|
+
case "End":
|
|
6175
|
+
e.preventDefault();
|
|
6176
|
+
this.#focusOptionAt(options.length - 1);
|
|
6177
|
+
break;
|
|
6178
|
+
case "Escape":
|
|
6179
|
+
e.preventDefault();
|
|
6180
|
+
this.open = false;
|
|
6181
|
+
this.#button?.focus();
|
|
6182
|
+
break;
|
|
6183
|
+
case "Enter":
|
|
6184
|
+
case " ": {
|
|
6185
|
+
this.#syncFocusedIndex();
|
|
6186
|
+
const focused = options[this.#focusedIndex];
|
|
6187
|
+
if (!focused) return;
|
|
6188
|
+
e.preventDefault();
|
|
6189
|
+
this.#selectOption(focused);
|
|
6190
|
+
break;
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
|
|
6195
|
+
#handlePopupClose() {
|
|
6196
|
+
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
6197
|
+
this.#button?.setAttribute("aria-expanded", "false");
|
|
6198
|
+
this.#button?.focus();
|
|
6199
|
+
this.#focusedIndex = -1;
|
|
5067
6200
|
}
|
|
5068
6201
|
|
|
5069
|
-
#
|
|
5070
|
-
this.#
|
|
5071
|
-
this
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
this
|
|
6202
|
+
#selectOption(option) {
|
|
6203
|
+
const value = this.#optionValue(option);
|
|
6204
|
+
this.setAttribute("value", value);
|
|
6205
|
+
this.#syncValue();
|
|
6206
|
+
this.#emitValueEvents(value);
|
|
6207
|
+
this.open = false;
|
|
5075
6208
|
}
|
|
5076
6209
|
|
|
5077
|
-
#
|
|
5078
|
-
|
|
5079
|
-
range.selectNodeContents(seg);
|
|
5080
|
-
const textWidth = range.getBoundingClientRect().width;
|
|
5081
|
-
const segRect = seg.getBoundingClientRect();
|
|
5082
|
-
const segWidth = segRect.width;
|
|
5083
|
-
const cs = getComputedStyle(seg);
|
|
5084
|
-
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
5085
|
-
const padR = parseFloat(cs.paddingRight) || 0;
|
|
5086
|
-
const contentWidth = segWidth - padL - padR;
|
|
5087
|
-
return textWidth > contentWidth + 0.5;
|
|
6210
|
+
#getEnabledOptions() {
|
|
6211
|
+
return this.#getOptions({ enabledOnly: true });
|
|
5088
6212
|
}
|
|
5089
6213
|
|
|
5090
|
-
#
|
|
5091
|
-
const
|
|
5092
|
-
|
|
5093
|
-
|
|
6214
|
+
#syncFocusedIndex() {
|
|
6215
|
+
const options = this.#getEnabledOptions();
|
|
6216
|
+
if (!options.length) {
|
|
6217
|
+
this.#focusedIndex = -1;
|
|
6218
|
+
return;
|
|
5094
6219
|
}
|
|
5095
|
-
|
|
6220
|
+
const active = options.find((opt) => opt === document.activeElement);
|
|
6221
|
+
const index = active ? options.indexOf(active) : -1;
|
|
6222
|
+
this.#focusedIndex = index >= 0 ? index : this.#focusedIndex;
|
|
6223
|
+
}
|
|
6224
|
+
|
|
6225
|
+
#focusOptionAt(index) {
|
|
6226
|
+
const options = this.#getEnabledOptions();
|
|
6227
|
+
if (!options.length) return;
|
|
6228
|
+
const next = ((index % options.length) + options.length) % options.length;
|
|
6229
|
+
this.#focusedIndex = next;
|
|
6230
|
+
options[next]?.focus();
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
#syncPopupWidth() {
|
|
6234
|
+
if (!this.#popup || !this.#button) return;
|
|
6235
|
+
const hostWidth = Math.ceil(this.getBoundingClientRect().width);
|
|
6236
|
+
const triggerWidth = Math.ceil(this.#button.getBoundingClientRect().width);
|
|
6237
|
+
const anchorWidth = Math.max(hostWidth, triggerWidth, 96);
|
|
6238
|
+
|
|
6239
|
+
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
6240
|
+
// Menus size to their options while remaining at least as wide as the trigger.
|
|
6241
|
+
this.#popup.style.setProperty("width", "max-content", "important");
|
|
6242
|
+
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
6243
|
+
this.#popup.style.setProperty(
|
|
6244
|
+
"max-width",
|
|
6245
|
+
"min(20rem, calc(100vw - 1rem))",
|
|
6246
|
+
"important",
|
|
6247
|
+
);
|
|
5096
6248
|
}
|
|
5097
6249
|
|
|
5098
|
-
#
|
|
5099
|
-
if (this.#
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
6250
|
+
#openList() {
|
|
6251
|
+
if (!this.#popup || figBooleanAttribute(this, "disabled")) return;
|
|
6252
|
+
if (this.#button) this.#popup.anchor = this.#button;
|
|
6253
|
+
this.#installPopupPositioning();
|
|
6254
|
+
this.#freezeMenuPosition = false;
|
|
6255
|
+
this.#frozenLabelRect = null;
|
|
6256
|
+
this.#frozenViewport = null;
|
|
6257
|
+
this.#syncValue();
|
|
6258
|
+
this.#syncPopupWidth();
|
|
6259
|
+
this.#popup.open = true;
|
|
6260
|
+
document.addEventListener("keydown", this.#boundKeydown, true);
|
|
6261
|
+
this.#button?.setAttribute("aria-expanded", "true");
|
|
6262
|
+
this.#focusedIndex = -1;
|
|
6263
|
+
requestAnimationFrame(() => {
|
|
6264
|
+
this.#syncPopupWidth();
|
|
6265
|
+
this.#positionPopupOverSelected();
|
|
6266
|
+
const panel = this.#getPanel();
|
|
6267
|
+
const options = this.#getEnabledOptions();
|
|
6268
|
+
const selectedIndex = options.findIndex(
|
|
6269
|
+
(opt) => this.#optionValue(opt) === this.value,
|
|
6270
|
+
);
|
|
6271
|
+
if (selectedIndex >= 0) {
|
|
6272
|
+
this.#focusOptionAt(selectedIndex);
|
|
6273
|
+
} else if (
|
|
6274
|
+
this.#button?.hasAttribute("data-focus-visible") ||
|
|
6275
|
+
this.#button?.matches?.(":focus-visible")
|
|
6276
|
+
) {
|
|
6277
|
+
this.#focusOptionAt(0);
|
|
5120
6278
|
}
|
|
5121
|
-
|
|
6279
|
+
panel?.syncOverflow?.();
|
|
6280
|
+
// Freeze after open align so later positionPopup passes don't undo scroll.
|
|
6281
|
+
// Window resize / trigger movement still realigns via geometry checks.
|
|
6282
|
+
this.#freezeMenuPosition = true;
|
|
6283
|
+
this.#rememberFrozenGeometry();
|
|
6284
|
+
});
|
|
6285
|
+
}
|
|
6286
|
+
|
|
6287
|
+
#closeList() {
|
|
6288
|
+
if (!this.#popup) return;
|
|
6289
|
+
this.#freezeMenuPosition = false;
|
|
6290
|
+
this.#frozenLabelRect = null;
|
|
6291
|
+
this.#frozenViewport = null;
|
|
6292
|
+
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
6293
|
+
this.#popup.open = false;
|
|
6294
|
+
this.#button?.setAttribute("aria-expanded", "false");
|
|
5122
6295
|
}
|
|
5123
6296
|
}
|
|
5124
|
-
figDefineElement("fig-
|
|
6297
|
+
figDefineElement("fig-select", FigSelect);
|
|
5125
6298
|
|
|
5126
6299
|
/* Slider */
|
|
5127
6300
|
/**
|
|
@@ -5823,6 +6996,8 @@ figDefineElement("fig-slider", FigSlider);
|
|
|
5823
6996
|
class FigInputText extends HTMLElement {
|
|
5824
6997
|
#isInteracting = false;
|
|
5825
6998
|
#passwordVisible = false;
|
|
6999
|
+
#value = "";
|
|
7000
|
+
#reflectingValue = false;
|
|
5826
7001
|
#boundMouseMove;
|
|
5827
7002
|
#boundMouseUp;
|
|
5828
7003
|
#boundWindowBlur;
|
|
@@ -5866,7 +7041,9 @@ class FigInputText extends HTMLElement {
|
|
|
5866
7041
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
5867
7042
|
);
|
|
5868
7043
|
};
|
|
5869
|
-
this.#boundFocusControl =
|
|
7044
|
+
this.#boundFocusControl = () => {
|
|
7045
|
+
if (!this.disabled) this.focus();
|
|
7046
|
+
};
|
|
5870
7047
|
this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
|
|
5871
7048
|
}
|
|
5872
7049
|
|
|
@@ -5907,6 +7084,7 @@ class FigInputText extends HTMLElement {
|
|
|
5907
7084
|
this.#syncSearchClear();
|
|
5908
7085
|
this.#syncSearchClearVisibility();
|
|
5909
7086
|
this.#syncPasswordToggle();
|
|
7087
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
5910
7088
|
figNormalizeTextOnlyInputSlots(this);
|
|
5911
7089
|
this.#startObserver();
|
|
5912
7090
|
|
|
@@ -5982,6 +7160,7 @@ class FigInputText extends HTMLElement {
|
|
|
5982
7160
|
#handleAdornmentClick(event) {
|
|
5983
7161
|
const adornment = event.target?.closest?.("[slot]");
|
|
5984
7162
|
if (!adornment || adornment.parentElement !== this) return;
|
|
7163
|
+
if (this.disabled) return;
|
|
5985
7164
|
this.focus();
|
|
5986
7165
|
}
|
|
5987
7166
|
#startObserver() {
|
|
@@ -6013,6 +7192,7 @@ class FigInputText extends HTMLElement {
|
|
|
6013
7192
|
this.#syncSearchClear();
|
|
6014
7193
|
this.#syncSearchClearVisibility();
|
|
6015
7194
|
this.#syncPasswordToggle();
|
|
7195
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6016
7196
|
figNormalizeTextOnlyInputSlots(this);
|
|
6017
7197
|
}
|
|
6018
7198
|
#syncInputA11yAttributes() {
|
|
@@ -6097,6 +7277,7 @@ class FigInputText extends HTMLElement {
|
|
|
6097
7277
|
button.addEventListener("click", (e) => {
|
|
6098
7278
|
e.preventDefault();
|
|
6099
7279
|
e.stopPropagation();
|
|
7280
|
+
if (this.disabled) return;
|
|
6100
7281
|
if (!this.input || this.input.value === "") {
|
|
6101
7282
|
this.focus();
|
|
6102
7283
|
return;
|
|
@@ -6155,6 +7336,7 @@ class FigInputText extends HTMLElement {
|
|
|
6155
7336
|
button.addEventListener("click", (e) => {
|
|
6156
7337
|
e.preventDefault();
|
|
6157
7338
|
e.stopPropagation();
|
|
7339
|
+
if (this.disabled) return;
|
|
6158
7340
|
this.#passwordVisible = !this.#passwordVisible;
|
|
6159
7341
|
if (this.input) {
|
|
6160
7342
|
this.input.type = this.#passwordVisible ? "text" : "password";
|
|
@@ -6172,6 +7354,14 @@ class FigInputText extends HTMLElement {
|
|
|
6172
7354
|
button?.setAttribute("aria-label", label);
|
|
6173
7355
|
icon?.setAttribute("name", this.#passwordVisible ? "visible" : "hidden");
|
|
6174
7356
|
}
|
|
7357
|
+
#syncGeneratedAdornmentDisabled() {
|
|
7358
|
+
this.querySelectorAll(
|
|
7359
|
+
'[data-generated="search-clear"] fig-button, [data-generated="password-toggle"] fig-button',
|
|
7360
|
+
).forEach((button) => {
|
|
7361
|
+
if (this.disabled) button.setAttribute("disabled", "");
|
|
7362
|
+
else button.removeAttribute("disabled");
|
|
7363
|
+
});
|
|
7364
|
+
}
|
|
6175
7365
|
#transformNumber(value) {
|
|
6176
7366
|
if (value === "") return "";
|
|
6177
7367
|
let transformed = Number(value) * (this.transform || 1);
|
|
@@ -6275,15 +7465,29 @@ class FigInputText extends HTMLElement {
|
|
|
6275
7465
|
return Number.isInteger(rounded) ? rounded : rounded.toFixed(precision);
|
|
6276
7466
|
}
|
|
6277
7467
|
|
|
6278
|
-
/*
|
|
6279
7468
|
get value() {
|
|
6280
|
-
return this
|
|
7469
|
+
return this.#value;
|
|
6281
7470
|
}
|
|
6282
7471
|
|
|
6283
7472
|
set value(val) {
|
|
6284
|
-
|
|
6285
|
-
this
|
|
6286
|
-
|
|
7473
|
+
const value = val ?? "";
|
|
7474
|
+
this.#value = value;
|
|
7475
|
+
const reflected = String(value);
|
|
7476
|
+
if (this.getAttribute("value") !== reflected) {
|
|
7477
|
+
this.#reflectingValue = true;
|
|
7478
|
+
this.setAttribute("value", reflected);
|
|
7479
|
+
this.#reflectingValue = false;
|
|
7480
|
+
}
|
|
7481
|
+
this.#syncRenderedValue(value);
|
|
7482
|
+
}
|
|
7483
|
+
|
|
7484
|
+
#syncRenderedValue(value) {
|
|
7485
|
+
if (!this.input || this.#isInteracting) return;
|
|
7486
|
+
const rendered =
|
|
7487
|
+
this.type === "number" ? String(this.#transformNumber(value)) : String(value ?? "");
|
|
7488
|
+
if (this.input.value !== rendered) this.input.value = rendered;
|
|
7489
|
+
this.#syncSearchClearVisibility();
|
|
7490
|
+
}
|
|
6287
7491
|
|
|
6288
7492
|
static get observedAttributes() {
|
|
6289
7493
|
return [
|
|
@@ -6313,6 +7517,7 @@ class FigInputText extends HTMLElement {
|
|
|
6313
7517
|
case "disabled":
|
|
6314
7518
|
this.disabled = this.input.disabled =
|
|
6315
7519
|
newValue !== null && newValue !== "false";
|
|
7520
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6316
7521
|
break;
|
|
6317
7522
|
case "readonly":
|
|
6318
7523
|
this.readonly = newValue !== null && newValue !== "false";
|
|
@@ -6329,13 +7534,9 @@ class FigInputText extends HTMLElement {
|
|
|
6329
7534
|
let value = newValue;
|
|
6330
7535
|
if (this.type === "number") {
|
|
6331
7536
|
value = this.#sanitizeInput(value, false);
|
|
6332
|
-
this.value = value;
|
|
6333
|
-
this.input.value = this.#transformNumber(value);
|
|
6334
|
-
} else {
|
|
6335
|
-
this.value = value;
|
|
6336
|
-
this.input.value = value;
|
|
6337
7537
|
}
|
|
6338
|
-
this.#
|
|
7538
|
+
this.#value = value ?? "";
|
|
7539
|
+
if (!this.#reflectingValue) this.#syncRenderedValue(this.#value);
|
|
6339
7540
|
break;
|
|
6340
7541
|
case "min":
|
|
6341
7542
|
case "max":
|
|
@@ -6378,6 +7579,7 @@ class FigInputText extends HTMLElement {
|
|
|
6378
7579
|
this.#syncSearchClear();
|
|
6379
7580
|
this.#syncSearchClearVisibility();
|
|
6380
7581
|
this.#syncPasswordToggle();
|
|
7582
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6381
7583
|
break;
|
|
6382
7584
|
case "multiline": {
|
|
6383
7585
|
const next = newValue !== null && newValue !== "false";
|
|
@@ -7405,10 +8607,12 @@ class FigInputColor extends HTMLElement {
|
|
|
7405
8607
|
|
|
7406
8608
|
#fillPickerAttrs() {
|
|
7407
8609
|
const attrs = {};
|
|
7408
|
-
const experimental = this.getAttribute("experimental");
|
|
7409
|
-
if (experimental) attrs["experimental"] = experimental;
|
|
7410
8610
|
for (const { name, value } of this.attributes) {
|
|
7411
|
-
if (
|
|
8611
|
+
if (
|
|
8612
|
+
name.startsWith("picker-") &&
|
|
8613
|
+
name !== "picker-anchor" &&
|
|
8614
|
+
name !== "picker-experimental"
|
|
8615
|
+
) {
|
|
7412
8616
|
attrs[name.slice(7)] = value;
|
|
7413
8617
|
}
|
|
7414
8618
|
}
|
|
@@ -7615,8 +8819,8 @@ class FigInputColor extends HTMLElement {
|
|
|
7615
8819
|
|
|
7616
8820
|
const picker = document.createElement("fig-fill-picker");
|
|
7617
8821
|
picker.innerHTML = "<span hidden></span>";
|
|
7618
|
-
picker.addEventListener("input", this.#
|
|
7619
|
-
picker.addEventListener("change", this.#
|
|
8822
|
+
picker.addEventListener("input", this.#boundFillPickerInput);
|
|
8823
|
+
picker.addEventListener("change", this.#boundChange);
|
|
7620
8824
|
this.appendChild(picker);
|
|
7621
8825
|
this.#fillPicker = picker;
|
|
7622
8826
|
this.#syncFillPicker();
|
|
@@ -7681,8 +8885,22 @@ class FigInputColor extends HTMLElement {
|
|
|
7681
8885
|
}
|
|
7682
8886
|
|
|
7683
8887
|
#setValues(hexValue) {
|
|
7684
|
-
|
|
7685
|
-
|
|
8888
|
+
let colorValue =
|
|
8889
|
+
typeof hexValue === "string" && hexValue.trim()
|
|
8890
|
+
? hexValue.trim()
|
|
8891
|
+
: "#D9D9D9";
|
|
8892
|
+
let rgba = this.convertToRGBA(colorValue);
|
|
8893
|
+
if (
|
|
8894
|
+
!rgba ||
|
|
8895
|
+
!Number.isFinite(rgba.r) ||
|
|
8896
|
+
!Number.isFinite(rgba.g) ||
|
|
8897
|
+
!Number.isFinite(rgba.b) ||
|
|
8898
|
+
!Number.isFinite(rgba.a)
|
|
8899
|
+
) {
|
|
8900
|
+
colorValue = "#D9D9D9";
|
|
8901
|
+
rgba = { r: 217, g: 217, b: 217, a: 1 };
|
|
8902
|
+
}
|
|
8903
|
+
this.rgba = rgba;
|
|
7686
8904
|
this.value = this.rgbAlphaToHex(
|
|
7687
8905
|
{
|
|
7688
8906
|
r: isNaN(this.rgba.r) ? 0 : this.rgba.r,
|
|
@@ -7873,7 +9091,6 @@ class FigInputColor extends HTMLElement {
|
|
|
7873
9091
|
"value",
|
|
7874
9092
|
"style",
|
|
7875
9093
|
"mode",
|
|
7876
|
-
"experimental",
|
|
7877
9094
|
"alpha",
|
|
7878
9095
|
"text",
|
|
7879
9096
|
"disabled",
|
|
@@ -7991,6 +9208,7 @@ class FigInputColor extends HTMLElement {
|
|
|
7991
9208
|
}
|
|
7992
9209
|
|
|
7993
9210
|
convertToRGBA(color) {
|
|
9211
|
+
if (typeof color !== "string") return null;
|
|
7994
9212
|
let r,
|
|
7995
9213
|
g,
|
|
7996
9214
|
b,
|
|
@@ -8415,7 +9633,6 @@ class FigInputFill extends HTMLElement {
|
|
|
8415
9633
|
"value",
|
|
8416
9634
|
"disabled",
|
|
8417
9635
|
"mode",
|
|
8418
|
-
"experimental",
|
|
8419
9636
|
"alpha",
|
|
8420
9637
|
"aria-label",
|
|
8421
9638
|
"aria-describedby",
|
|
@@ -8499,13 +9716,15 @@ class FigInputFill extends HTMLElement {
|
|
|
8499
9716
|
// Backward-compat: direct attributes forwarded to fill picker
|
|
8500
9717
|
const mode = this.getAttribute("mode");
|
|
8501
9718
|
if (mode) attrs["mode"] = mode;
|
|
8502
|
-
const experimental = this.getAttribute("experimental");
|
|
8503
|
-
if (experimental) attrs["experimental"] = experimental;
|
|
8504
9719
|
const alpha = this.getAttribute("alpha");
|
|
8505
9720
|
if (alpha) attrs["alpha"] = alpha;
|
|
8506
9721
|
// picker-* overrides (except anchor, handled programmatically)
|
|
8507
9722
|
for (const { name, value } of this.attributes) {
|
|
8508
|
-
if (
|
|
9723
|
+
if (
|
|
9724
|
+
name.startsWith("picker-") &&
|
|
9725
|
+
name !== "picker-anchor" &&
|
|
9726
|
+
name !== "picker-experimental"
|
|
9727
|
+
) {
|
|
8509
9728
|
attrs[name.slice(7)] = value;
|
|
8510
9729
|
}
|
|
8511
9730
|
}
|
|
@@ -8741,7 +9960,6 @@ class FigInputFill extends HTMLElement {
|
|
|
8741
9960
|
if (detail.video) this.#video = detail.video;
|
|
8742
9961
|
break;
|
|
8743
9962
|
}
|
|
8744
|
-
|
|
8745
9963
|
// Update controls (don't re-render to keep dialog open)
|
|
8746
9964
|
if (typeChanged) {
|
|
8747
9965
|
this.#updateControlsForType();
|
|
@@ -9141,7 +10359,6 @@ class FigInputFill extends HTMLElement {
|
|
|
9141
10359
|
this.#syncDisabled();
|
|
9142
10360
|
break;
|
|
9143
10361
|
case "mode":
|
|
9144
|
-
case "experimental":
|
|
9145
10362
|
// Pass through to internal fill picker
|
|
9146
10363
|
if (this.#fillPicker) {
|
|
9147
10364
|
if (newValue) {
|
|
@@ -9183,6 +10400,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9183
10400
|
#expandedPickers = [];
|
|
9184
10401
|
#renderRAF = null;
|
|
9185
10402
|
#boundHandleKeyDown = this.#handleKeyDown.bind(this);
|
|
10403
|
+
#boundHandleHostFocus = () => this.focus();
|
|
9186
10404
|
|
|
9187
10405
|
static get observedAttributes() {
|
|
9188
10406
|
return ["value", "disabled", "min", "max", "open", "fixed"];
|
|
@@ -9226,7 +10444,9 @@ class FigInputPalette extends HTMLElement {
|
|
|
9226
10444
|
}
|
|
9227
10445
|
|
|
9228
10446
|
connectedCallback() {
|
|
9229
|
-
|
|
10447
|
+
this.setAttribute("tabindex", "-1");
|
|
10448
|
+
this.removeEventListener("focus", this.#boundHandleHostFocus);
|
|
10449
|
+
this.addEventListener("focus", this.#boundHandleHostFocus);
|
|
9230
10450
|
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
9231
10451
|
this.addEventListener("keydown", this.#boundHandleKeyDown);
|
|
9232
10452
|
if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
|
|
@@ -9244,13 +10464,14 @@ class FigInputPalette extends HTMLElement {
|
|
|
9244
10464
|
this.#renderRAF = null;
|
|
9245
10465
|
}
|
|
9246
10466
|
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
10467
|
+
this.removeEventListener("focus", this.#boundHandleHostFocus);
|
|
9247
10468
|
this.#inlinePickers = [];
|
|
9248
10469
|
this.#expandedPickers = [];
|
|
9249
10470
|
}
|
|
9250
10471
|
|
|
9251
10472
|
#handleKeyDown(event) {
|
|
9252
10473
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
9253
|
-
if (event.target !== this
|
|
10474
|
+
if (event.target !== this.querySelector(".palette-colors-inline")) return;
|
|
9254
10475
|
if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
|
|
9255
10476
|
event.preventDefault();
|
|
9256
10477
|
event.stopPropagation();
|
|
@@ -9275,7 +10496,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9275
10496
|
this.#render();
|
|
9276
10497
|
break;
|
|
9277
10498
|
case "open":
|
|
9278
|
-
|
|
10499
|
+
this.#syncTriggerState();
|
|
9279
10500
|
break;
|
|
9280
10501
|
}
|
|
9281
10502
|
}
|
|
@@ -9371,8 +10592,13 @@ class FigInputPalette extends HTMLElement {
|
|
|
9371
10592
|
const inlineWrap = document.createElement("div");
|
|
9372
10593
|
inlineWrap.className = "palette-colors-inline";
|
|
9373
10594
|
inlineWrap.setAttribute("role", "button");
|
|
10595
|
+
inlineWrap.setAttribute("tabindex", disabled ? "-1" : "+0");
|
|
9374
10596
|
inlineWrap.setAttribute("aria-expanded", String(this.open));
|
|
9375
10597
|
inlineWrap.setAttribute("aria-label", "Edit palette colors");
|
|
10598
|
+
inlineWrap.addEventListener("blur", () => {
|
|
10599
|
+
inlineWrap.style.removeProperty("outline");
|
|
10600
|
+
inlineWrap.style.removeProperty("outline-offset");
|
|
10601
|
+
});
|
|
9376
10602
|
const openPalette = () => {
|
|
9377
10603
|
if (
|
|
9378
10604
|
this.hasAttribute("disabled") &&
|
|
@@ -9383,12 +10609,6 @@ class FigInputPalette extends HTMLElement {
|
|
|
9383
10609
|
inlineWrap.setAttribute("aria-expanded", "true");
|
|
9384
10610
|
};
|
|
9385
10611
|
inlineWrap.addEventListener("click", openPalette);
|
|
9386
|
-
inlineWrap.addEventListener("keydown", (event) => {
|
|
9387
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
9388
|
-
event.preventDefault();
|
|
9389
|
-
event.stopPropagation();
|
|
9390
|
-
openPalette();
|
|
9391
|
-
});
|
|
9392
10612
|
|
|
9393
10613
|
const wrap = document.createElement("div");
|
|
9394
10614
|
wrap.className = "palette-colors";
|
|
@@ -9399,6 +10619,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9399
10619
|
});
|
|
9400
10620
|
inlineWrap.appendChild(wrap);
|
|
9401
10621
|
this.appendChild(inlineWrap);
|
|
10622
|
+
this.#syncTriggerState();
|
|
9402
10623
|
|
|
9403
10624
|
if (!this.#isFixed) this.#createAddButton(disabled, this);
|
|
9404
10625
|
|
|
@@ -9595,6 +10816,31 @@ class FigInputPalette extends HTMLElement {
|
|
|
9595
10816
|
else addBtn.removeAttribute("disabled");
|
|
9596
10817
|
}
|
|
9597
10818
|
this.#syncRemoveButtons(disabled);
|
|
10819
|
+
this.#syncTriggerState();
|
|
10820
|
+
}
|
|
10821
|
+
|
|
10822
|
+
#syncTriggerState() {
|
|
10823
|
+
const trigger = this.querySelector(".palette-colors-inline");
|
|
10824
|
+
if (!trigger) return;
|
|
10825
|
+
const disabled =
|
|
10826
|
+
this.hasAttribute("disabled") &&
|
|
10827
|
+
this.getAttribute("disabled") !== "false";
|
|
10828
|
+
trigger.setAttribute("tabindex", disabled ? "-1" : "+0");
|
|
10829
|
+
trigger.setAttribute("aria-disabled", String(disabled));
|
|
10830
|
+
trigger.setAttribute("aria-expanded", String(this.open));
|
|
10831
|
+
}
|
|
10832
|
+
|
|
10833
|
+
focus() {
|
|
10834
|
+
if (
|
|
10835
|
+
this.hasAttribute("disabled") &&
|
|
10836
|
+
this.getAttribute("disabled") !== "false"
|
|
10837
|
+
)
|
|
10838
|
+
return;
|
|
10839
|
+
const trigger = this.querySelector(".palette-colors-inline");
|
|
10840
|
+
if (!trigger) return;
|
|
10841
|
+
trigger.style.outline = "var(--figma-focus-outline)";
|
|
10842
|
+
trigger.style.outlineOffset = "var(--figma-focus-outline-offset)";
|
|
10843
|
+
trigger.focus();
|
|
9598
10844
|
}
|
|
9599
10845
|
|
|
9600
10846
|
#syncRemoveButtons(disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") {
|
|
@@ -9923,11 +11169,9 @@ class FigInputGradient extends HTMLElement {
|
|
|
9923
11169
|
const mode = this.#editMode;
|
|
9924
11170
|
|
|
9925
11171
|
if (mode === "picker" && hasFigFillPicker()) {
|
|
9926
|
-
const experimental = this.getAttribute("experimental");
|
|
9927
|
-
const expAttr = experimental ? ` experimental="${experimental}"` : "";
|
|
9928
11172
|
const gradientValue = JSON.stringify(this.value);
|
|
9929
11173
|
this.innerHTML = `
|
|
9930
|
-
<fig-fill-picker mode="gradient"
|
|
11174
|
+
<fig-fill-picker mode="gradient" value='${gradientValue}'${disabled ? " disabled" : ""}>
|
|
9931
11175
|
<fig-swatch background="${this.#buildGradientCSS()}"${this.#swatchSizeAttr()}${disabled ? " disabled" : ""}></fig-swatch>
|
|
9932
11176
|
</fig-fill-picker>`;
|
|
9933
11177
|
this.#swatch = this.querySelector("fig-swatch");
|
|
@@ -10819,7 +12063,6 @@ figDefineElement("fig-switch", FigSwitch);
|
|
|
10819
12063
|
* @attr {string} placeholder - Placeholder text for the input
|
|
10820
12064
|
* @attr {string} value - The current input value
|
|
10821
12065
|
* @attr {boolean} disabled - Disables the input and dropdown button
|
|
10822
|
-
* @attr {string} experimental - Feature flag passed to internal fig-dropdown
|
|
10823
12066
|
*/
|
|
10824
12067
|
class FigComboInput extends HTMLElement {
|
|
10825
12068
|
static observedAttributes = [
|
|
@@ -10827,7 +12070,6 @@ class FigComboInput extends HTMLElement {
|
|
|
10827
12070
|
"placeholder",
|
|
10828
12071
|
"value",
|
|
10829
12072
|
"disabled",
|
|
10830
|
-
"experimental",
|
|
10831
12073
|
"aria-label",
|
|
10832
12074
|
"aria-labelledby",
|
|
10833
12075
|
"aria-describedby",
|
|
@@ -10908,15 +12150,11 @@ class FigComboInput extends HTMLElement {
|
|
|
10908
12150
|
const options = this.#getOptions();
|
|
10909
12151
|
const placeholder = this.getAttribute("placeholder") || "";
|
|
10910
12152
|
const currentValue = this.value;
|
|
10911
|
-
const experimental = this.getAttribute("experimental");
|
|
10912
|
-
const expAttr = experimental
|
|
10913
|
-
? ` experimental="${figEscapeAttribute(experimental)}"`
|
|
10914
|
-
: "";
|
|
10915
12153
|
const dropdownLabel = this.#dropdownLabel();
|
|
10916
12154
|
|
|
10917
12155
|
const dropdownHTML = this.#usesCustomDropdown
|
|
10918
12156
|
? ""
|
|
10919
|
-
: `<fig-dropdown type="dropdown" label="${figEscapeAttribute(dropdownLabel)}"
|
|
12157
|
+
: `<fig-dropdown type="dropdown" label="${figEscapeAttribute(dropdownLabel)}">${options.map((o) => `<option>${figEscapeAttribute(o.trim())}</option>`).join("")}</fig-dropdown>`;
|
|
10920
12158
|
|
|
10921
12159
|
this.innerHTML = `<div class="input-combo">
|
|
10922
12160
|
<fig-input-text placeholder="${figEscapeAttribute(placeholder)}" value="${figEscapeAttribute(currentValue)}"></fig-input-text>
|
|
@@ -10938,9 +12176,6 @@ class FigComboInput extends HTMLElement {
|
|
|
10938
12176
|
if (!this.#customDropdown.hasAttribute("label")) {
|
|
10939
12177
|
this.#customDropdown.setAttribute("label", dropdownLabel);
|
|
10940
12178
|
}
|
|
10941
|
-
if (experimental) {
|
|
10942
|
-
this.#customDropdown.setAttribute("experimental", experimental);
|
|
10943
|
-
}
|
|
10944
12179
|
this.#button.append(this.#customDropdown);
|
|
10945
12180
|
}
|
|
10946
12181
|
|
|
@@ -11088,13 +12323,6 @@ class FigComboInput extends HTMLElement {
|
|
|
11088
12323
|
case "disabled":
|
|
11089
12324
|
this.#applyDisabled(newValue !== null && newValue !== "false");
|
|
11090
12325
|
break;
|
|
11091
|
-
case "experimental":
|
|
11092
|
-
if (this.#dropdown) {
|
|
11093
|
-
if (newValue) this.#dropdown.setAttribute("experimental", newValue);
|
|
11094
|
-
else if (!this.#usesCustomDropdown)
|
|
11095
|
-
this.#dropdown.removeAttribute("experimental");
|
|
11096
|
-
}
|
|
11097
|
-
break;
|
|
11098
12326
|
case "aria-label":
|
|
11099
12327
|
case "aria-labelledby":
|
|
11100
12328
|
case "aria-describedby":
|
|
@@ -12892,9 +14120,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
12892
14120
|
#line2 = null;
|
|
12893
14121
|
#handle1 = null;
|
|
12894
14122
|
#handle2 = null;
|
|
12895
|
-
#
|
|
12896
|
-
#bezierEndpointEnd = null;
|
|
12897
|
-
#dropdown = null;
|
|
14123
|
+
#select = null;
|
|
12898
14124
|
#valueInput = null;
|
|
12899
14125
|
#presetName = null;
|
|
12900
14126
|
#targetLine = null;
|
|
@@ -12902,10 +14128,10 @@ class FigEasingCurve extends HTMLElement {
|
|
|
12902
14128
|
#drawWidth = 200;
|
|
12903
14129
|
#drawHeight = 200;
|
|
12904
14130
|
#bounds = null;
|
|
12905
|
-
#
|
|
14131
|
+
#boundaryTop = null;
|
|
14132
|
+
#boundaryBottom = null;
|
|
12906
14133
|
#resizeObserver = null;
|
|
12907
14134
|
#bezierHandleRadius = 5;
|
|
12908
|
-
#bezierEndpointRadius = 2;
|
|
12909
14135
|
#durationBarWidth = 10;
|
|
12910
14136
|
#durationBarHeight = 10;
|
|
12911
14137
|
#durationBarRadius = 3;
|
|
@@ -13023,7 +14249,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13023
14249
|
this.#render();
|
|
13024
14250
|
} else {
|
|
13025
14251
|
if (this.#svg) this.#updatePaths();
|
|
13026
|
-
this.#
|
|
14252
|
+
this.#syncSelect();
|
|
13027
14253
|
this.#syncValueInput();
|
|
13028
14254
|
}
|
|
13029
14255
|
}
|
|
@@ -13246,14 +14472,15 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13246
14472
|
.replace(/>/g, ">");
|
|
13247
14473
|
}
|
|
13248
14474
|
|
|
13249
|
-
#
|
|
14475
|
+
#getSelectHTML() {
|
|
13250
14476
|
let optionsHTML = "";
|
|
13251
14477
|
let currentGroup = undefined;
|
|
13252
14478
|
for (const p of FigEasingCurve.PRESETS) {
|
|
13253
14479
|
if (!this.#isEditEnabled() && !p.value && !p.spring) continue;
|
|
13254
14480
|
if (p.group !== currentGroup) {
|
|
13255
|
-
if (
|
|
13256
|
-
|
|
14481
|
+
if (p.group) {
|
|
14482
|
+
optionsHTML += `<fig-menu-separator label="${FigEasingCurve.#escapeAttribute(p.group)}"></fig-menu-separator>`;
|
|
14483
|
+
}
|
|
13257
14484
|
currentGroup = p.group;
|
|
13258
14485
|
}
|
|
13259
14486
|
let icon;
|
|
@@ -13269,40 +14496,37 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13269
14496
|
];
|
|
13270
14497
|
icon = FigEasingCurve.curveIcon(...v);
|
|
13271
14498
|
}
|
|
13272
|
-
const
|
|
13273
|
-
optionsHTML += `<option value="${
|
|
14499
|
+
const name = FigEasingCurve.#escapeAttribute(p.name);
|
|
14500
|
+
optionsHTML += `<fig-select-option value="${name}" label="${name}"><span slot="prepend">${icon}</span><span>${name}</span></fig-select-option>`;
|
|
13274
14501
|
}
|
|
13275
|
-
|
|
13276
|
-
return `<fig-
|
|
14502
|
+
const value = FigEasingCurve.#escapeAttribute(this.#presetName);
|
|
14503
|
+
return `<fig-select class="fig-easing-curve-select" label="Easing preset" value="${value}" full><fig-select-options>${optionsHTML}</fig-select-options></fig-select>`;
|
|
13277
14504
|
}
|
|
13278
14505
|
|
|
13279
14506
|
#getInnerHTML() {
|
|
13280
14507
|
const size = 200;
|
|
13281
|
-
const
|
|
13282
|
-
if (!this.#isEditEnabled()) return
|
|
14508
|
+
const select = this.#getSelectHTML();
|
|
14509
|
+
if (!this.#isEditEnabled()) return select;
|
|
13283
14510
|
const valueInput = `<fig-input-text class="fig-easing-curve-value-input" value="${FigEasingCurve.#escapeAttribute(this.value)}" full></fig-input-text>`;
|
|
13284
14511
|
|
|
13285
14512
|
if (this.#mode === "spring") {
|
|
13286
14513
|
const targetY = 40;
|
|
13287
|
-
|
|
13288
|
-
return `${dropdown}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
14514
|
+
return `${select}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
13289
14515
|
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
13290
14516
|
<line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
|
|
13291
|
-
<line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
|
|
13292
14517
|
<path class="fig-easing-curve-path"/>
|
|
13293
14518
|
<foreignObject class="fig-easing-curve-handle" data-handle="bounce" width="20" height="20"><fig-handle size="small" aria-label="Spring bounce handle"></fig-handle></foreignObject>
|
|
13294
14519
|
<foreignObject class="fig-easing-curve-handle fig-easing-curve-duration-bar" data-handle="duration" width="20" height="20"><fig-handle size="small" aria-label="Spring duration handle"></fig-handle></foreignObject>
|
|
13295
14520
|
</svg></div>${valueInput}`;
|
|
13296
14521
|
}
|
|
13297
14522
|
|
|
13298
|
-
return `${
|
|
14523
|
+
return `${select}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
13299
14524
|
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
13300
|
-
<line class="fig-easing-curve-
|
|
14525
|
+
<line class="fig-easing-curve-boundary" data-boundary="top" x1="0" y1="0" x2="${size}" y2="0"/>
|
|
14526
|
+
<line class="fig-easing-curve-boundary" data-boundary="bottom" x1="0" y1="${size}" x2="${size}" y2="${size}"/>
|
|
14527
|
+
<path class="fig-easing-curve-path"/>
|
|
13301
14528
|
<line class="fig-easing-curve-arm" data-arm="1"/>
|
|
13302
14529
|
<line class="fig-easing-curve-arm" data-arm="2"/>
|
|
13303
|
-
<path class="fig-easing-curve-path"/>
|
|
13304
|
-
<circle class="fig-easing-curve-endpoint" data-endpoint="start" r="${this.#bezierEndpointRadius}"/>
|
|
13305
|
-
<circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
|
|
13306
14530
|
<foreignObject class="fig-easing-curve-handle" data-handle="1" width="20" height="20"><fig-handle size="small" aria-label="First easing control point"></fig-handle></foreignObject>
|
|
13307
14531
|
<foreignObject class="fig-easing-curve-handle" data-handle="2" width="20" height="20"><fig-handle size="small" aria-label="Second easing control point"></fig-handle></foreignObject>
|
|
13308
14532
|
</svg></div>${valueInput}`;
|
|
@@ -13316,10 +14540,6 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13316
14540
|
}
|
|
13317
14541
|
|
|
13318
14542
|
#syncMetricsFromCSS() {
|
|
13319
|
-
this.#bezierEndpointRadius = this.#readCssNumber(
|
|
13320
|
-
"--easing-bezier-endpoint-radius",
|
|
13321
|
-
this.#bezierEndpointRadius,
|
|
13322
|
-
);
|
|
13323
14543
|
this.#durationBarRadius = this.#readCssNumber(
|
|
13324
14544
|
"--easing-duration-bar-radius",
|
|
13325
14545
|
this.#durationBarRadius,
|
|
@@ -13337,13 +14557,12 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13337
14557
|
this.#handle2 =
|
|
13338
14558
|
this.querySelector('[data-handle="2"]') ||
|
|
13339
14559
|
this.querySelector('[data-handle="duration"]');
|
|
13340
|
-
this.#
|
|
13341
|
-
this.#bezierEndpointEnd = this.querySelector('[data-endpoint="end"]');
|
|
13342
|
-
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
14560
|
+
this.#select = this.querySelector(".fig-easing-curve-select");
|
|
13343
14561
|
this.#valueInput = this.querySelector(".fig-easing-curve-value-input");
|
|
13344
14562
|
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
13345
14563
|
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
13346
|
-
this.#
|
|
14564
|
+
this.#boundaryTop = this.querySelector('[data-boundary="top"]');
|
|
14565
|
+
this.#boundaryBottom = this.querySelector('[data-boundary="bottom"]');
|
|
13347
14566
|
}
|
|
13348
14567
|
|
|
13349
14568
|
#syncHandleSizes() {
|
|
@@ -13392,12 +14611,37 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13392
14611
|
|
|
13393
14612
|
// --- Coordinate helpers ---
|
|
13394
14613
|
|
|
14614
|
+
#bezierDomain() {
|
|
14615
|
+
const minVal = Math.min(0, this.#cp1.y, this.#cp2.y);
|
|
14616
|
+
const maxVal = Math.max(1, this.#cp1.y, this.#cp2.y);
|
|
14617
|
+
const range = maxVal - minVal || 1;
|
|
14618
|
+
const pad = Math.min(
|
|
14619
|
+
this.#bezierHandleRadius,
|
|
14620
|
+
Math.max(0, (this.#drawHeight - 1) / 2),
|
|
14621
|
+
);
|
|
14622
|
+
return {
|
|
14623
|
+
minVal,
|
|
14624
|
+
maxVal,
|
|
14625
|
+
range,
|
|
14626
|
+
pad,
|
|
14627
|
+
draw: Math.max(1, this.#drawHeight - pad * 2),
|
|
14628
|
+
};
|
|
14629
|
+
}
|
|
14630
|
+
|
|
13395
14631
|
#toSVG(nx, ny) {
|
|
13396
|
-
|
|
14632
|
+
const { maxVal, range, pad, draw } = this.#bezierDomain();
|
|
14633
|
+
return {
|
|
14634
|
+
x: nx * this.#drawWidth,
|
|
14635
|
+
y: pad + ((maxVal - ny) / range) * draw,
|
|
14636
|
+
};
|
|
13397
14637
|
}
|
|
13398
14638
|
|
|
13399
14639
|
#fromSVG(sx, sy) {
|
|
13400
|
-
|
|
14640
|
+
const { maxVal, range, pad, draw } = this.#bezierDomain();
|
|
14641
|
+
return {
|
|
14642
|
+
x: sx / this.#drawWidth,
|
|
14643
|
+
y: maxVal - ((sy - pad) / draw) * range,
|
|
14644
|
+
};
|
|
13401
14645
|
}
|
|
13402
14646
|
|
|
13403
14647
|
#springScale = { minVal: 0, maxVal: 1.2, totalTime: 1 };
|
|
@@ -13442,17 +14686,20 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13442
14686
|
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
13443
14687
|
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
13444
14688
|
}
|
|
13445
|
-
if (this.#diagonal) {
|
|
13446
|
-
this.#diagonal.setAttribute("x1", "0");
|
|
13447
|
-
this.#diagonal.setAttribute("y1", this.#drawHeight);
|
|
13448
|
-
this.#diagonal.setAttribute("x2", this.#drawWidth);
|
|
13449
|
-
this.#diagonal.setAttribute("y2", "0");
|
|
13450
|
-
}
|
|
13451
|
-
|
|
13452
14689
|
const p0 = this.#toSVG(0, 0);
|
|
13453
14690
|
const p1 = this.#toSVG(this.#cp1.x, this.#cp1.y);
|
|
13454
14691
|
const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
|
|
13455
14692
|
const p3 = this.#toSVG(1, 1);
|
|
14693
|
+
for (const [boundary, y] of [
|
|
14694
|
+
[this.#boundaryTop, p3.y],
|
|
14695
|
+
[this.#boundaryBottom, p0.y],
|
|
14696
|
+
]) {
|
|
14697
|
+
if (!boundary) continue;
|
|
14698
|
+
boundary.setAttribute("x1", "0");
|
|
14699
|
+
boundary.setAttribute("y1", y);
|
|
14700
|
+
boundary.setAttribute("x2", this.#drawWidth);
|
|
14701
|
+
boundary.setAttribute("y2", y);
|
|
14702
|
+
}
|
|
13456
14703
|
|
|
13457
14704
|
this.#curve.setAttribute(
|
|
13458
14705
|
"d",
|
|
@@ -13471,14 +14718,6 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13471
14718
|
this.#handle1.setAttribute("y", p1.y - hr);
|
|
13472
14719
|
this.#handle2.setAttribute("x", p2.x - hr);
|
|
13473
14720
|
this.#handle2.setAttribute("y", p2.y - hr);
|
|
13474
|
-
if (this.#bezierEndpointStart) {
|
|
13475
|
-
this.#bezierEndpointStart.setAttribute("cx", p0.x);
|
|
13476
|
-
this.#bezierEndpointStart.setAttribute("cy", p0.y);
|
|
13477
|
-
}
|
|
13478
|
-
if (this.#bezierEndpointEnd) {
|
|
13479
|
-
this.#bezierEndpointEnd.setAttribute("cx", p3.x);
|
|
13480
|
-
this.#bezierEndpointEnd.setAttribute("cy", p3.y);
|
|
13481
|
-
}
|
|
13482
14721
|
this.#syncBezierHandleTabOrder();
|
|
13483
14722
|
}
|
|
13484
14723
|
|
|
@@ -13571,11 +14810,11 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13571
14810
|
return peak;
|
|
13572
14811
|
}
|
|
13573
14812
|
|
|
13574
|
-
// ---
|
|
14813
|
+
// --- Select ---
|
|
13575
14814
|
|
|
13576
|
-
#
|
|
13577
|
-
if (!this.#
|
|
13578
|
-
this.#
|
|
14815
|
+
#syncSelect() {
|
|
14816
|
+
if (!this.#select) return;
|
|
14817
|
+
this.#select.value = this.#presetName;
|
|
13579
14818
|
this.#refreshCustomPresetIcons();
|
|
13580
14819
|
}
|
|
13581
14820
|
|
|
@@ -13614,23 +14853,24 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13614
14853
|
this.#render();
|
|
13615
14854
|
} else {
|
|
13616
14855
|
this.#updatePaths();
|
|
13617
|
-
this.#
|
|
14856
|
+
this.#syncSelect();
|
|
13618
14857
|
if (eventType === "change") this.#syncValueInput();
|
|
13619
14858
|
}
|
|
13620
14859
|
this.#emit(eventType);
|
|
13621
14860
|
}
|
|
13622
14861
|
|
|
13623
|
-
#setOptionIconByValue(
|
|
13624
|
-
if (!
|
|
13625
|
-
for (const option of
|
|
14862
|
+
#setOptionIconByValue(optionValue, icon) {
|
|
14863
|
+
if (!this.#select) return;
|
|
14864
|
+
for (const option of this.#select.querySelectorAll("fig-select-option")) {
|
|
13626
14865
|
if (option.value === optionValue) {
|
|
13627
|
-
option.
|
|
14866
|
+
const prepend = option.querySelector(':scope > [slot="prepend"]');
|
|
14867
|
+
if (prepend) prepend.innerHTML = icon;
|
|
13628
14868
|
}
|
|
13629
14869
|
}
|
|
13630
14870
|
}
|
|
13631
14871
|
|
|
13632
14872
|
#refreshCustomPresetIcons() {
|
|
13633
|
-
if (!this.#
|
|
14873
|
+
if (!this.#select) return;
|
|
13634
14874
|
if (!this.#isEditEnabled()) return;
|
|
13635
14875
|
const bezierIcon = FigEasingCurve.curveIcon(
|
|
13636
14876
|
this.#cp1.x,
|
|
@@ -13640,25 +14880,14 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13640
14880
|
);
|
|
13641
14881
|
const springIcon = FigEasingCurve.#springIcon(this.#spring);
|
|
13642
14882
|
|
|
13643
|
-
|
|
13644
|
-
this.#setOptionIconByValue(
|
|
13645
|
-
this.#setOptionIconByValue(this.#dropdown, "Custom spring", springIcon);
|
|
13646
|
-
this.#setOptionIconByValue(
|
|
13647
|
-
this.#dropdown.select,
|
|
13648
|
-
"Custom bezier",
|
|
13649
|
-
bezierIcon,
|
|
13650
|
-
);
|
|
13651
|
-
this.#setOptionIconByValue(
|
|
13652
|
-
this.#dropdown.select,
|
|
13653
|
-
"Custom spring",
|
|
13654
|
-
springIcon,
|
|
13655
|
-
);
|
|
14883
|
+
this.#setOptionIconByValue("Custom bezier", bezierIcon);
|
|
14884
|
+
this.#setOptionIconByValue("Custom spring", springIcon);
|
|
13656
14885
|
}
|
|
13657
14886
|
|
|
13658
14887
|
#syncAfterHandleInput(eventType) {
|
|
13659
14888
|
this.#updatePaths();
|
|
13660
14889
|
this.#presetName = this.#matchPreset();
|
|
13661
|
-
this.#
|
|
14890
|
+
this.#syncSelect();
|
|
13662
14891
|
this.#syncValueInput();
|
|
13663
14892
|
this.#emit(eventType);
|
|
13664
14893
|
}
|
|
@@ -13847,8 +15076,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13847
15076
|
}
|
|
13848
15077
|
}
|
|
13849
15078
|
|
|
13850
|
-
if (this.#
|
|
13851
|
-
this.#
|
|
15079
|
+
if (this.#select) {
|
|
15080
|
+
this.#select.addEventListener("change", (e) => {
|
|
13852
15081
|
const name = e.detail;
|
|
13853
15082
|
const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
|
|
13854
15083
|
if (!preset) return;
|
|
@@ -13921,11 +15150,30 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13921
15150
|
e.preventDefault();
|
|
13922
15151
|
this.#isDragging = handle;
|
|
13923
15152
|
this.#syncActiveBezierArm();
|
|
15153
|
+
const svgRect = this.#svg.getBoundingClientRect();
|
|
15154
|
+
const startClientX = e.clientX;
|
|
15155
|
+
const startClientY = e.clientY;
|
|
15156
|
+
const fromHandle = e.target?.closest?.(
|
|
15157
|
+
".fig-easing-curve-handle, fig-handle",
|
|
15158
|
+
);
|
|
15159
|
+
const currentPoint = handle === 1 ? this.#cp1 : this.#cp2;
|
|
15160
|
+
const svgPoint = this.#clientToSVG(e);
|
|
15161
|
+
const startPoint = fromHandle
|
|
15162
|
+
? { ...currentPoint }
|
|
15163
|
+
: this.#fromSVG(svgPoint.x, svgPoint.y);
|
|
15164
|
+
const { range, draw } = this.#bezierDomain();
|
|
15165
|
+
const unitsPerClientY =
|
|
15166
|
+
range /
|
|
15167
|
+
Math.max(1, (draw / this.#drawHeight) * Math.max(1, svgRect.height));
|
|
13924
15168
|
|
|
13925
15169
|
const onMove = (e) => {
|
|
13926
15170
|
if (!this.#isDragging) return;
|
|
13927
|
-
const
|
|
13928
|
-
|
|
15171
|
+
const norm = {
|
|
15172
|
+
x:
|
|
15173
|
+
startPoint.x +
|
|
15174
|
+
(e.clientX - startClientX) / Math.max(1, svgRect.width),
|
|
15175
|
+
y: startPoint.y - (e.clientY - startClientY) * unitsPerClientY,
|
|
15176
|
+
};
|
|
13929
15177
|
|
|
13930
15178
|
norm.x = Math.round(norm.x * 100) / 100;
|
|
13931
15179
|
norm.y = Math.round(norm.y * 100) / 100;
|
|
@@ -13940,7 +15188,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13940
15188
|
}
|
|
13941
15189
|
this.#updatePaths();
|
|
13942
15190
|
this.#presetName = this.#matchPreset();
|
|
13943
|
-
this.#
|
|
15191
|
+
this.#syncSelect();
|
|
13944
15192
|
this.#syncValueInput();
|
|
13945
15193
|
this.#emit("input");
|
|
13946
15194
|
};
|
|
@@ -13991,7 +15239,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13991
15239
|
|
|
13992
15240
|
this.#updatePaths();
|
|
13993
15241
|
this.#presetName = this.#matchPreset();
|
|
13994
|
-
this.#
|
|
15242
|
+
this.#syncSelect();
|
|
13995
15243
|
this.#syncValueInput();
|
|
13996
15244
|
this.#emit("input");
|
|
13997
15245
|
};
|
|
@@ -16576,6 +17824,7 @@ class FigChooser extends HTMLElement {
|
|
|
16576
17824
|
this.#setupDrag();
|
|
16577
17825
|
this.#startObserver();
|
|
16578
17826
|
this.#startResizeObserver();
|
|
17827
|
+
this.#syncDisabledChoices();
|
|
16579
17828
|
|
|
16580
17829
|
figNextFrame(this, () => {
|
|
16581
17830
|
this.#syncSelection();
|
|
@@ -16629,17 +17878,7 @@ class FigChooser extends HTMLElement {
|
|
|
16629
17878
|
this.#selectByValue(newValue);
|
|
16630
17879
|
}
|
|
16631
17880
|
if (name === "disabled") {
|
|
16632
|
-
|
|
16633
|
-
const choices = this.choices;
|
|
16634
|
-
for (const choice of choices) {
|
|
16635
|
-
if (isDisabled) {
|
|
16636
|
-
choice.setAttribute("aria-disabled", "true");
|
|
16637
|
-
choice.setAttribute("tabindex", "-1");
|
|
16638
|
-
} else {
|
|
16639
|
-
choice.removeAttribute("aria-disabled");
|
|
16640
|
-
choice.setAttribute("tabindex", "0");
|
|
16641
|
-
}
|
|
16642
|
-
}
|
|
17881
|
+
this.#syncDisabledChoices();
|
|
16643
17882
|
}
|
|
16644
17883
|
if (name === "choice-element") {
|
|
16645
17884
|
requestAnimationFrame(() => this.#syncSelection());
|
|
@@ -16691,6 +17930,23 @@ class FigChooser extends HTMLElement {
|
|
|
16691
17930
|
this.selectedChoice = choices[0];
|
|
16692
17931
|
}
|
|
16693
17932
|
|
|
17933
|
+
#syncDisabledChoices() {
|
|
17934
|
+
const chooserDisabled = figBooleanAttribute(this, "disabled");
|
|
17935
|
+
if (chooserDisabled) this.setAttribute("aria-disabled", "true");
|
|
17936
|
+
else this.removeAttribute("aria-disabled");
|
|
17937
|
+
for (const choice of this.choices) {
|
|
17938
|
+
const disabled =
|
|
17939
|
+
chooserDisabled || figBooleanAttribute(choice, "disabled");
|
|
17940
|
+
if (disabled) {
|
|
17941
|
+
choice.setAttribute("aria-disabled", "true");
|
|
17942
|
+
choice.setAttribute("tabindex", "-1");
|
|
17943
|
+
} else {
|
|
17944
|
+
choice.removeAttribute("aria-disabled");
|
|
17945
|
+
choice.setAttribute("tabindex", "0");
|
|
17946
|
+
}
|
|
17947
|
+
}
|
|
17948
|
+
}
|
|
17949
|
+
|
|
16694
17950
|
#selectByValue(value) {
|
|
16695
17951
|
const choices = this.choices;
|
|
16696
17952
|
for (const choice of choices) {
|
|
@@ -17053,6 +18309,7 @@ class FigChooser extends HTMLElement {
|
|
|
17053
18309
|
if (this.#isUnwrapping) return;
|
|
17054
18310
|
this.#removeLegacyScroller();
|
|
17055
18311
|
this.#applyOverflowMode();
|
|
18312
|
+
this.#syncDisabledChoices();
|
|
17056
18313
|
const choices = this.choices;
|
|
17057
18314
|
if (this.#selectedChoice && !choices.includes(this.#selectedChoice)) {
|
|
17058
18315
|
this.#selectedChoice = null;
|
|
@@ -17089,6 +18346,7 @@ class FigHandle extends HTMLElement {
|
|
|
17089
18346
|
#isDragging = false;
|
|
17090
18347
|
#didDrag = false;
|
|
17091
18348
|
#boundPointerDown = null;
|
|
18349
|
+
#activeDragCleanup = null;
|
|
17092
18350
|
#applyingValue = false;
|
|
17093
18351
|
#colorTip = null;
|
|
17094
18352
|
#directColorPicker = null;
|
|
@@ -17527,11 +18785,14 @@ class FigHandle extends HTMLElement {
|
|
|
17527
18785
|
}
|
|
17528
18786
|
|
|
17529
18787
|
#teardownDrag() {
|
|
18788
|
+
this.#activeDragCleanup?.();
|
|
18789
|
+
this.#activeDragCleanup = null;
|
|
17530
18790
|
if (this.#boundPointerDown) {
|
|
17531
18791
|
this.removeEventListener("pointerdown", this.#boundPointerDown);
|
|
17532
18792
|
this.#boundPointerDown = null;
|
|
17533
18793
|
}
|
|
17534
18794
|
this.#isDragging = false;
|
|
18795
|
+
this.#didDrag = false;
|
|
17535
18796
|
}
|
|
17536
18797
|
|
|
17537
18798
|
#onPointerDown(e) {
|
|
@@ -17540,6 +18801,7 @@ class FigHandle extends HTMLElement {
|
|
|
17540
18801
|
const container = this.#getContainer();
|
|
17541
18802
|
if (!container) return;
|
|
17542
18803
|
|
|
18804
|
+
this.#activeDragCleanup?.();
|
|
17543
18805
|
this.#isDragging = true;
|
|
17544
18806
|
const axes = this.#axes;
|
|
17545
18807
|
let lastRect = null;
|
|
@@ -17617,11 +18879,7 @@ class FigHandle extends HTMLElement {
|
|
|
17617
18879
|
};
|
|
17618
18880
|
|
|
17619
18881
|
const onUp = (e) => {
|
|
17620
|
-
|
|
17621
|
-
this.style.cursor = "";
|
|
17622
|
-
this.classList.remove("dragging");
|
|
17623
|
-
window.removeEventListener("pointermove", onMove);
|
|
17624
|
-
window.removeEventListener("pointerup", onUp);
|
|
18882
|
+
cleanup();
|
|
17625
18883
|
if (this.#didDrag) {
|
|
17626
18884
|
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
17627
18885
|
this.#syncValueAttribute();
|
|
@@ -17646,6 +18904,17 @@ class FigHandle extends HTMLElement {
|
|
|
17646
18904
|
this.#didDrag = false;
|
|
17647
18905
|
};
|
|
17648
18906
|
|
|
18907
|
+
const cleanup = () => {
|
|
18908
|
+
window.removeEventListener("pointermove", onMove);
|
|
18909
|
+
window.removeEventListener("pointerup", onUp);
|
|
18910
|
+
this.#isDragging = false;
|
|
18911
|
+
this.style.cursor = "";
|
|
18912
|
+
this.classList.remove("dragging");
|
|
18913
|
+
if (this.#activeDragCleanup === cleanup) {
|
|
18914
|
+
this.#activeDragCleanup = null;
|
|
18915
|
+
}
|
|
18916
|
+
};
|
|
18917
|
+
this.#activeDragCleanup = cleanup;
|
|
17649
18918
|
window.addEventListener("pointermove", onMove);
|
|
17650
18919
|
window.addEventListener("pointerup", onUp);
|
|
17651
18920
|
}
|
|
@@ -18077,14 +19346,19 @@ figDefineElement("fig-menu-separator", FigMenuSeparator);
|
|
|
18077
19346
|
|
|
18078
19347
|
class FigMenu extends HTMLElement {
|
|
18079
19348
|
#popup = null;
|
|
19349
|
+
#panel = null;
|
|
18080
19350
|
#trigger = null;
|
|
18081
19351
|
#virtualAnchor = null;
|
|
18082
19352
|
#observer = null;
|
|
19353
|
+
#resizeObserver = null;
|
|
19354
|
+
#navStart = null;
|
|
19355
|
+
#navEnd = null;
|
|
18083
19356
|
#boundTriggerClick;
|
|
18084
19357
|
#boundTriggerContextMenu;
|
|
18085
19358
|
#boundPopupClick;
|
|
18086
19359
|
#boundMenuKeydown;
|
|
18087
19360
|
#boundPopupClose;
|
|
19361
|
+
#boundSyncOverflow = this.#syncOverflow.bind(this);
|
|
18088
19362
|
#focusedIndex = -1;
|
|
18089
19363
|
|
|
18090
19364
|
static get observedAttributes() {
|
|
@@ -18114,6 +19388,7 @@ class FigMenu extends HTMLElement {
|
|
|
18114
19388
|
|
|
18115
19389
|
set open(val) {
|
|
18116
19390
|
if (val) {
|
|
19391
|
+
if (this.#isDisabled()) return;
|
|
18117
19392
|
this.setAttribute("open", "");
|
|
18118
19393
|
} else {
|
|
18119
19394
|
this.removeAttribute("open");
|
|
@@ -18125,6 +19400,7 @@ class FigMenu extends HTMLElement {
|
|
|
18125
19400
|
this.#createPopup();
|
|
18126
19401
|
this.#moveItemsToPopup();
|
|
18127
19402
|
this.#setupListeners();
|
|
19403
|
+
this.#setupOverflow();
|
|
18128
19404
|
this.#setupObserver();
|
|
18129
19405
|
this.#syncDisabled();
|
|
18130
19406
|
|
|
@@ -18135,6 +19411,7 @@ class FigMenu extends HTMLElement {
|
|
|
18135
19411
|
|
|
18136
19412
|
disconnectedCallback() {
|
|
18137
19413
|
this.#teardownListeners();
|
|
19414
|
+
this.#teardownOverflow();
|
|
18138
19415
|
document.removeEventListener("keydown", this.#boundMenuKeydown, true);
|
|
18139
19416
|
if (this.#observer) {
|
|
18140
19417
|
this.#observer.disconnect();
|
|
@@ -18143,13 +19420,14 @@ class FigMenu extends HTMLElement {
|
|
|
18143
19420
|
if (this.#popup) {
|
|
18144
19421
|
this.#popup.removeEventListener("close", this.#boundPopupClose);
|
|
18145
19422
|
const items = Array.from(
|
|
18146
|
-
this.#
|
|
19423
|
+
this.#panel?.querySelectorAll(
|
|
18147
19424
|
":scope > fig-menu-item, :scope > fig-menu-separator",
|
|
18148
|
-
),
|
|
19425
|
+
) ?? [],
|
|
18149
19426
|
);
|
|
18150
19427
|
for (const item of items) this.insertBefore(item, this.#popup);
|
|
18151
19428
|
this.#popup.remove();
|
|
18152
19429
|
this.#popup = null;
|
|
19430
|
+
this.#panel = null;
|
|
18153
19431
|
}
|
|
18154
19432
|
}
|
|
18155
19433
|
|
|
@@ -18160,19 +19438,17 @@ class FigMenu extends HTMLElement {
|
|
|
18160
19438
|
if (newValue === null || newValue === "false") {
|
|
18161
19439
|
this.#closeMenu();
|
|
18162
19440
|
} else {
|
|
19441
|
+
if (this.#isDisabled()) {
|
|
19442
|
+
this.removeAttribute("open");
|
|
19443
|
+
return;
|
|
19444
|
+
}
|
|
18163
19445
|
this.#openMenu();
|
|
18164
19446
|
}
|
|
18165
19447
|
return;
|
|
18166
19448
|
}
|
|
18167
19449
|
|
|
18168
19450
|
if (name === "disabled") {
|
|
18169
|
-
|
|
18170
|
-
if (newValue !== null && newValue !== "false") {
|
|
18171
|
-
this.#trigger.setAttribute("disabled", "");
|
|
18172
|
-
} else {
|
|
18173
|
-
this.#trigger.removeAttribute("disabled");
|
|
18174
|
-
}
|
|
18175
|
-
}
|
|
19451
|
+
this.#syncDisabled();
|
|
18176
19452
|
return;
|
|
18177
19453
|
}
|
|
18178
19454
|
|
|
@@ -18199,6 +19475,7 @@ class FigMenu extends HTMLElement {
|
|
|
18199
19475
|
#createPopup() {
|
|
18200
19476
|
this.#popup = document.createElement("dialog", { is: "fig-popup" });
|
|
18201
19477
|
this.#popup.setAttribute("is", "fig-popup");
|
|
19478
|
+
this.#popup.classList.add("fig-menu-popup");
|
|
18202
19479
|
this.#popup.setAttribute("theme", "menu");
|
|
18203
19480
|
this.#popup.setAttribute("role", "menu");
|
|
18204
19481
|
this.#popup.setAttribute("id", this.#popup.getAttribute("id") || figUniqueId());
|
|
@@ -18216,16 +19493,21 @@ class FigMenu extends HTMLElement {
|
|
|
18216
19493
|
this.#popup.anchor = this.#trigger;
|
|
18217
19494
|
}
|
|
18218
19495
|
|
|
19496
|
+
this.#panel = document.createElement("div");
|
|
19497
|
+
this.#panel.className = "fig-menu-options";
|
|
19498
|
+
this.#panel.setAttribute("role", "presentation");
|
|
19499
|
+
this.#popup.appendChild(this.#panel);
|
|
18219
19500
|
this.#popup.addEventListener("close", this.#boundPopupClose);
|
|
18220
19501
|
this.appendChild(this.#popup);
|
|
18221
19502
|
}
|
|
18222
19503
|
|
|
18223
19504
|
#moveItemsToPopup() {
|
|
19505
|
+
if (!this.#panel) return;
|
|
18224
19506
|
const items = Array.from(this.querySelectorAll(
|
|
18225
19507
|
":scope > fig-menu-item, :scope > fig-menu-separator"
|
|
18226
19508
|
));
|
|
18227
19509
|
for (const item of items) {
|
|
18228
|
-
this.#
|
|
19510
|
+
this.#panel.appendChild(item);
|
|
18229
19511
|
}
|
|
18230
19512
|
}
|
|
18231
19513
|
|
|
@@ -18265,7 +19547,7 @@ class FigMenu extends HTMLElement {
|
|
|
18265
19547
|
(node.tagName === "FIG-MENU-ITEM" || node.tagName === "FIG-MENU-SEPARATOR") &&
|
|
18266
19548
|
node.parentElement === this
|
|
18267
19549
|
) {
|
|
18268
|
-
this.#
|
|
19550
|
+
this.#panel?.appendChild(node);
|
|
18269
19551
|
} else if (!this.#trigger && node.parentElement === this) {
|
|
18270
19552
|
this.#detectTrigger();
|
|
18271
19553
|
if (this.#trigger) {
|
|
@@ -18280,13 +19562,68 @@ class FigMenu extends HTMLElement {
|
|
|
18280
19562
|
}
|
|
18281
19563
|
}
|
|
18282
19564
|
}
|
|
19565
|
+
this.#syncOverflow();
|
|
18283
19566
|
});
|
|
18284
19567
|
this.#observer.observe(this, { childList: true });
|
|
18285
19568
|
}
|
|
18286
19569
|
|
|
19570
|
+
#setupOverflow() {
|
|
19571
|
+
if (!this.#panel) return;
|
|
19572
|
+
this.#ensureNavButtons();
|
|
19573
|
+
this.#panel.addEventListener("scroll", this.#boundSyncOverflow, {
|
|
19574
|
+
passive: true,
|
|
19575
|
+
});
|
|
19576
|
+
this.#resizeObserver?.disconnect();
|
|
19577
|
+
this.#resizeObserver = new ResizeObserver(() => this.#syncOverflow());
|
|
19578
|
+
this.#resizeObserver.observe(this.#panel);
|
|
19579
|
+
requestAnimationFrame(() => this.#syncOverflow());
|
|
19580
|
+
}
|
|
19581
|
+
|
|
19582
|
+
#teardownOverflow() {
|
|
19583
|
+
this.#panel?.removeEventListener("scroll", this.#boundSyncOverflow);
|
|
19584
|
+
this.#resizeObserver?.disconnect();
|
|
19585
|
+
this.#resizeObserver = null;
|
|
19586
|
+
this.#removeNavButtons();
|
|
19587
|
+
}
|
|
19588
|
+
|
|
19589
|
+
#syncOverflow() {
|
|
19590
|
+
return figSyncOverflowState(this.#panel, this.#panel, "y");
|
|
19591
|
+
}
|
|
19592
|
+
|
|
19593
|
+
#ensureNavButtons() {
|
|
19594
|
+
if (
|
|
19595
|
+
this.#navStart &&
|
|
19596
|
+
this.#navEnd &&
|
|
19597
|
+
this.#panel?.contains(this.#navStart) &&
|
|
19598
|
+
this.#panel?.contains(this.#navEnd)
|
|
19599
|
+
) {
|
|
19600
|
+
return;
|
|
19601
|
+
}
|
|
19602
|
+
this.#removeNavButtons();
|
|
19603
|
+
const buttons = createFigOverflowButtons({
|
|
19604
|
+
owner: "menu",
|
|
19605
|
+
startLabel: "Scroll up",
|
|
19606
|
+
endLabel: "Scroll down",
|
|
19607
|
+
onStart: () => figScrollOverflowPage(this.#panel, "y", -1),
|
|
19608
|
+
onEnd: () => figScrollOverflowPage(this.#panel, "y", 1),
|
|
19609
|
+
});
|
|
19610
|
+
this.#navStart = buttons.start;
|
|
19611
|
+
this.#navEnd = buttons.end;
|
|
19612
|
+
this.#panel.prepend(this.#navStart);
|
|
19613
|
+
this.#panel.append(this.#navEnd);
|
|
19614
|
+
}
|
|
19615
|
+
|
|
19616
|
+
#removeNavButtons() {
|
|
19617
|
+
this.#navStart?.remove();
|
|
19618
|
+
this.#navEnd?.remove();
|
|
19619
|
+
this.#navStart = null;
|
|
19620
|
+
this.#navEnd = null;
|
|
19621
|
+
this.#panel?.classList.remove("overflow-start", "overflow-end");
|
|
19622
|
+
}
|
|
19623
|
+
|
|
18287
19624
|
#getItems() {
|
|
18288
|
-
if (!this.#
|
|
18289
|
-
return Array.from(this.#
|
|
19625
|
+
if (!this.#panel) return [];
|
|
19626
|
+
return Array.from(this.#panel.querySelectorAll("fig-menu-item")).filter(
|
|
18290
19627
|
(item) =>
|
|
18291
19628
|
!item.hasAttribute("disabled") || item.getAttribute("disabled") === "false",
|
|
18292
19629
|
);
|
|
@@ -18310,12 +19647,18 @@ class FigMenu extends HTMLElement {
|
|
|
18310
19647
|
if (!items.length) return;
|
|
18311
19648
|
const wrapped = (index + items.length) % items.length;
|
|
18312
19649
|
this.#focusedIndex = wrapped;
|
|
18313
|
-
items[wrapped]
|
|
19650
|
+
const item = items[wrapped];
|
|
19651
|
+
item.focus();
|
|
19652
|
+
figScrollElementToCenter(this.#panel, item, "y");
|
|
18314
19653
|
}
|
|
18315
19654
|
|
|
18316
19655
|
#syncDisabled() {
|
|
19656
|
+
const disabled = this.#isDisabled();
|
|
19657
|
+
if (disabled) {
|
|
19658
|
+
if (this.open) this.removeAttribute("open");
|
|
19659
|
+
else this.#closeMenu();
|
|
19660
|
+
}
|
|
18317
19661
|
if (!this.#trigger) return;
|
|
18318
|
-
const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
|
|
18319
19662
|
if (disabled) {
|
|
18320
19663
|
this.#trigger.setAttribute("disabled", "");
|
|
18321
19664
|
this.#trigger.setAttribute("aria-disabled", "true");
|
|
@@ -18326,9 +19669,15 @@ class FigMenu extends HTMLElement {
|
|
|
18326
19669
|
}
|
|
18327
19670
|
}
|
|
18328
19671
|
|
|
19672
|
+
#isDisabled() {
|
|
19673
|
+
return (
|
|
19674
|
+
this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
|
|
19675
|
+
);
|
|
19676
|
+
}
|
|
19677
|
+
|
|
18329
19678
|
#handleTriggerClick(e) {
|
|
18330
19679
|
if (this.#usesContextMenuTrigger()) return;
|
|
18331
|
-
if (this
|
|
19680
|
+
if (this.#isDisabled()) return;
|
|
18332
19681
|
e.stopPropagation();
|
|
18333
19682
|
const popupShowing = this.#popup?.matches?.(":open") ?? false;
|
|
18334
19683
|
if (this.open && !popupShowing) {
|
|
@@ -18347,7 +19696,7 @@ class FigMenu extends HTMLElement {
|
|
|
18347
19696
|
|
|
18348
19697
|
#handleTriggerContextMenu(e) {
|
|
18349
19698
|
if (!this.#usesContextMenuTrigger()) return;
|
|
18350
|
-
if (this
|
|
19699
|
+
if (this.#isDisabled()) return;
|
|
18351
19700
|
e.preventDefault();
|
|
18352
19701
|
e.stopPropagation();
|
|
18353
19702
|
this.#showAtAfterPointerRelease(e.clientX, e.clientY);
|
|
@@ -18381,6 +19730,7 @@ class FigMenu extends HTMLElement {
|
|
|
18381
19730
|
}
|
|
18382
19731
|
|
|
18383
19732
|
#handlePopupClick(e) {
|
|
19733
|
+
if (this.#isDisabled()) return;
|
|
18384
19734
|
const item = e.target.closest("fig-menu-item");
|
|
18385
19735
|
if (!item) return;
|
|
18386
19736
|
if (item.hasAttribute("disabled") && item.getAttribute("disabled") !== "false") return;
|
|
@@ -18389,6 +19739,7 @@ class FigMenu extends HTMLElement {
|
|
|
18389
19739
|
}
|
|
18390
19740
|
|
|
18391
19741
|
#handleMenuKeydown(e) {
|
|
19742
|
+
if (this.#isDisabled()) return;
|
|
18392
19743
|
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
18393
19744
|
if (e.currentTarget === this && this.#popup?.contains(e.target)) return;
|
|
18394
19745
|
if (!this.open || !this.#popup?.matches?.(":open")) {
|
|
@@ -18474,6 +19825,7 @@ class FigMenu extends HTMLElement {
|
|
|
18474
19825
|
}
|
|
18475
19826
|
|
|
18476
19827
|
showAt(x, y) {
|
|
19828
|
+
if (this.#isDisabled()) return;
|
|
18477
19829
|
this.#virtualAnchor = {
|
|
18478
19830
|
getBoundingClientRect: () => ({
|
|
18479
19831
|
width: 0,
|
|
@@ -18491,12 +19843,16 @@ class FigMenu extends HTMLElement {
|
|
|
18491
19843
|
}
|
|
18492
19844
|
if (this.open) this.open = false;
|
|
18493
19845
|
requestAnimationFrame(() => {
|
|
19846
|
+
if (this.#isDisabled()) return;
|
|
18494
19847
|
this.open = true;
|
|
18495
19848
|
});
|
|
18496
19849
|
}
|
|
18497
19850
|
|
|
18498
19851
|
#openMenu() {
|
|
18499
|
-
if (!this.#popup)
|
|
19852
|
+
if (!this.#popup || this.#isDisabled()) {
|
|
19853
|
+
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
19854
|
+
return;
|
|
19855
|
+
}
|
|
18500
19856
|
this.#popup.open = true;
|
|
18501
19857
|
document.addEventListener("keydown", this.#boundMenuKeydown, true);
|
|
18502
19858
|
if (this.#trigger) {
|
|
@@ -18504,6 +19860,7 @@ class FigMenu extends HTMLElement {
|
|
|
18504
19860
|
}
|
|
18505
19861
|
this.#focusedIndex = -1;
|
|
18506
19862
|
requestAnimationFrame(() => {
|
|
19863
|
+
this.#syncOverflow();
|
|
18507
19864
|
if (!this.#trigger?.matches?.(":focus-visible")) return;
|
|
18508
19865
|
this.#focusItemAt(0);
|
|
18509
19866
|
});
|