@rogieking/figui3 6.21.1 → 6.22.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 +338 -200
- 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 +128 -39
- 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 +1582 -277
- package/package.json +1 -1
package/fig.js
CHANGED
|
@@ -330,9 +330,11 @@ function figSupportsPopover() {
|
|
|
330
330
|
class FigButton extends HTMLElement {
|
|
331
331
|
type;
|
|
332
332
|
#selected;
|
|
333
|
+
#slottedDisabledStates = new WeakMap();
|
|
333
334
|
#a11yAttributes = ["aria-label", "aria-labelledby", "aria-describedby", "title"];
|
|
334
335
|
#boundHandleControlKeydown = this.#handleControlKeydown.bind(this);
|
|
335
336
|
#boundHandleClick = this.#handleClick.bind(this);
|
|
337
|
+
#boundHandleSlotChange = () => this.#syncSlottedControlDisabled();
|
|
336
338
|
#boundHandleFocus = () => {
|
|
337
339
|
if (this.button?.matches(":focus-visible")) {
|
|
338
340
|
this.setAttribute("data-focus-visible", "");
|
|
@@ -398,6 +400,9 @@ class FigButton extends HTMLElement {
|
|
|
398
400
|
this.button.addEventListener("blur", this.#boundHandleBlur);
|
|
399
401
|
}
|
|
400
402
|
|
|
403
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
404
|
+
slot?.removeEventListener("slotchange", this.#boundHandleSlotChange);
|
|
405
|
+
slot?.addEventListener("slotchange", this.#boundHandleSlotChange);
|
|
401
406
|
this.removeEventListener("keydown", this.#boundHandleControlKeydown);
|
|
402
407
|
this.addEventListener("keydown", this.#boundHandleControlKeydown);
|
|
403
408
|
|
|
@@ -514,9 +519,30 @@ class FigButton extends HTMLElement {
|
|
|
514
519
|
this.button.type = "button";
|
|
515
520
|
this.button.setAttribute("type", "button");
|
|
516
521
|
}
|
|
522
|
+
this.#syncSlottedControlDisabled();
|
|
517
523
|
this.#syncA11yAttributes();
|
|
518
524
|
this.#syncPressedState();
|
|
519
525
|
}
|
|
526
|
+
#syncSlottedControlDisabled() {
|
|
527
|
+
const control = this.#getSlottedControl();
|
|
528
|
+
if (!control) return;
|
|
529
|
+
const disabled = this.#isDisabled();
|
|
530
|
+
if (disabled) {
|
|
531
|
+
if (!this.#slottedDisabledStates.has(control)) {
|
|
532
|
+
this.#slottedDisabledStates.set(
|
|
533
|
+
control,
|
|
534
|
+
control.hasAttribute("disabled") &&
|
|
535
|
+
control.getAttribute("disabled") !== "false",
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
control.setAttribute("disabled", "");
|
|
539
|
+
} else if (this.#slottedDisabledStates.has(control)) {
|
|
540
|
+
const wasDisabled = this.#slottedDisabledStates.get(control);
|
|
541
|
+
this.#slottedDisabledStates.delete(control);
|
|
542
|
+
if (wasDisabled) control.setAttribute("disabled", "");
|
|
543
|
+
else control.removeAttribute("disabled");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
520
546
|
static get observedAttributes() {
|
|
521
547
|
return [
|
|
522
548
|
"disabled",
|
|
@@ -555,6 +581,9 @@ class FigButton extends HTMLElement {
|
|
|
555
581
|
}
|
|
556
582
|
disconnectedCallback() {
|
|
557
583
|
this.removeEventListener("keydown", this.#boundHandleControlKeydown);
|
|
584
|
+
this.shadowRoot
|
|
585
|
+
?.querySelector("slot")
|
|
586
|
+
?.removeEventListener("slotchange", this.#boundHandleSlotChange);
|
|
558
587
|
}
|
|
559
588
|
}
|
|
560
589
|
figDefineElement("fig-button", FigButton);
|
|
@@ -570,8 +599,6 @@ class FigDropdown extends HTMLElement {
|
|
|
570
599
|
#boundHandleSelectInput;
|
|
571
600
|
#boundHandleSelectChange;
|
|
572
601
|
#boundHandleSelectKeydown;
|
|
573
|
-
#selectedContentEnabled = false;
|
|
574
|
-
#selectedContentEl = null;
|
|
575
602
|
|
|
576
603
|
get label() {
|
|
577
604
|
return this.#label;
|
|
@@ -591,54 +618,6 @@ class FigDropdown extends HTMLElement {
|
|
|
591
618
|
this.#boundSlotChange = this.slotChange.bind(this);
|
|
592
619
|
}
|
|
593
620
|
|
|
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
621
|
#addEventListeners() {
|
|
643
622
|
this.select.addEventListener("input", this.#boundHandleSelectInput);
|
|
644
623
|
this.select.addEventListener("change", this.#boundHandleSelectChange);
|
|
@@ -703,8 +682,6 @@ class FigDropdown extends HTMLElement {
|
|
|
703
682
|
this.select.firstChild.remove();
|
|
704
683
|
}
|
|
705
684
|
|
|
706
|
-
this.#enableSelectedContentIfNeeded();
|
|
707
|
-
|
|
708
685
|
if (this.type === "dropdown") {
|
|
709
686
|
const hiddenOption = document.createElement("option");
|
|
710
687
|
hiddenOption.setAttribute("hidden", "true");
|
|
@@ -720,7 +697,6 @@ class FigDropdown extends HTMLElement {
|
|
|
720
697
|
if (selectedValue !== null) {
|
|
721
698
|
this.#syncSelectedValue(selectedValue);
|
|
722
699
|
}
|
|
723
|
-
this.#syncSelectedContent();
|
|
724
700
|
if (this.type === "dropdown") {
|
|
725
701
|
this.select.selectedIndex = -1;
|
|
726
702
|
}
|
|
@@ -743,7 +719,6 @@ class FigDropdown extends HTMLElement {
|
|
|
743
719
|
this.#selectedValue = selectedValue;
|
|
744
720
|
}
|
|
745
721
|
this.setAttribute("value", selectedValue);
|
|
746
|
-
this.#syncSelectedContent();
|
|
747
722
|
this.dispatchEvent(
|
|
748
723
|
new CustomEvent("input", {
|
|
749
724
|
detail: selectedValue,
|
|
@@ -771,7 +746,6 @@ class FigDropdown extends HTMLElement {
|
|
|
771
746
|
if (this.type === "dropdown") {
|
|
772
747
|
this.select.selectedIndex = -1;
|
|
773
748
|
}
|
|
774
|
-
this.#syncSelectedContent();
|
|
775
749
|
this.dispatchEvent(
|
|
776
750
|
new CustomEvent("change", {
|
|
777
751
|
detail: selectedValue,
|
|
@@ -784,7 +758,6 @@ class FigDropdown extends HTMLElement {
|
|
|
784
758
|
#handleSelectKeydown(e) {
|
|
785
759
|
if (this.closest('fig-button[type="select"]')) return;
|
|
786
760
|
if (e.key !== "Enter" || e.defaultPrevented) return;
|
|
787
|
-
if (this.#selectedContentEnabled && this.select.matches(":open")) return;
|
|
788
761
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
789
762
|
if (this.select.disabled || this.select.multiple) return;
|
|
790
763
|
if (typeof this.select.showPicker !== "function") return;
|
|
@@ -817,7 +790,7 @@ class FigDropdown extends HTMLElement {
|
|
|
817
790
|
this.setAttribute("value", value);
|
|
818
791
|
}
|
|
819
792
|
static get observedAttributes() {
|
|
820
|
-
return ["value", "type", "
|
|
793
|
+
return ["value", "type", "label", "disabled"];
|
|
821
794
|
}
|
|
822
795
|
#syncDisabled() {
|
|
823
796
|
const disabled =
|
|
@@ -830,7 +803,6 @@ class FigDropdown extends HTMLElement {
|
|
|
830
803
|
return;
|
|
831
804
|
}
|
|
832
805
|
if (this.select) this.select.value = value ?? "";
|
|
833
|
-
this.#syncSelectedContent();
|
|
834
806
|
}
|
|
835
807
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
836
808
|
if (name === "value") {
|
|
@@ -840,9 +812,6 @@ class FigDropdown extends HTMLElement {
|
|
|
840
812
|
this.type = newValue || "select";
|
|
841
813
|
if (this.isConnected) this.slotChange();
|
|
842
814
|
}
|
|
843
|
-
if (name === "experimental") {
|
|
844
|
-
this.slotChange();
|
|
845
|
-
}
|
|
846
815
|
if (name === "label") {
|
|
847
816
|
this.#label = newValue || "Menu";
|
|
848
817
|
this.select.setAttribute("aria-label", this.#label);
|
|
@@ -887,6 +856,8 @@ class FigTooltip extends HTMLElement {
|
|
|
887
856
|
#boundHidePopupOutsideClick;
|
|
888
857
|
#boundShowDelayedPopup;
|
|
889
858
|
#boundHandlePointerLeave;
|
|
859
|
+
#boundHandleFocus;
|
|
860
|
+
#boundHandleBlur;
|
|
890
861
|
#boundHandleTouchStart;
|
|
891
862
|
#boundHandleTouchMove;
|
|
892
863
|
#boundHandleTouchEnd;
|
|
@@ -896,6 +867,7 @@ class FigTooltip extends HTMLElement {
|
|
|
896
867
|
#parentDialog = null;
|
|
897
868
|
#triggerEl = null;
|
|
898
869
|
#childObserver = null;
|
|
870
|
+
#suppressFocusOpen = false;
|
|
899
871
|
#touchTimeout;
|
|
900
872
|
#isTouching = false;
|
|
901
873
|
constructor() {
|
|
@@ -908,6 +880,13 @@ class FigTooltip extends HTMLElement {
|
|
|
908
880
|
this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
|
|
909
881
|
this.#boundShowDelayedPopup = this.showDelayedPopup.bind(this);
|
|
910
882
|
this.#boundHandlePointerLeave = this.#handlePointerLeave.bind(this);
|
|
883
|
+
this.#boundHandleFocus = () => {
|
|
884
|
+
if (!this.#suppressFocusOpen) this.showDelayedPopup();
|
|
885
|
+
};
|
|
886
|
+
this.#boundHandleBlur = () => {
|
|
887
|
+
this.#suppressFocusOpen = false;
|
|
888
|
+
this.hidePopup();
|
|
889
|
+
};
|
|
911
890
|
this.#boundHandleTouchStart = this.#handleTouchStart.bind(this);
|
|
912
891
|
this.#boundHandleTouchMove = this.#handleTouchMove.bind(this);
|
|
913
892
|
this.#boundHandleTouchEnd = this.#handleTouchEnd.bind(this);
|
|
@@ -1002,6 +981,8 @@ class FigTooltip extends HTMLElement {
|
|
|
1002
981
|
trigger.addEventListener("touchcancel", this.#boundHandleTouchCancel, {
|
|
1003
982
|
passive: true,
|
|
1004
983
|
});
|
|
984
|
+
trigger.addEventListener("focus", this.#boundHandleFocus);
|
|
985
|
+
trigger.addEventListener("blur", this.#boundHandleBlur);
|
|
1005
986
|
} else if (this.action === "click") {
|
|
1006
987
|
trigger.addEventListener("click", this.#boundShowDelayedPopup);
|
|
1007
988
|
trigger.addEventListener("touchstart", this.#boundShowDelayedPopup, {
|
|
@@ -1020,6 +1001,8 @@ class FigTooltip extends HTMLElement {
|
|
|
1020
1001
|
trigger.removeEventListener("touchmove", this.#boundHandleTouchMove);
|
|
1021
1002
|
trigger.removeEventListener("touchend", this.#boundHandleTouchEnd);
|
|
1022
1003
|
trigger.removeEventListener("touchcancel", this.#boundHandleTouchCancel);
|
|
1004
|
+
trigger.removeEventListener("focus", this.#boundHandleFocus);
|
|
1005
|
+
trigger.removeEventListener("blur", this.#boundHandleBlur);
|
|
1023
1006
|
} else if (this.action === "click") {
|
|
1024
1007
|
trigger.removeEventListener("click", this.#boundShowDelayedPopup);
|
|
1025
1008
|
trigger.removeEventListener("touchstart", this.#boundShowDelayedPopup);
|
|
@@ -1421,6 +1404,16 @@ class FigTooltip extends HTMLElement {
|
|
|
1421
1404
|
if (!(node instanceof FigTooltip)) continue;
|
|
1422
1405
|
if (node.action !== "hover") continue;
|
|
1423
1406
|
if (node.#showPersisted) continue;
|
|
1407
|
+
node.#suppressFocusOpen = true;
|
|
1408
|
+
setTimeout(() => {
|
|
1409
|
+
const trigger = node.#triggerEl;
|
|
1410
|
+
if (
|
|
1411
|
+
trigger !== document.activeElement &&
|
|
1412
|
+
!trigger?.matches?.(":focus, :focus-within")
|
|
1413
|
+
) {
|
|
1414
|
+
node.#suppressFocusOpen = false;
|
|
1415
|
+
}
|
|
1416
|
+
}, 0);
|
|
1424
1417
|
if (node.isOpen || node.timeout) node.hidePopup();
|
|
1425
1418
|
}
|
|
1426
1419
|
for (const anchor of Array.from(FigTooltip.#programmaticAnchors)) {
|
|
@@ -5051,77 +5044,1277 @@ class FigOptions extends HTMLElement {
|
|
|
5051
5044
|
}
|
|
5052
5045
|
}
|
|
5053
5046
|
|
|
5054
|
-
#syncValueToChild() {
|
|
5055
|
-
if (!this.#childControl || this.#suppressEvents) return;
|
|
5056
|
-
const val = this.getAttribute("value") || "";
|
|
5057
|
-
this.#childControl.value = val;
|
|
5047
|
+
#syncValueToChild() {
|
|
5048
|
+
if (!this.#childControl || this.#suppressEvents) return;
|
|
5049
|
+
const val = this.getAttribute("value") || "";
|
|
5050
|
+
this.#childControl.value = val;
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
#syncAttrToChild(attr) {
|
|
5054
|
+
if (!this.#childControl) return;
|
|
5055
|
+
if (this.hasAttribute(attr)) {
|
|
5056
|
+
this.#childControl.setAttribute(attr, this.getAttribute(attr) || "");
|
|
5057
|
+
} else {
|
|
5058
|
+
this.#childControl.removeAttribute(attr);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
|
|
5062
|
+
#startResizeObserver() {
|
|
5063
|
+
this.#resizeObserver?.disconnect();
|
|
5064
|
+
this.#resizeObserver = new ResizeObserver(() => {
|
|
5065
|
+
this.#checkOverflow();
|
|
5066
|
+
});
|
|
5067
|
+
this.#resizeObserver.observe(this);
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
#isSegmentTruncated(seg) {
|
|
5071
|
+
const range = document.createRange();
|
|
5072
|
+
range.selectNodeContents(seg);
|
|
5073
|
+
const textWidth = range.getBoundingClientRect().width;
|
|
5074
|
+
const segRect = seg.getBoundingClientRect();
|
|
5075
|
+
const segWidth = segRect.width;
|
|
5076
|
+
const cs = getComputedStyle(seg);
|
|
5077
|
+
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
5078
|
+
const padR = parseFloat(cs.paddingRight) || 0;
|
|
5079
|
+
const contentWidth = segWidth - padL - padR;
|
|
5080
|
+
return textWidth > contentWidth + 0.5;
|
|
5081
|
+
}
|
|
5082
|
+
|
|
5083
|
+
#anySegmentTruncated() {
|
|
5084
|
+
const segments = this.querySelectorAll("fig-segment");
|
|
5085
|
+
for (const seg of segments) {
|
|
5086
|
+
if (this.#isSegmentTruncated(seg)) return true;
|
|
5087
|
+
}
|
|
5088
|
+
return false;
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
#checkOverflow() {
|
|
5092
|
+
if (this.#parsedOptions.length <= 1) return;
|
|
5093
|
+
|
|
5094
|
+
if (this.#currentMode === "segments") {
|
|
5095
|
+
const sc = this.#childControl;
|
|
5096
|
+
const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
|
|
5097
|
+
if (containerOverflow || this.#anySegmentTruncated()) {
|
|
5098
|
+
this.#naturalWidth = this.clientWidth;
|
|
5099
|
+
this.#renderDropdown();
|
|
5100
|
+
}
|
|
5101
|
+
} else {
|
|
5102
|
+
if (this.#naturalWidth > 0 && this.clientWidth >= this.#naturalWidth) {
|
|
5103
|
+
this.#renderSegments();
|
|
5104
|
+
requestAnimationFrame(() => {
|
|
5105
|
+
requestAnimationFrame(() => {
|
|
5106
|
+
const sc = this.#childControl;
|
|
5107
|
+
const containerOverflow = sc && sc.scrollWidth > sc.clientWidth + 1;
|
|
5108
|
+
if (containerOverflow || this.#anySegmentTruncated()) {
|
|
5109
|
+
this.#renderDropdown();
|
|
5110
|
+
}
|
|
5111
|
+
});
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
figDefineElement("fig-options", FigOptions);
|
|
5118
|
+
|
|
5119
|
+
/* Select — dropdown-styled trigger + fig-popup listbox */
|
|
5120
|
+
/** Parse options attr — same formats as fig-options / propskit-select. */
|
|
5121
|
+
function figSelectParseOptionsAttribute(raw) {
|
|
5122
|
+
const text = raw || "";
|
|
5123
|
+
if (text.startsWith("[")) {
|
|
5124
|
+
try {
|
|
5125
|
+
const parsed = JSON.parse(text);
|
|
5126
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
5127
|
+
} catch {
|
|
5128
|
+
/* fall through */
|
|
5129
|
+
}
|
|
5130
|
+
}
|
|
5131
|
+
const delimiter = text.includes("\n") ? "\n" : ",";
|
|
5132
|
+
return text
|
|
5133
|
+
.split(delimiter)
|
|
5134
|
+
.map((s) => s.trim())
|
|
5135
|
+
.filter(Boolean);
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5138
|
+
function figSelectOptionEntryValue(opt) {
|
|
5139
|
+
if (opt && typeof opt === "object") {
|
|
5140
|
+
return String(opt.value ?? opt.label ?? "");
|
|
5141
|
+
}
|
|
5142
|
+
return String(opt ?? "");
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
function figSelectOptionEntryLabel(opt) {
|
|
5146
|
+
if (opt && typeof opt === "object") {
|
|
5147
|
+
return String(opt.label ?? opt.value ?? "");
|
|
5148
|
+
}
|
|
5149
|
+
return String(opt ?? "");
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5152
|
+
/**
|
|
5153
|
+
* A selectable option for fig-select.
|
|
5154
|
+
* Supports light-DOM slots: `slot="prepend"` (leading) and `slot="append"` (trailing).
|
|
5155
|
+
* Use the `label` attribute for the closed-trigger label when option content is rich.
|
|
5156
|
+
*
|
|
5157
|
+
* @attr {string} value - Option value
|
|
5158
|
+
* @attr {string} label - Optional display label for the select trigger
|
|
5159
|
+
* @attr {boolean} disabled - Whether the option is disabled
|
|
5160
|
+
* @attr {boolean} selected - Whether the option is selected
|
|
5161
|
+
*/
|
|
5162
|
+
class FigSelectOption extends HTMLElement {
|
|
5163
|
+
static get observedAttributes() {
|
|
5164
|
+
return ["value", "disabled", "selected", "label"];
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
get value() {
|
|
5168
|
+
const attr = this.getAttribute("value");
|
|
5169
|
+
if (attr !== null) return attr;
|
|
5170
|
+
return (this.textContent || "").trim();
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
set value(val) {
|
|
5174
|
+
if (val === null || val === undefined) {
|
|
5175
|
+
this.removeAttribute("value");
|
|
5176
|
+
} else {
|
|
5177
|
+
this.setAttribute("value", String(val));
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
|
|
5181
|
+
get disabled() {
|
|
5182
|
+
return figBooleanAttribute(this, "disabled");
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
set disabled(val) {
|
|
5186
|
+
if (val) this.setAttribute("disabled", "");
|
|
5187
|
+
else this.removeAttribute("disabled");
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
get selected() {
|
|
5191
|
+
return figBooleanAttribute(this, "selected");
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
set selected(val) {
|
|
5195
|
+
if (val) this.setAttribute("selected", "");
|
|
5196
|
+
else this.removeAttribute("selected");
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
connectedCallback() {
|
|
5200
|
+
if (!this.hasAttribute("role")) this.setAttribute("role", "option");
|
|
5201
|
+
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5202
|
+
this.#syncDisabled();
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5205
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5206
|
+
if (oldValue === newValue) return;
|
|
5207
|
+
if (name === "disabled") this.#syncDisabled();
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
#syncDisabled() {
|
|
5211
|
+
const disabled = this.disabled;
|
|
5212
|
+
if (disabled) {
|
|
5213
|
+
this.setAttribute("aria-disabled", "true");
|
|
5214
|
+
this.setAttribute("tabindex", "-1");
|
|
5215
|
+
} else {
|
|
5216
|
+
this.removeAttribute("aria-disabled");
|
|
5217
|
+
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
figDefineElement("fig-select-option", FigSelectOption);
|
|
5222
|
+
|
|
5223
|
+
function figSelectSyncOverflowState(host, scrollEl, threshold = 2) {
|
|
5224
|
+
if (!host || !scrollEl) return false;
|
|
5225
|
+
const scrollable = scrollEl.scrollHeight - scrollEl.clientHeight > threshold;
|
|
5226
|
+
const atStart = !scrollable || scrollEl.scrollTop <= threshold;
|
|
5227
|
+
const atEnd =
|
|
5228
|
+
!scrollable ||
|
|
5229
|
+
scrollEl.scrollTop + scrollEl.clientHeight >=
|
|
5230
|
+
scrollEl.scrollHeight - threshold;
|
|
5231
|
+
host.classList.toggle("overflow-start", !atStart);
|
|
5232
|
+
host.classList.toggle("overflow-end", !atEnd);
|
|
5233
|
+
return scrollable;
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
function figSelectScrollOverflowPage(scrollEl, direction = 1) {
|
|
5237
|
+
if (!scrollEl) return;
|
|
5238
|
+
scrollEl.scrollBy({
|
|
5239
|
+
top: scrollEl.clientHeight * 0.8 * direction,
|
|
5240
|
+
behavior: "smooth",
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
function figSelectCreateOverflowButtons({ onStart, onEnd } = {}) {
|
|
5245
|
+
const makeButton = (direction, onClick) => {
|
|
5246
|
+
const button = document.createElement("button");
|
|
5247
|
+
button.type = "button";
|
|
5248
|
+
button.className = `fig-overflow fig-overflow-${direction}`;
|
|
5249
|
+
button.dataset.figOverflow = direction;
|
|
5250
|
+
button.setAttribute("data-fig-select-nav", direction);
|
|
5251
|
+
button.setAttribute("tabindex", "-1");
|
|
5252
|
+
button.setAttribute(
|
|
5253
|
+
"aria-label",
|
|
5254
|
+
direction === "start" ? "Scroll up" : "Scroll down",
|
|
5255
|
+
);
|
|
5256
|
+
const icon = document.createElement("fig-icon");
|
|
5257
|
+
icon.setAttribute("name", "chevron");
|
|
5258
|
+
icon.setAttribute("size", "small");
|
|
5259
|
+
icon.className = "fig-overflow-chevron";
|
|
5260
|
+
button.appendChild(icon);
|
|
5261
|
+
button.addEventListener("click", (event) => {
|
|
5262
|
+
event.preventDefault();
|
|
5263
|
+
event.stopPropagation();
|
|
5264
|
+
onClick?.(event);
|
|
5265
|
+
});
|
|
5266
|
+
return button;
|
|
5267
|
+
};
|
|
5268
|
+
return {
|
|
5269
|
+
start: makeButton("start", onStart),
|
|
5270
|
+
end: makeButton("end", onEnd),
|
|
5271
|
+
};
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
/** Light-DOM panel wrapper projected into fig-select's popup; owns overflow buttons. */
|
|
5275
|
+
class FigSelectOptions extends HTMLElement {
|
|
5276
|
+
#navStart = null;
|
|
5277
|
+
#navEnd = null;
|
|
5278
|
+
#resizeObserver = null;
|
|
5279
|
+
#boundSyncOverflow = this.syncOverflow.bind(this);
|
|
5280
|
+
|
|
5281
|
+
connectedCallback() {
|
|
5282
|
+
if (!this.hasAttribute("slot")) this.setAttribute("slot", "panel");
|
|
5283
|
+
this.#unwrapLegacyChooser();
|
|
5284
|
+
this.#ensureNavButtons();
|
|
5285
|
+
this.addEventListener("scroll", this.#boundSyncOverflow, { passive: true });
|
|
5286
|
+
this.#resizeObserver?.disconnect();
|
|
5287
|
+
this.#resizeObserver = new ResizeObserver(() => this.syncOverflow());
|
|
5288
|
+
this.#resizeObserver.observe(this);
|
|
5289
|
+
requestAnimationFrame(() => this.syncOverflow());
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
disconnectedCallback() {
|
|
5293
|
+
this.removeEventListener("scroll", this.#boundSyncOverflow);
|
|
5294
|
+
this.#resizeObserver?.disconnect();
|
|
5295
|
+
this.#resizeObserver = null;
|
|
5296
|
+
this.#removeNavButtons();
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
syncOverflow() {
|
|
5300
|
+
return figSelectSyncOverflowState(this, this);
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
scrollToOption(option, behavior = "auto") {
|
|
5304
|
+
if (!option || !this.contains(option)) return;
|
|
5305
|
+
requestAnimationFrame(() => {
|
|
5306
|
+
if (!option.isConnected) return;
|
|
5307
|
+
if (this.scrollHeight <= this.clientHeight + 1) {
|
|
5308
|
+
this.syncOverflow();
|
|
5309
|
+
return;
|
|
5310
|
+
}
|
|
5311
|
+
const optionRect = option.getBoundingClientRect();
|
|
5312
|
+
const hostRect = this.getBoundingClientRect();
|
|
5313
|
+
const optionTop = optionRect.top - hostRect.top + this.scrollTop;
|
|
5314
|
+
const maxScroll = this.scrollHeight - this.clientHeight;
|
|
5315
|
+
const top = Math.max(
|
|
5316
|
+
0,
|
|
5317
|
+
Math.min(
|
|
5318
|
+
optionTop + optionRect.height / 2 - this.clientHeight / 2,
|
|
5319
|
+
maxScroll,
|
|
5320
|
+
),
|
|
5321
|
+
);
|
|
5322
|
+
this.scrollTo({ top, behavior });
|
|
5323
|
+
this.syncOverflow();
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
5326
|
+
|
|
5327
|
+
#unwrapLegacyChooser() {
|
|
5328
|
+
const chooser = this.querySelector(":scope > fig-chooser");
|
|
5329
|
+
if (!chooser) return;
|
|
5330
|
+
while (chooser.firstChild) {
|
|
5331
|
+
this.insertBefore(chooser.firstChild, chooser);
|
|
5332
|
+
}
|
|
5333
|
+
chooser.remove();
|
|
5334
|
+
}
|
|
5335
|
+
|
|
5336
|
+
#ensureNavButtons() {
|
|
5337
|
+
if (
|
|
5338
|
+
this.#navStart &&
|
|
5339
|
+
this.#navEnd &&
|
|
5340
|
+
this.contains(this.#navStart) &&
|
|
5341
|
+
this.contains(this.#navEnd)
|
|
5342
|
+
) {
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
this.#removeNavButtons();
|
|
5346
|
+
const buttons = figSelectCreateOverflowButtons({
|
|
5347
|
+
onStart: () => figSelectScrollOverflowPage(this, -1),
|
|
5348
|
+
onEnd: () => figSelectScrollOverflowPage(this, 1),
|
|
5349
|
+
});
|
|
5350
|
+
this.#navStart = buttons.start;
|
|
5351
|
+
this.#navEnd = buttons.end;
|
|
5352
|
+
this.prepend(this.#navStart);
|
|
5353
|
+
this.append(this.#navEnd);
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
#removeNavButtons() {
|
|
5357
|
+
this.#navStart?.remove();
|
|
5358
|
+
this.#navEnd?.remove();
|
|
5359
|
+
this.#navStart = null;
|
|
5360
|
+
this.#navEnd = null;
|
|
5361
|
+
this.classList.remove("overflow-start", "overflow-end");
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
figDefineElement("fig-select-options", FigSelectOptions);
|
|
5365
|
+
|
|
5366
|
+
class FigSelect extends HTMLElement {
|
|
5367
|
+
#button = null;
|
|
5368
|
+
#popup = null;
|
|
5369
|
+
#prependEl = null;
|
|
5370
|
+
#labelEl = null;
|
|
5371
|
+
#panelSlot = null;
|
|
5372
|
+
#observer = null;
|
|
5373
|
+
#initialized = false;
|
|
5374
|
+
#focusedIndex = -1;
|
|
5375
|
+
#syncingValue = false;
|
|
5376
|
+
#popupPositionPatched = false;
|
|
5377
|
+
#originalPositionPopup = null;
|
|
5378
|
+
/**
|
|
5379
|
+
* After open align, ignore content/scroll-driven positionPopup passes so
|
|
5380
|
+
* overflow paging isn't yanked back. Still realign when the trigger moves
|
|
5381
|
+
* or the viewport size changes (window resize, layout shift, page scroll).
|
|
5382
|
+
*/
|
|
5383
|
+
#freezeMenuPosition = false;
|
|
5384
|
+
#frozenLabelRect = null;
|
|
5385
|
+
#frozenViewport = null;
|
|
5386
|
+
#syncingOptions = false;
|
|
5387
|
+
#boundTriggerClick = this.#handleTriggerClick.bind(this);
|
|
5388
|
+
#boundOptionClick = this.#handleOptionClick.bind(this);
|
|
5389
|
+
#boundKeydown = this.#handleKeydown.bind(this);
|
|
5390
|
+
#boundPopupClose = this.#handlePopupClose.bind(this);
|
|
5391
|
+
#boundSlotChange = this.#handleSlotChange.bind(this);
|
|
5392
|
+
|
|
5393
|
+
static get observedAttributes() {
|
|
5394
|
+
return [
|
|
5395
|
+
"value",
|
|
5396
|
+
"disabled",
|
|
5397
|
+
"label",
|
|
5398
|
+
"options",
|
|
5399
|
+
"position",
|
|
5400
|
+
"offset",
|
|
5401
|
+
"closedby",
|
|
5402
|
+
"open",
|
|
5403
|
+
];
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
get value() {
|
|
5407
|
+
return this.getAttribute("value") ?? "";
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
set value(val) {
|
|
5411
|
+
if (val === null || val === undefined) this.removeAttribute("value");
|
|
5412
|
+
else this.setAttribute("value", String(val));
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
get open() {
|
|
5416
|
+
return figBooleanAttribute(this, "open");
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
set open(val) {
|
|
5420
|
+
if (val) this.setAttribute("open", "");
|
|
5421
|
+
else this.removeAttribute("open");
|
|
5422
|
+
}
|
|
5423
|
+
|
|
5424
|
+
connectedCallback() {
|
|
5425
|
+
if (!this.#initialized) this.#initialize();
|
|
5426
|
+
this.#ensurePanelSlotAttrs();
|
|
5427
|
+
this.#syncOptionsFromAttribute();
|
|
5428
|
+
this.#syncDisabled();
|
|
5429
|
+
this.#syncPopupAttrs();
|
|
5430
|
+
this.#syncValue();
|
|
5431
|
+
this.#setupObserver();
|
|
5432
|
+
if (this.open) this.#openList();
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5435
|
+
disconnectedCallback() {
|
|
5436
|
+
this.#teardownListeners();
|
|
5437
|
+
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
5438
|
+
this.#observer?.disconnect();
|
|
5439
|
+
this.#observer = null;
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
5443
|
+
if (oldValue === newValue || !this.#initialized) return;
|
|
5444
|
+
if (name === "options") {
|
|
5445
|
+
this.#syncOptionsFromAttribute();
|
|
5446
|
+
this.#syncValue();
|
|
5447
|
+
return;
|
|
5448
|
+
}
|
|
5449
|
+
if (name === "value" || name === "label") {
|
|
5450
|
+
this.#syncValue();
|
|
5451
|
+
return;
|
|
5452
|
+
}
|
|
5453
|
+
if (name === "disabled") {
|
|
5454
|
+
this.#syncDisabled();
|
|
5455
|
+
return;
|
|
5456
|
+
}
|
|
5457
|
+
if (name === "open") {
|
|
5458
|
+
if (newValue === null || newValue === "false") this.#closeList();
|
|
5459
|
+
else this.#openList();
|
|
5460
|
+
return;
|
|
5461
|
+
}
|
|
5462
|
+
if (name === "position" || name === "offset" || name === "closedby") {
|
|
5463
|
+
this.#syncPopupAttrs();
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
focus(options) {
|
|
5468
|
+
this.#button?.focus(options);
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
blur() {
|
|
5472
|
+
this.#button?.blur();
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5475
|
+
#isMenuChild(node) {
|
|
5476
|
+
return (
|
|
5477
|
+
node?.nodeType === 1 &&
|
|
5478
|
+
(node.tagName === "FIG-SELECT-OPTION" ||
|
|
5479
|
+
node.tagName === "FIG-MENU-SEPARATOR" ||
|
|
5480
|
+
node.tagName === "FIG-SELECT-OPTIONS")
|
|
5481
|
+
);
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
#ensurePanelSlotAttrs() {
|
|
5485
|
+
for (const panel of this.querySelectorAll(":scope > fig-select-options")) {
|
|
5486
|
+
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
|
|
5490
|
+
#getPanel() {
|
|
5491
|
+
const assigned = this.#panelSlot?.assignedElements({ flatten: true }) ?? [];
|
|
5492
|
+
const fromSlot = assigned.find(
|
|
5493
|
+
(el) => el.tagName === "FIG-SELECT-OPTIONS",
|
|
5494
|
+
);
|
|
5495
|
+
if (fromSlot) return fromSlot;
|
|
5496
|
+
return this.querySelector(":scope > fig-select-options");
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
#hasAuthoredOptions() {
|
|
5500
|
+
return Boolean(
|
|
5501
|
+
this.querySelector(
|
|
5502
|
+
":scope > fig-select-option:not([data-fig-generated]), :scope > fig-select-options > fig-select-option:not([data-fig-generated])",
|
|
5503
|
+
),
|
|
5504
|
+
);
|
|
5505
|
+
}
|
|
5506
|
+
|
|
5507
|
+
#ensureOptionsPanel() {
|
|
5508
|
+
let panel = this.#getPanel();
|
|
5509
|
+
if (panel) {
|
|
5510
|
+
if (!panel.hasAttribute("slot")) panel.setAttribute("slot", "panel");
|
|
5511
|
+
return panel;
|
|
5512
|
+
}
|
|
5513
|
+
panel = document.createElement("fig-select-options");
|
|
5514
|
+
panel.setAttribute("slot", "panel");
|
|
5515
|
+
panel.setAttribute("data-fig-generated", "");
|
|
5516
|
+
this.appendChild(panel);
|
|
5517
|
+
return panel;
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
/**
|
|
5521
|
+
* When no authored fig-select-option exists, build panel/options from the
|
|
5522
|
+
* options attribute (comma / newline / JSON — same as fig-options).
|
|
5523
|
+
*/
|
|
5524
|
+
#syncOptionsFromAttribute() {
|
|
5525
|
+
if (this.#hasAuthoredOptions()) return;
|
|
5526
|
+
|
|
5527
|
+
const hasOptionsAttr = this.hasAttribute("options");
|
|
5528
|
+
const panel = hasOptionsAttr
|
|
5529
|
+
? this.#ensureOptionsPanel()
|
|
5530
|
+
: this.#getPanel();
|
|
5531
|
+
if (!panel) return;
|
|
5532
|
+
|
|
5533
|
+
this.#syncingOptions = true;
|
|
5534
|
+
try {
|
|
5535
|
+
for (const opt of panel.querySelectorAll(
|
|
5536
|
+
":scope > fig-select-option[data-fig-generated]",
|
|
5537
|
+
)) {
|
|
5538
|
+
opt.remove();
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5541
|
+
if (!hasOptionsAttr) return;
|
|
5542
|
+
|
|
5543
|
+
const parsed = figSelectParseOptionsAttribute(this.getAttribute("options"));
|
|
5544
|
+
const endBtn = panel.querySelector(":scope > .fig-overflow-end");
|
|
5545
|
+
for (const entry of parsed) {
|
|
5546
|
+
const el = document.createElement("fig-select-option");
|
|
5547
|
+
el.setAttribute("data-fig-generated", "");
|
|
5548
|
+
el.setAttribute("value", figSelectOptionEntryValue(entry));
|
|
5549
|
+
el.textContent = figSelectOptionEntryLabel(entry);
|
|
5550
|
+
if (endBtn) panel.insertBefore(el, endBtn);
|
|
5551
|
+
else panel.appendChild(el);
|
|
5552
|
+
}
|
|
5553
|
+
} finally {
|
|
5554
|
+
this.#syncingOptions = false;
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
#initialize() {
|
|
5559
|
+
this.#initialized = true;
|
|
5560
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
5561
|
+
shadow.innerHTML = `
|
|
5562
|
+
<style>
|
|
5563
|
+
:host {
|
|
5564
|
+
display: inline-flex;
|
|
5565
|
+
position: relative;
|
|
5566
|
+
align-items: center;
|
|
5567
|
+
min-width: 0;
|
|
5568
|
+
}
|
|
5569
|
+
:host([full]:not([full="false"])) {
|
|
5570
|
+
display: flex;
|
|
5571
|
+
width: 100%;
|
|
5572
|
+
}
|
|
5573
|
+
.fig-select-trigger {
|
|
5574
|
+
display: flex;
|
|
5575
|
+
align-items: center;
|
|
5576
|
+
justify-content: flex-start;
|
|
5577
|
+
flex: 1;
|
|
5578
|
+
min-width: 0;
|
|
5579
|
+
width: var(--fig-select-trigger-width, 100%);
|
|
5580
|
+
height: 100%;
|
|
5581
|
+
margin: 0;
|
|
5582
|
+
padding: 0 var(--spacer-4, 1rem) 0 var(--spacer-2, 0.5rem);
|
|
5583
|
+
border: 0;
|
|
5584
|
+
border-radius: inherit;
|
|
5585
|
+
background: transparent;
|
|
5586
|
+
box-shadow: none;
|
|
5587
|
+
color: inherit;
|
|
5588
|
+
font: inherit;
|
|
5589
|
+
font-weight: inherit;
|
|
5590
|
+
text-align: left;
|
|
5591
|
+
white-space: nowrap;
|
|
5592
|
+
overflow: hidden;
|
|
5593
|
+
text-overflow: ellipsis;
|
|
5594
|
+
cursor: default;
|
|
5595
|
+
}
|
|
5596
|
+
.fig-select-trigger:has(.fig-select-prepend:not(:empty)) {
|
|
5597
|
+
padding-left: 0;
|
|
5598
|
+
}
|
|
5599
|
+
.fig-select-trigger:hover,
|
|
5600
|
+
.fig-select-trigger:active,
|
|
5601
|
+
.fig-select-trigger:active:hover {
|
|
5602
|
+
background: transparent;
|
|
5603
|
+
box-shadow: none;
|
|
5604
|
+
color: inherit;
|
|
5605
|
+
}
|
|
5606
|
+
.fig-select-trigger:focus-visible,
|
|
5607
|
+
.fig-select-trigger[data-focus-visible] {
|
|
5608
|
+
outline: var(--figma-focus-outline);
|
|
5609
|
+
outline-offset: var(--figma-focus-outline-offset);
|
|
5610
|
+
}
|
|
5611
|
+
:host([disabled]:not([disabled="false"])) .fig-select-trigger,
|
|
5612
|
+
:host([disabled]:not([disabled="false"])) .fig-select-label {
|
|
5613
|
+
color: var(--figma-color-text-tertiary);
|
|
5614
|
+
}
|
|
5615
|
+
.fig-select-label {
|
|
5616
|
+
display: block;
|
|
5617
|
+
width: 100%;
|
|
5618
|
+
min-width: 0;
|
|
5619
|
+
overflow: hidden;
|
|
5620
|
+
text-overflow: ellipsis;
|
|
5621
|
+
white-space: nowrap;
|
|
5622
|
+
text-align: left;
|
|
5623
|
+
}
|
|
5624
|
+
.fig-select-prepend {
|
|
5625
|
+
display: inline-flex;
|
|
5626
|
+
flex: 0 0 auto;
|
|
5627
|
+
align-items: center;
|
|
5628
|
+
margin-right: var(--spacer-1, 0.25rem);
|
|
5629
|
+
pointer-events: none;
|
|
5630
|
+
}
|
|
5631
|
+
.fig-select-prepend:empty {
|
|
5632
|
+
display: none;
|
|
5633
|
+
}
|
|
5634
|
+
/* Listbox chrome from document fig-select::part(listbox).
|
|
5635
|
+
Overflow UI lives on slotted fig-select-options.
|
|
5636
|
+
Never set display except when open — closed <dialog> must stay display:none. */
|
|
5637
|
+
dialog[is="fig-popup"] {
|
|
5638
|
+
flex-direction: column;
|
|
5639
|
+
overflow: hidden;
|
|
5640
|
+
}
|
|
5641
|
+
dialog[is="fig-popup"][open] {
|
|
5642
|
+
display: flex;
|
|
5643
|
+
}
|
|
5644
|
+
::slotted(fig-select-options) {
|
|
5645
|
+
flex: 1 1 auto;
|
|
5646
|
+
min-height: 0;
|
|
5647
|
+
max-height: inherit;
|
|
5648
|
+
}
|
|
5649
|
+
</style>
|
|
5650
|
+
`;
|
|
5651
|
+
|
|
5652
|
+
const button = document.createElement("fig-button");
|
|
5653
|
+
button.className = "fig-select-trigger";
|
|
5654
|
+
button.setAttribute("part", "trigger");
|
|
5655
|
+
button.setAttribute("variant", "ghost");
|
|
5656
|
+
button.setAttribute("aria-haspopup", "listbox");
|
|
5657
|
+
button.setAttribute("aria-expanded", "false");
|
|
5658
|
+
|
|
5659
|
+
const prependEl = document.createElement("span");
|
|
5660
|
+
prependEl.className = "fig-select-prepend";
|
|
5661
|
+
prependEl.setAttribute("part", "prepend");
|
|
5662
|
+
prependEl.setAttribute("aria-hidden", "true");
|
|
5663
|
+
|
|
5664
|
+
const labelEl = document.createElement("span");
|
|
5665
|
+
labelEl.className = "fig-select-label";
|
|
5666
|
+
labelEl.setAttribute("part", "label");
|
|
5667
|
+
button.append(prependEl, labelEl);
|
|
5668
|
+
|
|
5669
|
+
const popup = document.createElement("dialog", { is: "fig-popup" });
|
|
5670
|
+
popup.setAttribute("is", "fig-popup");
|
|
5671
|
+
popup.setAttribute("part", "listbox");
|
|
5672
|
+
popup.setAttribute("theme", "menu");
|
|
5673
|
+
popup.setAttribute("role", "listbox");
|
|
5674
|
+
// Top-layer via popover so the menu escapes ancestor contain/overflow
|
|
5675
|
+
// (e.g. fig-fill-picker-dialog). Stays in shadow so option slots still work —
|
|
5676
|
+
// unlike tooltips, we cannot portal this popup to the overlay root.
|
|
5677
|
+
if ("popover" in HTMLElement.prototype) {
|
|
5678
|
+
popup.setAttribute("popover", "manual");
|
|
5679
|
+
}
|
|
5680
|
+
popup.id = figUniqueId();
|
|
5681
|
+
button.setAttribute("aria-controls", popup.id);
|
|
5682
|
+
|
|
5683
|
+
const panelSlot = document.createElement("slot");
|
|
5684
|
+
panelSlot.setAttribute("name", "panel");
|
|
5685
|
+
popup.appendChild(panelSlot);
|
|
5686
|
+
|
|
5687
|
+
shadow.append(button, popup);
|
|
5688
|
+
|
|
5689
|
+
this.#button = button;
|
|
5690
|
+
this.#prependEl = prependEl;
|
|
5691
|
+
this.#labelEl = labelEl;
|
|
5692
|
+
this.#popup = popup;
|
|
5693
|
+
this.#panelSlot = panelSlot;
|
|
5694
|
+
popup.anchor = button;
|
|
5695
|
+
|
|
5696
|
+
this.#ensurePanelSlotAttrs();
|
|
5697
|
+
this.#setupListeners();
|
|
5698
|
+
this.#installPopupPositioning();
|
|
5699
|
+
|
|
5700
|
+
if (!this.hasAttribute("value")) {
|
|
5701
|
+
const selected = this.#getOptions().find((opt) =>
|
|
5702
|
+
figBooleanAttribute(opt, "selected"),
|
|
5703
|
+
);
|
|
5704
|
+
if (selected) this.setAttribute("value", selected.value);
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5708
|
+
#installPopupPositioning() {
|
|
5709
|
+
if (!this.#popup || this.#popupPositionPatched) return;
|
|
5710
|
+
if (typeof this.#popup.positionPopup !== "function") return;
|
|
5711
|
+
this.#originalPositionPopup = this.#popup.positionPopup.bind(this.#popup);
|
|
5712
|
+
this.#popup.positionPopup = () => {
|
|
5713
|
+
if (!this.open) {
|
|
5714
|
+
this.#originalPositionPopup?.();
|
|
5715
|
+
return;
|
|
5716
|
+
}
|
|
5717
|
+
this.#positionPopupOverSelected();
|
|
5718
|
+
};
|
|
5719
|
+
this.#popupPositionPatched = true;
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
#getOptionTextRect(option) {
|
|
5723
|
+
if (!option) return null;
|
|
5724
|
+
const range = document.createRange();
|
|
5725
|
+
range.selectNodeContents(option);
|
|
5726
|
+
const rects = [...range.getClientRects()].filter(
|
|
5727
|
+
(rect) => rect.width > 0 && rect.height > 0,
|
|
5728
|
+
);
|
|
5729
|
+
if (rects.length) return rects[0];
|
|
5730
|
+
return option.getBoundingClientRect();
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5733
|
+
#getViewportMargins() {
|
|
5734
|
+
if (typeof this.#popup?.parseViewportMargins === "function") {
|
|
5735
|
+
return this.#popup.parseViewportMargins();
|
|
5736
|
+
}
|
|
5737
|
+
return { top: 8, right: 8, bottom: 8, left: 8 };
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
#readLabelRectSnapshot() {
|
|
5741
|
+
const rect = this.#labelEl?.getBoundingClientRect();
|
|
5742
|
+
if (!rect) return null;
|
|
5743
|
+
return {
|
|
5744
|
+
x: rect.x,
|
|
5745
|
+
y: rect.y,
|
|
5746
|
+
width: rect.width,
|
|
5747
|
+
height: rect.height,
|
|
5748
|
+
};
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
#readViewportSnapshot() {
|
|
5752
|
+
const vv = window.visualViewport;
|
|
5753
|
+
return {
|
|
5754
|
+
width: vv?.width ?? window.innerWidth,
|
|
5755
|
+
height: vv?.height ?? window.innerHeight,
|
|
5756
|
+
offsetLeft: vv?.offsetLeft ?? 0,
|
|
5757
|
+
offsetTop: vv?.offsetTop ?? 0,
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5761
|
+
#rectSnapshotChanged(prev, next, epsilon = 0.25) {
|
|
5762
|
+
if (!prev && !next) return false;
|
|
5763
|
+
if (!prev || !next) return true;
|
|
5764
|
+
return (
|
|
5765
|
+
Math.abs(prev.x - next.x) > epsilon ||
|
|
5766
|
+
Math.abs(prev.y - next.y) > epsilon ||
|
|
5767
|
+
Math.abs(prev.width - next.width) > epsilon ||
|
|
5768
|
+
Math.abs(prev.height - next.height) > epsilon
|
|
5769
|
+
);
|
|
5770
|
+
}
|
|
5771
|
+
|
|
5772
|
+
#viewportSnapshotChanged(prev, next, epsilon = 0.25) {
|
|
5773
|
+
if (!prev && !next) return false;
|
|
5774
|
+
if (!prev || !next) return true;
|
|
5775
|
+
return (
|
|
5776
|
+
Math.abs(prev.width - next.width) > epsilon ||
|
|
5777
|
+
Math.abs(prev.height - next.height) > epsilon ||
|
|
5778
|
+
Math.abs(prev.offsetLeft - next.offsetLeft) > epsilon ||
|
|
5779
|
+
Math.abs(prev.offsetTop - next.offsetTop) > epsilon
|
|
5780
|
+
);
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
#shouldSkipFrozenPositionPass() {
|
|
5784
|
+
if (!this.#freezeMenuPosition) return false;
|
|
5785
|
+
const labelMoved = this.#rectSnapshotChanged(
|
|
5786
|
+
this.#frozenLabelRect,
|
|
5787
|
+
this.#readLabelRectSnapshot(),
|
|
5788
|
+
);
|
|
5789
|
+
const viewportChanged = this.#viewportSnapshotChanged(
|
|
5790
|
+
this.#frozenViewport,
|
|
5791
|
+
this.#readViewportSnapshot(),
|
|
5792
|
+
);
|
|
5793
|
+
// Skip only when neither the trigger nor the viewport moved — typical of
|
|
5794
|
+
// overflow scroll / content sync fighting the open-time alignment.
|
|
5795
|
+
return !labelMoved && !viewportChanged;
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5798
|
+
#rememberFrozenGeometry() {
|
|
5799
|
+
this.#frozenLabelRect = this.#readLabelRectSnapshot();
|
|
5800
|
+
this.#frozenViewport = this.#readViewportSnapshot();
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
#positionPopupOverSelected() {
|
|
5804
|
+
// Content ResizeObserver / overflow scroll re-enter here; keep the
|
|
5805
|
+
// open-time alignment unless the trigger or viewport actually changed.
|
|
5806
|
+
if (this.#shouldSkipFrozenPositionPass()) return;
|
|
5807
|
+
|
|
5808
|
+
const popup = this.#popup;
|
|
5809
|
+
const label = this.#labelEl;
|
|
5810
|
+
if (!popup || !label) {
|
|
5811
|
+
this.#originalPositionPopup?.();
|
|
5812
|
+
return;
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5815
|
+
const options = this.#getOptions();
|
|
5816
|
+
const selected =
|
|
5817
|
+
options.find((opt) => this.#optionValue(opt) === this.value) ||
|
|
5818
|
+
options[0];
|
|
5819
|
+
if (!selected) {
|
|
5820
|
+
this.#originalPositionPopup?.();
|
|
5821
|
+
return;
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5824
|
+
// Lay out with the default positioning first so option metrics are valid.
|
|
5825
|
+
this.#originalPositionPopup?.();
|
|
5826
|
+
|
|
5827
|
+
const popupRect = popup.getBoundingClientRect();
|
|
5828
|
+
const labelRect = label.getBoundingClientRect();
|
|
5829
|
+
const optionTextRect = this.#getOptionTextRect(selected);
|
|
5830
|
+
if (
|
|
5831
|
+
!popupRect.width ||
|
|
5832
|
+
!popupRect.height ||
|
|
5833
|
+
!labelRect.width ||
|
|
5834
|
+
!optionTextRect
|
|
5835
|
+
) {
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
const selectedOffsetX = optionTextRect.left - popupRect.left;
|
|
5840
|
+
const selectedOffsetY = optionTextRect.top - popupRect.top;
|
|
5841
|
+
const full = figBooleanAttribute(this, "full");
|
|
5842
|
+
// [full]: pin menu to host width/edges. Otherwise overlay selected
|
|
5843
|
+
// option text on the trigger label (blend-mode style).
|
|
5844
|
+
let left = full
|
|
5845
|
+
? this.getBoundingClientRect().left
|
|
5846
|
+
: labelRect.left - selectedOffsetX;
|
|
5847
|
+
let top = labelRect.top - selectedOffsetY;
|
|
5848
|
+
|
|
5849
|
+
// Keep the whole menu in-view when aligning over the selected option
|
|
5850
|
+
// would otherwise push it past a viewport edge (corners / far sides).
|
|
5851
|
+
const margins = this.#getViewportMargins();
|
|
5852
|
+
if (typeof popup.clampToViewport === "function") {
|
|
5853
|
+
({ left, top } = popup.clampToViewport({ left, top }, popupRect, margins));
|
|
5854
|
+
} else {
|
|
5855
|
+
const minLeft = margins.left;
|
|
5856
|
+
const minTop = margins.top;
|
|
5857
|
+
const maxLeft = window.innerWidth - popupRect.width - margins.right;
|
|
5858
|
+
const maxTop = window.innerHeight - popupRect.height - margins.bottom;
|
|
5859
|
+
left = Math.min(Math.max(left, minLeft), Math.max(minLeft, maxLeft));
|
|
5860
|
+
top = Math.min(Math.max(top, minTop), Math.max(minTop, maxTop));
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
// !important: fig-select::part(listbox) and dialog UA rules can otherwise
|
|
5864
|
+
// keep the menu at its static/anchor position past the viewport edge.
|
|
5865
|
+
popup.style.setProperty("right", "auto", "important");
|
|
5866
|
+
popup.style.setProperty("bottom", "auto", "important");
|
|
5867
|
+
popup.style.setProperty("left", `${Math.round(left)}px`, "important");
|
|
5868
|
+
popup.style.setProperty("top", `${Math.round(top)}px`, "important");
|
|
5869
|
+
|
|
5870
|
+
// Nudge the panel scroller so the selected label stays over the trigger.
|
|
5871
|
+
const panel = this.#getPanel();
|
|
5872
|
+
const alignedTextRect = this.#getOptionTextRect(selected);
|
|
5873
|
+
if (
|
|
5874
|
+
alignedTextRect &&
|
|
5875
|
+
panel &&
|
|
5876
|
+
panel.scrollHeight > panel.clientHeight + 1
|
|
5877
|
+
) {
|
|
5878
|
+
const deltaY = alignedTextRect.top - labelRect.top;
|
|
5879
|
+
if (Math.abs(deltaY) > 0.5) {
|
|
5880
|
+
panel.scrollTop += deltaY;
|
|
5881
|
+
}
|
|
5882
|
+
panel.syncOverflow?.();
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
if (this.#freezeMenuPosition || this.open) {
|
|
5886
|
+
this.#rememberFrozenGeometry();
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
|
|
5890
|
+
#setupListeners() {
|
|
5891
|
+
this.#button?.addEventListener("click", this.#boundTriggerClick);
|
|
5892
|
+
this.#button?.addEventListener("keydown", this.#boundKeydown);
|
|
5893
|
+
// Host click: slotted options stay in light DOM (not dialog.contains).
|
|
5894
|
+
this.addEventListener("click", this.#boundOptionClick);
|
|
5895
|
+
this.#popup?.addEventListener("keydown", this.#boundKeydown);
|
|
5896
|
+
this.#popup?.addEventListener("close", this.#boundPopupClose);
|
|
5897
|
+
this.#panelSlot?.addEventListener("slotchange", this.#boundSlotChange);
|
|
5898
|
+
}
|
|
5899
|
+
|
|
5900
|
+
#teardownListeners() {
|
|
5901
|
+
this.#button?.removeEventListener("click", this.#boundTriggerClick);
|
|
5902
|
+
this.#button?.removeEventListener("keydown", this.#boundKeydown);
|
|
5903
|
+
this.removeEventListener("click", this.#boundOptionClick);
|
|
5904
|
+
this.#popup?.removeEventListener("keydown", this.#boundKeydown);
|
|
5905
|
+
this.#popup?.removeEventListener("close", this.#boundPopupClose);
|
|
5906
|
+
this.#panelSlot?.removeEventListener("slotchange", this.#boundSlotChange);
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5909
|
+
#handleSlotChange() {
|
|
5910
|
+
this.#ensurePanelSlotAttrs();
|
|
5911
|
+
this.#syncValue();
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
#setupObserver() {
|
|
5915
|
+
if (this.#observer) return;
|
|
5916
|
+
this.#observer = new MutationObserver((mutations) => {
|
|
5917
|
+
if (this.#syncingValue || this.#syncingOptions) return;
|
|
5918
|
+
let needsSync = false;
|
|
5919
|
+
for (const mutation of mutations) {
|
|
5920
|
+
if (mutation.type === "childList") {
|
|
5921
|
+
if (
|
|
5922
|
+
[...mutation.addedNodes].some((node) => this.#isMenuChild(node)) ||
|
|
5923
|
+
[...mutation.removedNodes].some((node) => this.#isMenuChild(node)) ||
|
|
5924
|
+
mutation.target?.closest?.("fig-select-option")
|
|
5925
|
+
) {
|
|
5926
|
+
needsSync = true;
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
if (
|
|
5930
|
+
mutation.type === "attributes" &&
|
|
5931
|
+
mutation.target?.tagName === "FIG-SELECT-OPTION" &&
|
|
5932
|
+
(mutation.attributeName === "value" ||
|
|
5933
|
+
mutation.attributeName === "disabled" ||
|
|
5934
|
+
mutation.attributeName === "label")
|
|
5935
|
+
) {
|
|
5936
|
+
needsSync = true;
|
|
5937
|
+
}
|
|
5938
|
+
if (
|
|
5939
|
+
mutation.type === "characterData" &&
|
|
5940
|
+
mutation.target?.parentElement?.tagName === "FIG-SELECT-OPTION"
|
|
5941
|
+
) {
|
|
5942
|
+
needsSync = true;
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
if (needsSync) this.#syncValue();
|
|
5946
|
+
});
|
|
5947
|
+
this.#observer.observe(this, {
|
|
5948
|
+
childList: true,
|
|
5949
|
+
subtree: true,
|
|
5950
|
+
characterData: true,
|
|
5951
|
+
attributes: true,
|
|
5952
|
+
attributeFilter: ["value", "disabled", "selected", "label"],
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
#getOptions({ enabledOnly = false } = {}) {
|
|
5957
|
+
const panel = this.#getPanel();
|
|
5958
|
+
const options = panel
|
|
5959
|
+
? Array.from(panel.querySelectorAll(":scope > fig-select-option"))
|
|
5960
|
+
: [];
|
|
5961
|
+
if (!enabledOnly) return options;
|
|
5962
|
+
return options.filter((opt) => !figBooleanAttribute(opt, "disabled"));
|
|
5963
|
+
}
|
|
5964
|
+
|
|
5965
|
+
#optionValue(option) {
|
|
5966
|
+
if (!option) return "";
|
|
5967
|
+
if (typeof option.value === "string") return option.value;
|
|
5968
|
+
const attr = option.getAttribute?.("value");
|
|
5969
|
+
if (attr != null) return attr;
|
|
5970
|
+
return (option.textContent || "").trim();
|
|
5971
|
+
}
|
|
5972
|
+
|
|
5973
|
+
#optionLabel(option) {
|
|
5974
|
+
if (!option) return "";
|
|
5975
|
+
const labelAttr = option.getAttribute?.("label");
|
|
5976
|
+
if (labelAttr != null && labelAttr !== "") return labelAttr.trim();
|
|
5977
|
+
|
|
5978
|
+
// Ignore prepend/append slot content when deriving a label from children.
|
|
5979
|
+
const parts = [];
|
|
5980
|
+
for (const node of option.childNodes) {
|
|
5981
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
5982
|
+
const text = node.textContent?.trim();
|
|
5983
|
+
if (text) parts.push(text);
|
|
5984
|
+
continue;
|
|
5985
|
+
}
|
|
5986
|
+
if (!(node instanceof Element)) continue;
|
|
5987
|
+
const slot = node.getAttribute("slot");
|
|
5988
|
+
if (slot === "prepend" || slot === "append") continue;
|
|
5989
|
+
const text = node.textContent?.trim();
|
|
5990
|
+
if (text) parts.push(text);
|
|
5991
|
+
}
|
|
5992
|
+
if (parts.length) return parts.join(" ").trim();
|
|
5993
|
+
return (option.textContent || "").trim();
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5996
|
+
#syncPrepend(option) {
|
|
5997
|
+
if (!this.#prependEl) return;
|
|
5998
|
+
const source = option?.querySelector?.(':scope > [slot="prepend"]');
|
|
5999
|
+
this.#prependEl.replaceChildren(
|
|
6000
|
+
...Array.from(source?.childNodes ?? [], (node) => node.cloneNode(true)),
|
|
6001
|
+
);
|
|
6002
|
+
}
|
|
6003
|
+
|
|
6004
|
+
#syncPopupAttrs() {
|
|
6005
|
+
if (!this.#popup) return;
|
|
6006
|
+
this.#popup.setAttribute(
|
|
6007
|
+
"position",
|
|
6008
|
+
this.getAttribute("position") || "bottom left",
|
|
6009
|
+
);
|
|
6010
|
+
const offset = this.getAttribute("offset");
|
|
6011
|
+
if (offset) this.#popup.setAttribute("offset", offset);
|
|
6012
|
+
else this.#popup.removeAttribute("offset");
|
|
6013
|
+
const closedby = this.getAttribute("closedby");
|
|
6014
|
+
if (closedby) this.#popup.setAttribute("closedby", closedby);
|
|
6015
|
+
else this.#popup.removeAttribute("closedby");
|
|
6016
|
+
}
|
|
6017
|
+
|
|
6018
|
+
#syncDisabled() {
|
|
6019
|
+
const disabled = figBooleanAttribute(this, "disabled");
|
|
6020
|
+
if (this.#button) {
|
|
6021
|
+
if (disabled) this.#button.setAttribute("disabled", "");
|
|
6022
|
+
else this.#button.removeAttribute("disabled");
|
|
6023
|
+
}
|
|
6024
|
+
if (disabled && this.open) this.open = false;
|
|
6025
|
+
}
|
|
6026
|
+
|
|
6027
|
+
#pickFallbackOption(options) {
|
|
6028
|
+
if (!options.length) return null;
|
|
6029
|
+
const selected = options.find((opt) =>
|
|
6030
|
+
figBooleanAttribute(opt, "selected"),
|
|
6031
|
+
);
|
|
6032
|
+
if (selected && !figBooleanAttribute(selected, "disabled")) {
|
|
6033
|
+
return selected;
|
|
6034
|
+
}
|
|
6035
|
+
return (
|
|
6036
|
+
options.find((opt) => !figBooleanAttribute(opt, "disabled")) ||
|
|
6037
|
+
options[0] ||
|
|
6038
|
+
null
|
|
6039
|
+
);
|
|
6040
|
+
}
|
|
6041
|
+
|
|
6042
|
+
#emitValueEvents(value) {
|
|
6043
|
+
this.dispatchEvent(
|
|
6044
|
+
new CustomEvent("input", {
|
|
6045
|
+
detail: value,
|
|
6046
|
+
bubbles: true,
|
|
6047
|
+
composed: true,
|
|
6048
|
+
}),
|
|
6049
|
+
);
|
|
6050
|
+
this.dispatchEvent(
|
|
6051
|
+
new CustomEvent("change", {
|
|
6052
|
+
detail: value,
|
|
6053
|
+
bubbles: true,
|
|
6054
|
+
composed: true,
|
|
6055
|
+
}),
|
|
6056
|
+
);
|
|
6057
|
+
}
|
|
6058
|
+
|
|
6059
|
+
#syncValue() {
|
|
6060
|
+
if (this.#syncingValue) return;
|
|
6061
|
+
this.#syncingValue = true;
|
|
6062
|
+
try {
|
|
6063
|
+
const options = this.#getOptions();
|
|
6064
|
+
const hasValueAttr = this.hasAttribute("value");
|
|
6065
|
+
const previousValue = hasValueAttr ? this.getAttribute("value") : null;
|
|
6066
|
+
let match = hasValueAttr
|
|
6067
|
+
? options.find((opt) => this.#optionValue(opt) === previousValue)
|
|
6068
|
+
: null;
|
|
6069
|
+
let valueCorrected = false;
|
|
6070
|
+
|
|
6071
|
+
if (!match) {
|
|
6072
|
+
if (hasValueAttr) {
|
|
6073
|
+
// Options may not be built yet (options attr sync). Keep value until then.
|
|
6074
|
+
if (!options.length) {
|
|
6075
|
+
if (this.#labelEl) {
|
|
6076
|
+
this.#labelEl.textContent =
|
|
6077
|
+
previousValue || this.getAttribute("label") || "";
|
|
6078
|
+
}
|
|
6079
|
+
return;
|
|
6080
|
+
}
|
|
6081
|
+
// Value orphaned (option removed / value attr changed) — clamp or clear.
|
|
6082
|
+
match = this.#pickFallbackOption(options);
|
|
6083
|
+
if (match) {
|
|
6084
|
+
const nextValue = this.#optionValue(match);
|
|
6085
|
+
if (previousValue !== nextValue) {
|
|
6086
|
+
this.setAttribute("value", nextValue);
|
|
6087
|
+
valueCorrected = true;
|
|
6088
|
+
}
|
|
6089
|
+
} else {
|
|
6090
|
+
this.removeAttribute("value");
|
|
6091
|
+
valueCorrected = true;
|
|
6092
|
+
}
|
|
6093
|
+
} else {
|
|
6094
|
+
// No host value yet — honor a selected option if present.
|
|
6095
|
+
match = options.find((opt) =>
|
|
6096
|
+
figBooleanAttribute(opt, "selected"),
|
|
6097
|
+
);
|
|
6098
|
+
if (match) {
|
|
6099
|
+
this.setAttribute("value", this.#optionValue(match));
|
|
6100
|
+
valueCorrected = true;
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
|
|
6105
|
+
for (const opt of options) {
|
|
6106
|
+
const selected = opt === match;
|
|
6107
|
+
opt.setAttribute("aria-selected", selected ? "true" : "false");
|
|
6108
|
+
if (selected) opt.setAttribute("selected", "");
|
|
6109
|
+
else opt.removeAttribute("selected");
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6112
|
+
const label =
|
|
6113
|
+
(match && this.#optionLabel(match)) || this.getAttribute("label") || "";
|
|
6114
|
+
if (this.#labelEl) this.#labelEl.textContent = label;
|
|
6115
|
+
this.#syncPrepend(match);
|
|
6116
|
+
|
|
6117
|
+
const ariaLabel = this.getAttribute("label") || "Select";
|
|
6118
|
+
this.#button?.setAttribute("aria-label", ariaLabel);
|
|
6119
|
+
|
|
6120
|
+
// Don't scrollToOption while open — reposition/sync would fight overflow paging.
|
|
6121
|
+
this.#getPanel()?.syncOverflow?.();
|
|
6122
|
+
|
|
6123
|
+
if (valueCorrected) {
|
|
6124
|
+
this.#emitValueEvents(this.getAttribute("value") ?? "");
|
|
6125
|
+
}
|
|
6126
|
+
} finally {
|
|
6127
|
+
this.#syncingValue = false;
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6130
|
+
|
|
6131
|
+
#handleTriggerClick(e) {
|
|
6132
|
+
if (figBooleanAttribute(this, "disabled")) return;
|
|
6133
|
+
e.preventDefault();
|
|
6134
|
+
e.stopPropagation();
|
|
6135
|
+
const nextOpen = !this.open;
|
|
6136
|
+
if (nextOpen && this.#popup && this.#button) {
|
|
6137
|
+
this.#popup.anchor = this.#button;
|
|
6138
|
+
}
|
|
6139
|
+
this.open = nextOpen;
|
|
5058
6140
|
}
|
|
5059
6141
|
|
|
5060
|
-
#
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
6142
|
+
#handleOptionClick(e) {
|
|
6143
|
+
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
6144
|
+
const option = path.find(
|
|
6145
|
+
(node) => node?.tagName === "FIG-SELECT-OPTION",
|
|
6146
|
+
);
|
|
6147
|
+
if (!option || !this.contains(option)) return;
|
|
6148
|
+
if (figBooleanAttribute(option, "disabled")) return;
|
|
6149
|
+
// Do not stopPropagation — React light-DOM onClick must still fire.
|
|
6150
|
+
this.#selectOption(option);
|
|
6151
|
+
}
|
|
6152
|
+
|
|
6153
|
+
#handleKeydown(e) {
|
|
6154
|
+
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
6155
|
+
|
|
6156
|
+
const listOpen = this.open && (this.#popup?.matches?.(":open") ?? false);
|
|
6157
|
+
if (!listOpen) {
|
|
6158
|
+
if (
|
|
6159
|
+
this.#button?.contains(e.target) &&
|
|
6160
|
+
(e.key === "ArrowDown" || e.key === "Enter" || e.key === " ")
|
|
6161
|
+
) {
|
|
6162
|
+
e.preventDefault();
|
|
6163
|
+
if (this.#popup && this.#button) this.#popup.anchor = this.#button;
|
|
6164
|
+
this.open = true;
|
|
6165
|
+
requestAnimationFrame(() => {
|
|
6166
|
+
const options = this.#getOptions({ enabledOnly: true });
|
|
6167
|
+
const selectedIndex = options.findIndex(
|
|
6168
|
+
(opt) => this.#optionValue(opt) === this.value,
|
|
6169
|
+
);
|
|
6170
|
+
this.#focusOptionAt(selectedIndex >= 0 ? selectedIndex : 0);
|
|
6171
|
+
});
|
|
6172
|
+
}
|
|
6173
|
+
return;
|
|
6174
|
+
}
|
|
6175
|
+
|
|
6176
|
+
const options = this.#getOptions({ enabledOnly: true });
|
|
6177
|
+
if (!options.length) return;
|
|
6178
|
+
|
|
6179
|
+
switch (e.key) {
|
|
6180
|
+
case "ArrowDown":
|
|
6181
|
+
e.preventDefault();
|
|
6182
|
+
this.#syncFocusedIndex();
|
|
6183
|
+
this.#focusOptionAt(this.#focusedIndex + 1);
|
|
6184
|
+
break;
|
|
6185
|
+
case "ArrowUp":
|
|
6186
|
+
e.preventDefault();
|
|
6187
|
+
this.#syncFocusedIndex();
|
|
6188
|
+
this.#focusOptionAt(this.#focusedIndex - 1);
|
|
6189
|
+
break;
|
|
6190
|
+
case "Home":
|
|
6191
|
+
e.preventDefault();
|
|
6192
|
+
this.#focusOptionAt(0);
|
|
6193
|
+
break;
|
|
6194
|
+
case "End":
|
|
6195
|
+
e.preventDefault();
|
|
6196
|
+
this.#focusOptionAt(options.length - 1);
|
|
6197
|
+
break;
|
|
6198
|
+
case "Escape":
|
|
6199
|
+
e.preventDefault();
|
|
6200
|
+
this.open = false;
|
|
6201
|
+
this.#button?.focus();
|
|
6202
|
+
break;
|
|
6203
|
+
case "Enter":
|
|
6204
|
+
case " ": {
|
|
6205
|
+
this.#syncFocusedIndex();
|
|
6206
|
+
const focused = options[this.#focusedIndex];
|
|
6207
|
+
if (!focused) return;
|
|
6208
|
+
e.preventDefault();
|
|
6209
|
+
this.#selectOption(focused);
|
|
6210
|
+
break;
|
|
6211
|
+
}
|
|
5066
6212
|
}
|
|
5067
6213
|
}
|
|
5068
6214
|
|
|
5069
|
-
#
|
|
5070
|
-
this
|
|
5071
|
-
this.#
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
this.#resizeObserver.observe(this);
|
|
6215
|
+
#handlePopupClose() {
|
|
6216
|
+
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
6217
|
+
this.#button?.setAttribute("aria-expanded", "false");
|
|
6218
|
+
this.#button?.focus();
|
|
6219
|
+
this.#focusedIndex = -1;
|
|
5075
6220
|
}
|
|
5076
6221
|
|
|
5077
|
-
#
|
|
5078
|
-
const
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
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;
|
|
6222
|
+
#selectOption(option) {
|
|
6223
|
+
const value = this.#optionValue(option);
|
|
6224
|
+
this.setAttribute("value", value);
|
|
6225
|
+
this.#syncValue();
|
|
6226
|
+
this.#emitValueEvents(value);
|
|
6227
|
+
this.open = false;
|
|
5088
6228
|
}
|
|
5089
6229
|
|
|
5090
|
-
#
|
|
5091
|
-
|
|
5092
|
-
for (const seg of segments) {
|
|
5093
|
-
if (this.#isSegmentTruncated(seg)) return true;
|
|
5094
|
-
}
|
|
5095
|
-
return false;
|
|
6230
|
+
#getEnabledOptions() {
|
|
6231
|
+
return this.#getOptions({ enabledOnly: true });
|
|
5096
6232
|
}
|
|
5097
6233
|
|
|
5098
|
-
#
|
|
5099
|
-
|
|
6234
|
+
#syncFocusedIndex() {
|
|
6235
|
+
const options = this.#getEnabledOptions();
|
|
6236
|
+
if (!options.length) {
|
|
6237
|
+
this.#focusedIndex = -1;
|
|
6238
|
+
return;
|
|
6239
|
+
}
|
|
6240
|
+
const active = options.find((opt) => opt === document.activeElement);
|
|
6241
|
+
const index = active ? options.indexOf(active) : -1;
|
|
6242
|
+
this.#focusedIndex = index >= 0 ? index : this.#focusedIndex;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
#focusOptionAt(index) {
|
|
6246
|
+
const options = this.#getEnabledOptions();
|
|
6247
|
+
if (!options.length) return;
|
|
6248
|
+
const next = ((index % options.length) + options.length) % options.length;
|
|
6249
|
+
this.#focusedIndex = next;
|
|
6250
|
+
options[next]?.focus();
|
|
6251
|
+
}
|
|
6252
|
+
|
|
6253
|
+
#syncPopupWidth() {
|
|
6254
|
+
if (!this.#popup || !this.#button) return;
|
|
6255
|
+
const hostWidth = Math.ceil(this.getBoundingClientRect().width);
|
|
6256
|
+
const triggerWidth = Math.ceil(this.#button.getBoundingClientRect().width);
|
|
6257
|
+
const anchorWidth = Math.max(hostWidth, triggerWidth, 96);
|
|
6258
|
+
|
|
6259
|
+
// Use !important — fig-select::part(listbox) width rules beat element.style.
|
|
6260
|
+
// Menus size to their options while remaining at least as wide as the trigger.
|
|
6261
|
+
this.#popup.style.setProperty("width", "max-content", "important");
|
|
6262
|
+
this.#popup.style.setProperty("min-width", `${anchorWidth}px`, "important");
|
|
6263
|
+
this.#popup.style.setProperty(
|
|
6264
|
+
"max-width",
|
|
6265
|
+
"min(20rem, calc(100vw - 1rem))",
|
|
6266
|
+
"important",
|
|
6267
|
+
);
|
|
6268
|
+
}
|
|
5100
6269
|
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
6270
|
+
#openList() {
|
|
6271
|
+
if (!this.#popup || figBooleanAttribute(this, "disabled")) return;
|
|
6272
|
+
if (this.#button) this.#popup.anchor = this.#button;
|
|
6273
|
+
this.#installPopupPositioning();
|
|
6274
|
+
this.#freezeMenuPosition = false;
|
|
6275
|
+
this.#frozenLabelRect = null;
|
|
6276
|
+
this.#frozenViewport = null;
|
|
6277
|
+
this.#syncValue();
|
|
6278
|
+
this.#syncPopupWidth();
|
|
6279
|
+
this.#popup.open = true;
|
|
6280
|
+
document.addEventListener("keydown", this.#boundKeydown, true);
|
|
6281
|
+
this.#button?.setAttribute("aria-expanded", "true");
|
|
6282
|
+
this.#focusedIndex = -1;
|
|
6283
|
+
requestAnimationFrame(() => {
|
|
6284
|
+
this.#syncPopupWidth();
|
|
6285
|
+
this.#positionPopupOverSelected();
|
|
6286
|
+
const panel = this.#getPanel();
|
|
6287
|
+
const options = this.#getEnabledOptions();
|
|
6288
|
+
const selectedIndex = options.findIndex(
|
|
6289
|
+
(opt) => this.#optionValue(opt) === this.value,
|
|
6290
|
+
);
|
|
6291
|
+
if (selectedIndex >= 0) {
|
|
6292
|
+
this.#focusOptionAt(selectedIndex);
|
|
6293
|
+
} else if (
|
|
6294
|
+
this.#button?.hasAttribute("data-focus-visible") ||
|
|
6295
|
+
this.#button?.matches?.(":focus-visible")
|
|
6296
|
+
) {
|
|
6297
|
+
this.#focusOptionAt(0);
|
|
5120
6298
|
}
|
|
5121
|
-
|
|
6299
|
+
panel?.syncOverflow?.();
|
|
6300
|
+
// Freeze after open align so later positionPopup passes don't undo scroll.
|
|
6301
|
+
// Window resize / trigger movement still realigns via geometry checks.
|
|
6302
|
+
this.#freezeMenuPosition = true;
|
|
6303
|
+
this.#rememberFrozenGeometry();
|
|
6304
|
+
});
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
#closeList() {
|
|
6308
|
+
if (!this.#popup) return;
|
|
6309
|
+
this.#freezeMenuPosition = false;
|
|
6310
|
+
this.#frozenLabelRect = null;
|
|
6311
|
+
this.#frozenViewport = null;
|
|
6312
|
+
document.removeEventListener("keydown", this.#boundKeydown, true);
|
|
6313
|
+
this.#popup.open = false;
|
|
6314
|
+
this.#button?.setAttribute("aria-expanded", "false");
|
|
5122
6315
|
}
|
|
5123
6316
|
}
|
|
5124
|
-
figDefineElement("fig-
|
|
6317
|
+
figDefineElement("fig-select", FigSelect);
|
|
5125
6318
|
|
|
5126
6319
|
/* Slider */
|
|
5127
6320
|
/**
|
|
@@ -5823,6 +7016,8 @@ figDefineElement("fig-slider", FigSlider);
|
|
|
5823
7016
|
class FigInputText extends HTMLElement {
|
|
5824
7017
|
#isInteracting = false;
|
|
5825
7018
|
#passwordVisible = false;
|
|
7019
|
+
#value = "";
|
|
7020
|
+
#reflectingValue = false;
|
|
5826
7021
|
#boundMouseMove;
|
|
5827
7022
|
#boundMouseUp;
|
|
5828
7023
|
#boundWindowBlur;
|
|
@@ -5866,7 +7061,9 @@ class FigInputText extends HTMLElement {
|
|
|
5866
7061
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
5867
7062
|
);
|
|
5868
7063
|
};
|
|
5869
|
-
this.#boundFocusControl =
|
|
7064
|
+
this.#boundFocusControl = () => {
|
|
7065
|
+
if (!this.disabled) this.focus();
|
|
7066
|
+
};
|
|
5870
7067
|
this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
|
|
5871
7068
|
}
|
|
5872
7069
|
|
|
@@ -5907,6 +7104,7 @@ class FigInputText extends HTMLElement {
|
|
|
5907
7104
|
this.#syncSearchClear();
|
|
5908
7105
|
this.#syncSearchClearVisibility();
|
|
5909
7106
|
this.#syncPasswordToggle();
|
|
7107
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
5910
7108
|
figNormalizeTextOnlyInputSlots(this);
|
|
5911
7109
|
this.#startObserver();
|
|
5912
7110
|
|
|
@@ -5982,6 +7180,7 @@ class FigInputText extends HTMLElement {
|
|
|
5982
7180
|
#handleAdornmentClick(event) {
|
|
5983
7181
|
const adornment = event.target?.closest?.("[slot]");
|
|
5984
7182
|
if (!adornment || adornment.parentElement !== this) return;
|
|
7183
|
+
if (this.disabled) return;
|
|
5985
7184
|
this.focus();
|
|
5986
7185
|
}
|
|
5987
7186
|
#startObserver() {
|
|
@@ -6013,6 +7212,7 @@ class FigInputText extends HTMLElement {
|
|
|
6013
7212
|
this.#syncSearchClear();
|
|
6014
7213
|
this.#syncSearchClearVisibility();
|
|
6015
7214
|
this.#syncPasswordToggle();
|
|
7215
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6016
7216
|
figNormalizeTextOnlyInputSlots(this);
|
|
6017
7217
|
}
|
|
6018
7218
|
#syncInputA11yAttributes() {
|
|
@@ -6097,6 +7297,7 @@ class FigInputText extends HTMLElement {
|
|
|
6097
7297
|
button.addEventListener("click", (e) => {
|
|
6098
7298
|
e.preventDefault();
|
|
6099
7299
|
e.stopPropagation();
|
|
7300
|
+
if (this.disabled) return;
|
|
6100
7301
|
if (!this.input || this.input.value === "") {
|
|
6101
7302
|
this.focus();
|
|
6102
7303
|
return;
|
|
@@ -6155,6 +7356,7 @@ class FigInputText extends HTMLElement {
|
|
|
6155
7356
|
button.addEventListener("click", (e) => {
|
|
6156
7357
|
e.preventDefault();
|
|
6157
7358
|
e.stopPropagation();
|
|
7359
|
+
if (this.disabled) return;
|
|
6158
7360
|
this.#passwordVisible = !this.#passwordVisible;
|
|
6159
7361
|
if (this.input) {
|
|
6160
7362
|
this.input.type = this.#passwordVisible ? "text" : "password";
|
|
@@ -6172,6 +7374,14 @@ class FigInputText extends HTMLElement {
|
|
|
6172
7374
|
button?.setAttribute("aria-label", label);
|
|
6173
7375
|
icon?.setAttribute("name", this.#passwordVisible ? "visible" : "hidden");
|
|
6174
7376
|
}
|
|
7377
|
+
#syncGeneratedAdornmentDisabled() {
|
|
7378
|
+
this.querySelectorAll(
|
|
7379
|
+
'[data-generated="search-clear"] fig-button, [data-generated="password-toggle"] fig-button',
|
|
7380
|
+
).forEach((button) => {
|
|
7381
|
+
if (this.disabled) button.setAttribute("disabled", "");
|
|
7382
|
+
else button.removeAttribute("disabled");
|
|
7383
|
+
});
|
|
7384
|
+
}
|
|
6175
7385
|
#transformNumber(value) {
|
|
6176
7386
|
if (value === "") return "";
|
|
6177
7387
|
let transformed = Number(value) * (this.transform || 1);
|
|
@@ -6275,15 +7485,29 @@ class FigInputText extends HTMLElement {
|
|
|
6275
7485
|
return Number.isInteger(rounded) ? rounded : rounded.toFixed(precision);
|
|
6276
7486
|
}
|
|
6277
7487
|
|
|
6278
|
-
/*
|
|
6279
7488
|
get value() {
|
|
6280
|
-
return this
|
|
7489
|
+
return this.#value;
|
|
6281
7490
|
}
|
|
6282
7491
|
|
|
6283
7492
|
set value(val) {
|
|
6284
|
-
|
|
6285
|
-
this
|
|
6286
|
-
|
|
7493
|
+
const value = val ?? "";
|
|
7494
|
+
this.#value = value;
|
|
7495
|
+
const reflected = String(value);
|
|
7496
|
+
if (this.getAttribute("value") !== reflected) {
|
|
7497
|
+
this.#reflectingValue = true;
|
|
7498
|
+
this.setAttribute("value", reflected);
|
|
7499
|
+
this.#reflectingValue = false;
|
|
7500
|
+
}
|
|
7501
|
+
this.#syncRenderedValue(value);
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7504
|
+
#syncRenderedValue(value) {
|
|
7505
|
+
if (!this.input || this.#isInteracting) return;
|
|
7506
|
+
const rendered =
|
|
7507
|
+
this.type === "number" ? String(this.#transformNumber(value)) : String(value ?? "");
|
|
7508
|
+
if (this.input.value !== rendered) this.input.value = rendered;
|
|
7509
|
+
this.#syncSearchClearVisibility();
|
|
7510
|
+
}
|
|
6287
7511
|
|
|
6288
7512
|
static get observedAttributes() {
|
|
6289
7513
|
return [
|
|
@@ -6313,6 +7537,7 @@ class FigInputText extends HTMLElement {
|
|
|
6313
7537
|
case "disabled":
|
|
6314
7538
|
this.disabled = this.input.disabled =
|
|
6315
7539
|
newValue !== null && newValue !== "false";
|
|
7540
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6316
7541
|
break;
|
|
6317
7542
|
case "readonly":
|
|
6318
7543
|
this.readonly = newValue !== null && newValue !== "false";
|
|
@@ -6329,13 +7554,9 @@ class FigInputText extends HTMLElement {
|
|
|
6329
7554
|
let value = newValue;
|
|
6330
7555
|
if (this.type === "number") {
|
|
6331
7556
|
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
7557
|
}
|
|
6338
|
-
this.#
|
|
7558
|
+
this.#value = value ?? "";
|
|
7559
|
+
if (!this.#reflectingValue) this.#syncRenderedValue(this.#value);
|
|
6339
7560
|
break;
|
|
6340
7561
|
case "min":
|
|
6341
7562
|
case "max":
|
|
@@ -6378,6 +7599,7 @@ class FigInputText extends HTMLElement {
|
|
|
6378
7599
|
this.#syncSearchClear();
|
|
6379
7600
|
this.#syncSearchClearVisibility();
|
|
6380
7601
|
this.#syncPasswordToggle();
|
|
7602
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6381
7603
|
break;
|
|
6382
7604
|
case "multiline": {
|
|
6383
7605
|
const next = newValue !== null && newValue !== "false";
|
|
@@ -7405,10 +8627,12 @@ class FigInputColor extends HTMLElement {
|
|
|
7405
8627
|
|
|
7406
8628
|
#fillPickerAttrs() {
|
|
7407
8629
|
const attrs = {};
|
|
7408
|
-
const experimental = this.getAttribute("experimental");
|
|
7409
|
-
if (experimental) attrs["experimental"] = experimental;
|
|
7410
8630
|
for (const { name, value } of this.attributes) {
|
|
7411
|
-
if (
|
|
8631
|
+
if (
|
|
8632
|
+
name.startsWith("picker-") &&
|
|
8633
|
+
name !== "picker-anchor" &&
|
|
8634
|
+
name !== "picker-experimental"
|
|
8635
|
+
) {
|
|
7412
8636
|
attrs[name.slice(7)] = value;
|
|
7413
8637
|
}
|
|
7414
8638
|
}
|
|
@@ -7615,8 +8839,8 @@ class FigInputColor extends HTMLElement {
|
|
|
7615
8839
|
|
|
7616
8840
|
const picker = document.createElement("fig-fill-picker");
|
|
7617
8841
|
picker.innerHTML = "<span hidden></span>";
|
|
7618
|
-
picker.addEventListener("input", this.#
|
|
7619
|
-
picker.addEventListener("change", this.#
|
|
8842
|
+
picker.addEventListener("input", this.#boundFillPickerInput);
|
|
8843
|
+
picker.addEventListener("change", this.#boundChange);
|
|
7620
8844
|
this.appendChild(picker);
|
|
7621
8845
|
this.#fillPicker = picker;
|
|
7622
8846
|
this.#syncFillPicker();
|
|
@@ -7681,8 +8905,22 @@ class FigInputColor extends HTMLElement {
|
|
|
7681
8905
|
}
|
|
7682
8906
|
|
|
7683
8907
|
#setValues(hexValue) {
|
|
7684
|
-
|
|
7685
|
-
|
|
8908
|
+
let colorValue =
|
|
8909
|
+
typeof hexValue === "string" && hexValue.trim()
|
|
8910
|
+
? hexValue.trim()
|
|
8911
|
+
: "#D9D9D9";
|
|
8912
|
+
let rgba = this.convertToRGBA(colorValue);
|
|
8913
|
+
if (
|
|
8914
|
+
!rgba ||
|
|
8915
|
+
!Number.isFinite(rgba.r) ||
|
|
8916
|
+
!Number.isFinite(rgba.g) ||
|
|
8917
|
+
!Number.isFinite(rgba.b) ||
|
|
8918
|
+
!Number.isFinite(rgba.a)
|
|
8919
|
+
) {
|
|
8920
|
+
colorValue = "#D9D9D9";
|
|
8921
|
+
rgba = { r: 217, g: 217, b: 217, a: 1 };
|
|
8922
|
+
}
|
|
8923
|
+
this.rgba = rgba;
|
|
7686
8924
|
this.value = this.rgbAlphaToHex(
|
|
7687
8925
|
{
|
|
7688
8926
|
r: isNaN(this.rgba.r) ? 0 : this.rgba.r,
|
|
@@ -7873,7 +9111,6 @@ class FigInputColor extends HTMLElement {
|
|
|
7873
9111
|
"value",
|
|
7874
9112
|
"style",
|
|
7875
9113
|
"mode",
|
|
7876
|
-
"experimental",
|
|
7877
9114
|
"alpha",
|
|
7878
9115
|
"text",
|
|
7879
9116
|
"disabled",
|
|
@@ -7991,6 +9228,7 @@ class FigInputColor extends HTMLElement {
|
|
|
7991
9228
|
}
|
|
7992
9229
|
|
|
7993
9230
|
convertToRGBA(color) {
|
|
9231
|
+
if (typeof color !== "string") return null;
|
|
7994
9232
|
let r,
|
|
7995
9233
|
g,
|
|
7996
9234
|
b,
|
|
@@ -8415,7 +9653,6 @@ class FigInputFill extends HTMLElement {
|
|
|
8415
9653
|
"value",
|
|
8416
9654
|
"disabled",
|
|
8417
9655
|
"mode",
|
|
8418
|
-
"experimental",
|
|
8419
9656
|
"alpha",
|
|
8420
9657
|
"aria-label",
|
|
8421
9658
|
"aria-describedby",
|
|
@@ -8499,13 +9736,15 @@ class FigInputFill extends HTMLElement {
|
|
|
8499
9736
|
// Backward-compat: direct attributes forwarded to fill picker
|
|
8500
9737
|
const mode = this.getAttribute("mode");
|
|
8501
9738
|
if (mode) attrs["mode"] = mode;
|
|
8502
|
-
const experimental = this.getAttribute("experimental");
|
|
8503
|
-
if (experimental) attrs["experimental"] = experimental;
|
|
8504
9739
|
const alpha = this.getAttribute("alpha");
|
|
8505
9740
|
if (alpha) attrs["alpha"] = alpha;
|
|
8506
9741
|
// picker-* overrides (except anchor, handled programmatically)
|
|
8507
9742
|
for (const { name, value } of this.attributes) {
|
|
8508
|
-
if (
|
|
9743
|
+
if (
|
|
9744
|
+
name.startsWith("picker-") &&
|
|
9745
|
+
name !== "picker-anchor" &&
|
|
9746
|
+
name !== "picker-experimental"
|
|
9747
|
+
) {
|
|
8509
9748
|
attrs[name.slice(7)] = value;
|
|
8510
9749
|
}
|
|
8511
9750
|
}
|
|
@@ -8741,7 +9980,6 @@ class FigInputFill extends HTMLElement {
|
|
|
8741
9980
|
if (detail.video) this.#video = detail.video;
|
|
8742
9981
|
break;
|
|
8743
9982
|
}
|
|
8744
|
-
|
|
8745
9983
|
// Update controls (don't re-render to keep dialog open)
|
|
8746
9984
|
if (typeChanged) {
|
|
8747
9985
|
this.#updateControlsForType();
|
|
@@ -9141,7 +10379,6 @@ class FigInputFill extends HTMLElement {
|
|
|
9141
10379
|
this.#syncDisabled();
|
|
9142
10380
|
break;
|
|
9143
10381
|
case "mode":
|
|
9144
|
-
case "experimental":
|
|
9145
10382
|
// Pass through to internal fill picker
|
|
9146
10383
|
if (this.#fillPicker) {
|
|
9147
10384
|
if (newValue) {
|
|
@@ -9183,6 +10420,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9183
10420
|
#expandedPickers = [];
|
|
9184
10421
|
#renderRAF = null;
|
|
9185
10422
|
#boundHandleKeyDown = this.#handleKeyDown.bind(this);
|
|
10423
|
+
#boundHandleHostFocus = () => this.focus();
|
|
9186
10424
|
|
|
9187
10425
|
static get observedAttributes() {
|
|
9188
10426
|
return ["value", "disabled", "min", "max", "open", "fixed"];
|
|
@@ -9226,7 +10464,9 @@ class FigInputPalette extends HTMLElement {
|
|
|
9226
10464
|
}
|
|
9227
10465
|
|
|
9228
10466
|
connectedCallback() {
|
|
9229
|
-
|
|
10467
|
+
this.setAttribute("tabindex", "-1");
|
|
10468
|
+
this.removeEventListener("focus", this.#boundHandleHostFocus);
|
|
10469
|
+
this.addEventListener("focus", this.#boundHandleHostFocus);
|
|
9230
10470
|
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
9231
10471
|
this.addEventListener("keydown", this.#boundHandleKeyDown);
|
|
9232
10472
|
if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
|
|
@@ -9244,13 +10484,14 @@ class FigInputPalette extends HTMLElement {
|
|
|
9244
10484
|
this.#renderRAF = null;
|
|
9245
10485
|
}
|
|
9246
10486
|
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
10487
|
+
this.removeEventListener("focus", this.#boundHandleHostFocus);
|
|
9247
10488
|
this.#inlinePickers = [];
|
|
9248
10489
|
this.#expandedPickers = [];
|
|
9249
10490
|
}
|
|
9250
10491
|
|
|
9251
10492
|
#handleKeyDown(event) {
|
|
9252
10493
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
9253
|
-
if (event.target !== this
|
|
10494
|
+
if (event.target !== this.querySelector(".palette-colors-inline")) return;
|
|
9254
10495
|
if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
|
|
9255
10496
|
event.preventDefault();
|
|
9256
10497
|
event.stopPropagation();
|
|
@@ -9275,7 +10516,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9275
10516
|
this.#render();
|
|
9276
10517
|
break;
|
|
9277
10518
|
case "open":
|
|
9278
|
-
|
|
10519
|
+
this.#syncTriggerState();
|
|
9279
10520
|
break;
|
|
9280
10521
|
}
|
|
9281
10522
|
}
|
|
@@ -9371,8 +10612,13 @@ class FigInputPalette extends HTMLElement {
|
|
|
9371
10612
|
const inlineWrap = document.createElement("div");
|
|
9372
10613
|
inlineWrap.className = "palette-colors-inline";
|
|
9373
10614
|
inlineWrap.setAttribute("role", "button");
|
|
10615
|
+
inlineWrap.setAttribute("tabindex", disabled ? "-1" : "+0");
|
|
9374
10616
|
inlineWrap.setAttribute("aria-expanded", String(this.open));
|
|
9375
10617
|
inlineWrap.setAttribute("aria-label", "Edit palette colors");
|
|
10618
|
+
inlineWrap.addEventListener("blur", () => {
|
|
10619
|
+
inlineWrap.style.removeProperty("outline");
|
|
10620
|
+
inlineWrap.style.removeProperty("outline-offset");
|
|
10621
|
+
});
|
|
9376
10622
|
const openPalette = () => {
|
|
9377
10623
|
if (
|
|
9378
10624
|
this.hasAttribute("disabled") &&
|
|
@@ -9383,12 +10629,6 @@ class FigInputPalette extends HTMLElement {
|
|
|
9383
10629
|
inlineWrap.setAttribute("aria-expanded", "true");
|
|
9384
10630
|
};
|
|
9385
10631
|
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
10632
|
|
|
9393
10633
|
const wrap = document.createElement("div");
|
|
9394
10634
|
wrap.className = "palette-colors";
|
|
@@ -9399,6 +10639,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9399
10639
|
});
|
|
9400
10640
|
inlineWrap.appendChild(wrap);
|
|
9401
10641
|
this.appendChild(inlineWrap);
|
|
10642
|
+
this.#syncTriggerState();
|
|
9402
10643
|
|
|
9403
10644
|
if (!this.#isFixed) this.#createAddButton(disabled, this);
|
|
9404
10645
|
|
|
@@ -9595,6 +10836,31 @@ class FigInputPalette extends HTMLElement {
|
|
|
9595
10836
|
else addBtn.removeAttribute("disabled");
|
|
9596
10837
|
}
|
|
9597
10838
|
this.#syncRemoveButtons(disabled);
|
|
10839
|
+
this.#syncTriggerState();
|
|
10840
|
+
}
|
|
10841
|
+
|
|
10842
|
+
#syncTriggerState() {
|
|
10843
|
+
const trigger = this.querySelector(".palette-colors-inline");
|
|
10844
|
+
if (!trigger) return;
|
|
10845
|
+
const disabled =
|
|
10846
|
+
this.hasAttribute("disabled") &&
|
|
10847
|
+
this.getAttribute("disabled") !== "false";
|
|
10848
|
+
trigger.setAttribute("tabindex", disabled ? "-1" : "+0");
|
|
10849
|
+
trigger.setAttribute("aria-disabled", String(disabled));
|
|
10850
|
+
trigger.setAttribute("aria-expanded", String(this.open));
|
|
10851
|
+
}
|
|
10852
|
+
|
|
10853
|
+
focus() {
|
|
10854
|
+
if (
|
|
10855
|
+
this.hasAttribute("disabled") &&
|
|
10856
|
+
this.getAttribute("disabled") !== "false"
|
|
10857
|
+
)
|
|
10858
|
+
return;
|
|
10859
|
+
const trigger = this.querySelector(".palette-colors-inline");
|
|
10860
|
+
if (!trigger) return;
|
|
10861
|
+
trigger.style.outline = "var(--figma-focus-outline)";
|
|
10862
|
+
trigger.style.outlineOffset = "var(--figma-focus-outline-offset)";
|
|
10863
|
+
trigger.focus();
|
|
9598
10864
|
}
|
|
9599
10865
|
|
|
9600
10866
|
#syncRemoveButtons(disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") {
|
|
@@ -9923,11 +11189,9 @@ class FigInputGradient extends HTMLElement {
|
|
|
9923
11189
|
const mode = this.#editMode;
|
|
9924
11190
|
|
|
9925
11191
|
if (mode === "picker" && hasFigFillPicker()) {
|
|
9926
|
-
const experimental = this.getAttribute("experimental");
|
|
9927
|
-
const expAttr = experimental ? ` experimental="${experimental}"` : "";
|
|
9928
11192
|
const gradientValue = JSON.stringify(this.value);
|
|
9929
11193
|
this.innerHTML = `
|
|
9930
|
-
<fig-fill-picker mode="gradient"
|
|
11194
|
+
<fig-fill-picker mode="gradient" value='${gradientValue}'${disabled ? " disabled" : ""}>
|
|
9931
11195
|
<fig-swatch background="${this.#buildGradientCSS()}"${this.#swatchSizeAttr()}${disabled ? " disabled" : ""}></fig-swatch>
|
|
9932
11196
|
</fig-fill-picker>`;
|
|
9933
11197
|
this.#swatch = this.querySelector("fig-swatch");
|
|
@@ -10819,7 +12083,6 @@ figDefineElement("fig-switch", FigSwitch);
|
|
|
10819
12083
|
* @attr {string} placeholder - Placeholder text for the input
|
|
10820
12084
|
* @attr {string} value - The current input value
|
|
10821
12085
|
* @attr {boolean} disabled - Disables the input and dropdown button
|
|
10822
|
-
* @attr {string} experimental - Feature flag passed to internal fig-dropdown
|
|
10823
12086
|
*/
|
|
10824
12087
|
class FigComboInput extends HTMLElement {
|
|
10825
12088
|
static observedAttributes = [
|
|
@@ -10827,7 +12090,6 @@ class FigComboInput extends HTMLElement {
|
|
|
10827
12090
|
"placeholder",
|
|
10828
12091
|
"value",
|
|
10829
12092
|
"disabled",
|
|
10830
|
-
"experimental",
|
|
10831
12093
|
"aria-label",
|
|
10832
12094
|
"aria-labelledby",
|
|
10833
12095
|
"aria-describedby",
|
|
@@ -10908,15 +12170,11 @@ class FigComboInput extends HTMLElement {
|
|
|
10908
12170
|
const options = this.#getOptions();
|
|
10909
12171
|
const placeholder = this.getAttribute("placeholder") || "";
|
|
10910
12172
|
const currentValue = this.value;
|
|
10911
|
-
const experimental = this.getAttribute("experimental");
|
|
10912
|
-
const expAttr = experimental
|
|
10913
|
-
? ` experimental="${figEscapeAttribute(experimental)}"`
|
|
10914
|
-
: "";
|
|
10915
12173
|
const dropdownLabel = this.#dropdownLabel();
|
|
10916
12174
|
|
|
10917
12175
|
const dropdownHTML = this.#usesCustomDropdown
|
|
10918
12176
|
? ""
|
|
10919
|
-
: `<fig-dropdown type="dropdown" label="${figEscapeAttribute(dropdownLabel)}"
|
|
12177
|
+
: `<fig-dropdown type="dropdown" label="${figEscapeAttribute(dropdownLabel)}">${options.map((o) => `<option>${figEscapeAttribute(o.trim())}</option>`).join("")}</fig-dropdown>`;
|
|
10920
12178
|
|
|
10921
12179
|
this.innerHTML = `<div class="input-combo">
|
|
10922
12180
|
<fig-input-text placeholder="${figEscapeAttribute(placeholder)}" value="${figEscapeAttribute(currentValue)}"></fig-input-text>
|
|
@@ -10938,9 +12196,6 @@ class FigComboInput extends HTMLElement {
|
|
|
10938
12196
|
if (!this.#customDropdown.hasAttribute("label")) {
|
|
10939
12197
|
this.#customDropdown.setAttribute("label", dropdownLabel);
|
|
10940
12198
|
}
|
|
10941
|
-
if (experimental) {
|
|
10942
|
-
this.#customDropdown.setAttribute("experimental", experimental);
|
|
10943
|
-
}
|
|
10944
12199
|
this.#button.append(this.#customDropdown);
|
|
10945
12200
|
}
|
|
10946
12201
|
|
|
@@ -11088,13 +12343,6 @@ class FigComboInput extends HTMLElement {
|
|
|
11088
12343
|
case "disabled":
|
|
11089
12344
|
this.#applyDisabled(newValue !== null && newValue !== "false");
|
|
11090
12345
|
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
12346
|
case "aria-label":
|
|
11099
12347
|
case "aria-labelledby":
|
|
11100
12348
|
case "aria-describedby":
|
|
@@ -12892,9 +14140,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
12892
14140
|
#line2 = null;
|
|
12893
14141
|
#handle1 = null;
|
|
12894
14142
|
#handle2 = null;
|
|
12895
|
-
#
|
|
12896
|
-
#bezierEndpointEnd = null;
|
|
12897
|
-
#dropdown = null;
|
|
14143
|
+
#select = null;
|
|
12898
14144
|
#valueInput = null;
|
|
12899
14145
|
#presetName = null;
|
|
12900
14146
|
#targetLine = null;
|
|
@@ -12902,10 +14148,10 @@ class FigEasingCurve extends HTMLElement {
|
|
|
12902
14148
|
#drawWidth = 200;
|
|
12903
14149
|
#drawHeight = 200;
|
|
12904
14150
|
#bounds = null;
|
|
12905
|
-
#
|
|
14151
|
+
#boundaryTop = null;
|
|
14152
|
+
#boundaryBottom = null;
|
|
12906
14153
|
#resizeObserver = null;
|
|
12907
14154
|
#bezierHandleRadius = 5;
|
|
12908
|
-
#bezierEndpointRadius = 2;
|
|
12909
14155
|
#durationBarWidth = 10;
|
|
12910
14156
|
#durationBarHeight = 10;
|
|
12911
14157
|
#durationBarRadius = 3;
|
|
@@ -13023,7 +14269,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13023
14269
|
this.#render();
|
|
13024
14270
|
} else {
|
|
13025
14271
|
if (this.#svg) this.#updatePaths();
|
|
13026
|
-
this.#
|
|
14272
|
+
this.#syncSelect();
|
|
13027
14273
|
this.#syncValueInput();
|
|
13028
14274
|
}
|
|
13029
14275
|
}
|
|
@@ -13246,14 +14492,15 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13246
14492
|
.replace(/>/g, ">");
|
|
13247
14493
|
}
|
|
13248
14494
|
|
|
13249
|
-
#
|
|
14495
|
+
#getSelectHTML() {
|
|
13250
14496
|
let optionsHTML = "";
|
|
13251
14497
|
let currentGroup = undefined;
|
|
13252
14498
|
for (const p of FigEasingCurve.PRESETS) {
|
|
13253
14499
|
if (!this.#isEditEnabled() && !p.value && !p.spring) continue;
|
|
13254
14500
|
if (p.group !== currentGroup) {
|
|
13255
|
-
if (
|
|
13256
|
-
|
|
14501
|
+
if (p.group) {
|
|
14502
|
+
optionsHTML += `<fig-menu-separator label="${FigEasingCurve.#escapeAttribute(p.group)}"></fig-menu-separator>`;
|
|
14503
|
+
}
|
|
13257
14504
|
currentGroup = p.group;
|
|
13258
14505
|
}
|
|
13259
14506
|
let icon;
|
|
@@ -13269,40 +14516,37 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13269
14516
|
];
|
|
13270
14517
|
icon = FigEasingCurve.curveIcon(...v);
|
|
13271
14518
|
}
|
|
13272
|
-
const
|
|
13273
|
-
optionsHTML += `<option value="${
|
|
14519
|
+
const name = FigEasingCurve.#escapeAttribute(p.name);
|
|
14520
|
+
optionsHTML += `<fig-select-option value="${name}" label="${name}"><span slot="prepend">${icon}</span><span>${name}</span></fig-select-option>`;
|
|
13274
14521
|
}
|
|
13275
|
-
|
|
13276
|
-
return `<fig-
|
|
14522
|
+
const value = FigEasingCurve.#escapeAttribute(this.#presetName);
|
|
14523
|
+
return `<fig-select class="fig-easing-curve-select" label="Easing preset" value="${value}" full><fig-select-options>${optionsHTML}</fig-select-options></fig-select>`;
|
|
13277
14524
|
}
|
|
13278
14525
|
|
|
13279
14526
|
#getInnerHTML() {
|
|
13280
14527
|
const size = 200;
|
|
13281
|
-
const
|
|
13282
|
-
if (!this.#isEditEnabled()) return
|
|
14528
|
+
const select = this.#getSelectHTML();
|
|
14529
|
+
if (!this.#isEditEnabled()) return select;
|
|
13283
14530
|
const valueInput = `<fig-input-text class="fig-easing-curve-value-input" value="${FigEasingCurve.#escapeAttribute(this.value)}" full></fig-input-text>`;
|
|
13284
14531
|
|
|
13285
14532
|
if (this.#mode === "spring") {
|
|
13286
14533
|
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">
|
|
14534
|
+
return `${select}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
13289
14535
|
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
13290
14536
|
<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
14537
|
<path class="fig-easing-curve-path"/>
|
|
13293
14538
|
<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
14539
|
<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
14540
|
</svg></div>${valueInput}`;
|
|
13296
14541
|
}
|
|
13297
14542
|
|
|
13298
|
-
return `${
|
|
14543
|
+
return `${select}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
13299
14544
|
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
13300
|
-
<line class="fig-easing-curve-
|
|
14545
|
+
<line class="fig-easing-curve-boundary" data-boundary="top" x1="0" y1="0" x2="${size}" y2="0"/>
|
|
14546
|
+
<line class="fig-easing-curve-boundary" data-boundary="bottom" x1="0" y1="${size}" x2="${size}" y2="${size}"/>
|
|
14547
|
+
<path class="fig-easing-curve-path"/>
|
|
13301
14548
|
<line class="fig-easing-curve-arm" data-arm="1"/>
|
|
13302
14549
|
<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
14550
|
<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
14551
|
<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
14552
|
</svg></div>${valueInput}`;
|
|
@@ -13316,10 +14560,6 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13316
14560
|
}
|
|
13317
14561
|
|
|
13318
14562
|
#syncMetricsFromCSS() {
|
|
13319
|
-
this.#bezierEndpointRadius = this.#readCssNumber(
|
|
13320
|
-
"--easing-bezier-endpoint-radius",
|
|
13321
|
-
this.#bezierEndpointRadius,
|
|
13322
|
-
);
|
|
13323
14563
|
this.#durationBarRadius = this.#readCssNumber(
|
|
13324
14564
|
"--easing-duration-bar-radius",
|
|
13325
14565
|
this.#durationBarRadius,
|
|
@@ -13337,13 +14577,12 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13337
14577
|
this.#handle2 =
|
|
13338
14578
|
this.querySelector('[data-handle="2"]') ||
|
|
13339
14579
|
this.querySelector('[data-handle="duration"]');
|
|
13340
|
-
this.#
|
|
13341
|
-
this.#bezierEndpointEnd = this.querySelector('[data-endpoint="end"]');
|
|
13342
|
-
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
14580
|
+
this.#select = this.querySelector(".fig-easing-curve-select");
|
|
13343
14581
|
this.#valueInput = this.querySelector(".fig-easing-curve-value-input");
|
|
13344
14582
|
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
13345
14583
|
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
13346
|
-
this.#
|
|
14584
|
+
this.#boundaryTop = this.querySelector('[data-boundary="top"]');
|
|
14585
|
+
this.#boundaryBottom = this.querySelector('[data-boundary="bottom"]');
|
|
13347
14586
|
}
|
|
13348
14587
|
|
|
13349
14588
|
#syncHandleSizes() {
|
|
@@ -13392,12 +14631,37 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13392
14631
|
|
|
13393
14632
|
// --- Coordinate helpers ---
|
|
13394
14633
|
|
|
14634
|
+
#bezierDomain() {
|
|
14635
|
+
const minVal = Math.min(0, this.#cp1.y, this.#cp2.y);
|
|
14636
|
+
const maxVal = Math.max(1, this.#cp1.y, this.#cp2.y);
|
|
14637
|
+
const range = maxVal - minVal || 1;
|
|
14638
|
+
const pad = Math.min(
|
|
14639
|
+
this.#bezierHandleRadius,
|
|
14640
|
+
Math.max(0, (this.#drawHeight - 1) / 2),
|
|
14641
|
+
);
|
|
14642
|
+
return {
|
|
14643
|
+
minVal,
|
|
14644
|
+
maxVal,
|
|
14645
|
+
range,
|
|
14646
|
+
pad,
|
|
14647
|
+
draw: Math.max(1, this.#drawHeight - pad * 2),
|
|
14648
|
+
};
|
|
14649
|
+
}
|
|
14650
|
+
|
|
13395
14651
|
#toSVG(nx, ny) {
|
|
13396
|
-
|
|
14652
|
+
const { maxVal, range, pad, draw } = this.#bezierDomain();
|
|
14653
|
+
return {
|
|
14654
|
+
x: nx * this.#drawWidth,
|
|
14655
|
+
y: pad + ((maxVal - ny) / range) * draw,
|
|
14656
|
+
};
|
|
13397
14657
|
}
|
|
13398
14658
|
|
|
13399
14659
|
#fromSVG(sx, sy) {
|
|
13400
|
-
|
|
14660
|
+
const { maxVal, range, pad, draw } = this.#bezierDomain();
|
|
14661
|
+
return {
|
|
14662
|
+
x: sx / this.#drawWidth,
|
|
14663
|
+
y: maxVal - ((sy - pad) / draw) * range,
|
|
14664
|
+
};
|
|
13401
14665
|
}
|
|
13402
14666
|
|
|
13403
14667
|
#springScale = { minVal: 0, maxVal: 1.2, totalTime: 1 };
|
|
@@ -13442,17 +14706,20 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13442
14706
|
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
13443
14707
|
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
13444
14708
|
}
|
|
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
14709
|
const p0 = this.#toSVG(0, 0);
|
|
13453
14710
|
const p1 = this.#toSVG(this.#cp1.x, this.#cp1.y);
|
|
13454
14711
|
const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
|
|
13455
14712
|
const p3 = this.#toSVG(1, 1);
|
|
14713
|
+
for (const [boundary, y] of [
|
|
14714
|
+
[this.#boundaryTop, p3.y],
|
|
14715
|
+
[this.#boundaryBottom, p0.y],
|
|
14716
|
+
]) {
|
|
14717
|
+
if (!boundary) continue;
|
|
14718
|
+
boundary.setAttribute("x1", "0");
|
|
14719
|
+
boundary.setAttribute("y1", y);
|
|
14720
|
+
boundary.setAttribute("x2", this.#drawWidth);
|
|
14721
|
+
boundary.setAttribute("y2", y);
|
|
14722
|
+
}
|
|
13456
14723
|
|
|
13457
14724
|
this.#curve.setAttribute(
|
|
13458
14725
|
"d",
|
|
@@ -13471,14 +14738,6 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13471
14738
|
this.#handle1.setAttribute("y", p1.y - hr);
|
|
13472
14739
|
this.#handle2.setAttribute("x", p2.x - hr);
|
|
13473
14740
|
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
14741
|
this.#syncBezierHandleTabOrder();
|
|
13483
14742
|
}
|
|
13484
14743
|
|
|
@@ -13571,11 +14830,11 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13571
14830
|
return peak;
|
|
13572
14831
|
}
|
|
13573
14832
|
|
|
13574
|
-
// ---
|
|
14833
|
+
// --- Select ---
|
|
13575
14834
|
|
|
13576
|
-
#
|
|
13577
|
-
if (!this.#
|
|
13578
|
-
this.#
|
|
14835
|
+
#syncSelect() {
|
|
14836
|
+
if (!this.#select) return;
|
|
14837
|
+
this.#select.value = this.#presetName;
|
|
13579
14838
|
this.#refreshCustomPresetIcons();
|
|
13580
14839
|
}
|
|
13581
14840
|
|
|
@@ -13614,23 +14873,24 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13614
14873
|
this.#render();
|
|
13615
14874
|
} else {
|
|
13616
14875
|
this.#updatePaths();
|
|
13617
|
-
this.#
|
|
14876
|
+
this.#syncSelect();
|
|
13618
14877
|
if (eventType === "change") this.#syncValueInput();
|
|
13619
14878
|
}
|
|
13620
14879
|
this.#emit(eventType);
|
|
13621
14880
|
}
|
|
13622
14881
|
|
|
13623
|
-
#setOptionIconByValue(
|
|
13624
|
-
if (!
|
|
13625
|
-
for (const option of
|
|
14882
|
+
#setOptionIconByValue(optionValue, icon) {
|
|
14883
|
+
if (!this.#select) return;
|
|
14884
|
+
for (const option of this.#select.querySelectorAll("fig-select-option")) {
|
|
13626
14885
|
if (option.value === optionValue) {
|
|
13627
|
-
option.
|
|
14886
|
+
const prepend = option.querySelector(':scope > [slot="prepend"]');
|
|
14887
|
+
if (prepend) prepend.innerHTML = icon;
|
|
13628
14888
|
}
|
|
13629
14889
|
}
|
|
13630
14890
|
}
|
|
13631
14891
|
|
|
13632
14892
|
#refreshCustomPresetIcons() {
|
|
13633
|
-
if (!this.#
|
|
14893
|
+
if (!this.#select) return;
|
|
13634
14894
|
if (!this.#isEditEnabled()) return;
|
|
13635
14895
|
const bezierIcon = FigEasingCurve.curveIcon(
|
|
13636
14896
|
this.#cp1.x,
|
|
@@ -13640,25 +14900,14 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13640
14900
|
);
|
|
13641
14901
|
const springIcon = FigEasingCurve.#springIcon(this.#spring);
|
|
13642
14902
|
|
|
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
|
-
);
|
|
14903
|
+
this.#setOptionIconByValue("Custom bezier", bezierIcon);
|
|
14904
|
+
this.#setOptionIconByValue("Custom spring", springIcon);
|
|
13656
14905
|
}
|
|
13657
14906
|
|
|
13658
14907
|
#syncAfterHandleInput(eventType) {
|
|
13659
14908
|
this.#updatePaths();
|
|
13660
14909
|
this.#presetName = this.#matchPreset();
|
|
13661
|
-
this.#
|
|
14910
|
+
this.#syncSelect();
|
|
13662
14911
|
this.#syncValueInput();
|
|
13663
14912
|
this.#emit(eventType);
|
|
13664
14913
|
}
|
|
@@ -13847,8 +15096,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13847
15096
|
}
|
|
13848
15097
|
}
|
|
13849
15098
|
|
|
13850
|
-
if (this.#
|
|
13851
|
-
this.#
|
|
15099
|
+
if (this.#select) {
|
|
15100
|
+
this.#select.addEventListener("change", (e) => {
|
|
13852
15101
|
const name = e.detail;
|
|
13853
15102
|
const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
|
|
13854
15103
|
if (!preset) return;
|
|
@@ -13921,11 +15170,30 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13921
15170
|
e.preventDefault();
|
|
13922
15171
|
this.#isDragging = handle;
|
|
13923
15172
|
this.#syncActiveBezierArm();
|
|
15173
|
+
const svgRect = this.#svg.getBoundingClientRect();
|
|
15174
|
+
const startClientX = e.clientX;
|
|
15175
|
+
const startClientY = e.clientY;
|
|
15176
|
+
const fromHandle = e.target?.closest?.(
|
|
15177
|
+
".fig-easing-curve-handle, fig-handle",
|
|
15178
|
+
);
|
|
15179
|
+
const currentPoint = handle === 1 ? this.#cp1 : this.#cp2;
|
|
15180
|
+
const svgPoint = this.#clientToSVG(e);
|
|
15181
|
+
const startPoint = fromHandle
|
|
15182
|
+
? { ...currentPoint }
|
|
15183
|
+
: this.#fromSVG(svgPoint.x, svgPoint.y);
|
|
15184
|
+
const { range, draw } = this.#bezierDomain();
|
|
15185
|
+
const unitsPerClientY =
|
|
15186
|
+
range /
|
|
15187
|
+
Math.max(1, (draw / this.#drawHeight) * Math.max(1, svgRect.height));
|
|
13924
15188
|
|
|
13925
15189
|
const onMove = (e) => {
|
|
13926
15190
|
if (!this.#isDragging) return;
|
|
13927
|
-
const
|
|
13928
|
-
|
|
15191
|
+
const norm = {
|
|
15192
|
+
x:
|
|
15193
|
+
startPoint.x +
|
|
15194
|
+
(e.clientX - startClientX) / Math.max(1, svgRect.width),
|
|
15195
|
+
y: startPoint.y - (e.clientY - startClientY) * unitsPerClientY,
|
|
15196
|
+
};
|
|
13929
15197
|
|
|
13930
15198
|
norm.x = Math.round(norm.x * 100) / 100;
|
|
13931
15199
|
norm.y = Math.round(norm.y * 100) / 100;
|
|
@@ -13940,7 +15208,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13940
15208
|
}
|
|
13941
15209
|
this.#updatePaths();
|
|
13942
15210
|
this.#presetName = this.#matchPreset();
|
|
13943
|
-
this.#
|
|
15211
|
+
this.#syncSelect();
|
|
13944
15212
|
this.#syncValueInput();
|
|
13945
15213
|
this.#emit("input");
|
|
13946
15214
|
};
|
|
@@ -13991,7 +15259,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13991
15259
|
|
|
13992
15260
|
this.#updatePaths();
|
|
13993
15261
|
this.#presetName = this.#matchPreset();
|
|
13994
|
-
this.#
|
|
15262
|
+
this.#syncSelect();
|
|
13995
15263
|
this.#syncValueInput();
|
|
13996
15264
|
this.#emit("input");
|
|
13997
15265
|
};
|
|
@@ -16576,6 +17844,7 @@ class FigChooser extends HTMLElement {
|
|
|
16576
17844
|
this.#setupDrag();
|
|
16577
17845
|
this.#startObserver();
|
|
16578
17846
|
this.#startResizeObserver();
|
|
17847
|
+
this.#syncDisabledChoices();
|
|
16579
17848
|
|
|
16580
17849
|
figNextFrame(this, () => {
|
|
16581
17850
|
this.#syncSelection();
|
|
@@ -16629,17 +17898,7 @@ class FigChooser extends HTMLElement {
|
|
|
16629
17898
|
this.#selectByValue(newValue);
|
|
16630
17899
|
}
|
|
16631
17900
|
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
|
-
}
|
|
17901
|
+
this.#syncDisabledChoices();
|
|
16643
17902
|
}
|
|
16644
17903
|
if (name === "choice-element") {
|
|
16645
17904
|
requestAnimationFrame(() => this.#syncSelection());
|
|
@@ -16691,6 +17950,23 @@ class FigChooser extends HTMLElement {
|
|
|
16691
17950
|
this.selectedChoice = choices[0];
|
|
16692
17951
|
}
|
|
16693
17952
|
|
|
17953
|
+
#syncDisabledChoices() {
|
|
17954
|
+
const chooserDisabled = figBooleanAttribute(this, "disabled");
|
|
17955
|
+
if (chooserDisabled) this.setAttribute("aria-disabled", "true");
|
|
17956
|
+
else this.removeAttribute("aria-disabled");
|
|
17957
|
+
for (const choice of this.choices) {
|
|
17958
|
+
const disabled =
|
|
17959
|
+
chooserDisabled || figBooleanAttribute(choice, "disabled");
|
|
17960
|
+
if (disabled) {
|
|
17961
|
+
choice.setAttribute("aria-disabled", "true");
|
|
17962
|
+
choice.setAttribute("tabindex", "-1");
|
|
17963
|
+
} else {
|
|
17964
|
+
choice.removeAttribute("aria-disabled");
|
|
17965
|
+
choice.setAttribute("tabindex", "0");
|
|
17966
|
+
}
|
|
17967
|
+
}
|
|
17968
|
+
}
|
|
17969
|
+
|
|
16694
17970
|
#selectByValue(value) {
|
|
16695
17971
|
const choices = this.choices;
|
|
16696
17972
|
for (const choice of choices) {
|
|
@@ -17053,6 +18329,7 @@ class FigChooser extends HTMLElement {
|
|
|
17053
18329
|
if (this.#isUnwrapping) return;
|
|
17054
18330
|
this.#removeLegacyScroller();
|
|
17055
18331
|
this.#applyOverflowMode();
|
|
18332
|
+
this.#syncDisabledChoices();
|
|
17056
18333
|
const choices = this.choices;
|
|
17057
18334
|
if (this.#selectedChoice && !choices.includes(this.#selectedChoice)) {
|
|
17058
18335
|
this.#selectedChoice = null;
|
|
@@ -17089,6 +18366,7 @@ class FigHandle extends HTMLElement {
|
|
|
17089
18366
|
#isDragging = false;
|
|
17090
18367
|
#didDrag = false;
|
|
17091
18368
|
#boundPointerDown = null;
|
|
18369
|
+
#activeDragCleanup = null;
|
|
17092
18370
|
#applyingValue = false;
|
|
17093
18371
|
#colorTip = null;
|
|
17094
18372
|
#directColorPicker = null;
|
|
@@ -17527,11 +18805,14 @@ class FigHandle extends HTMLElement {
|
|
|
17527
18805
|
}
|
|
17528
18806
|
|
|
17529
18807
|
#teardownDrag() {
|
|
18808
|
+
this.#activeDragCleanup?.();
|
|
18809
|
+
this.#activeDragCleanup = null;
|
|
17530
18810
|
if (this.#boundPointerDown) {
|
|
17531
18811
|
this.removeEventListener("pointerdown", this.#boundPointerDown);
|
|
17532
18812
|
this.#boundPointerDown = null;
|
|
17533
18813
|
}
|
|
17534
18814
|
this.#isDragging = false;
|
|
18815
|
+
this.#didDrag = false;
|
|
17535
18816
|
}
|
|
17536
18817
|
|
|
17537
18818
|
#onPointerDown(e) {
|
|
@@ -17540,6 +18821,7 @@ class FigHandle extends HTMLElement {
|
|
|
17540
18821
|
const container = this.#getContainer();
|
|
17541
18822
|
if (!container) return;
|
|
17542
18823
|
|
|
18824
|
+
this.#activeDragCleanup?.();
|
|
17543
18825
|
this.#isDragging = true;
|
|
17544
18826
|
const axes = this.#axes;
|
|
17545
18827
|
let lastRect = null;
|
|
@@ -17617,11 +18899,7 @@ class FigHandle extends HTMLElement {
|
|
|
17617
18899
|
};
|
|
17618
18900
|
|
|
17619
18901
|
const onUp = (e) => {
|
|
17620
|
-
|
|
17621
|
-
this.style.cursor = "";
|
|
17622
|
-
this.classList.remove("dragging");
|
|
17623
|
-
window.removeEventListener("pointermove", onMove);
|
|
17624
|
-
window.removeEventListener("pointerup", onUp);
|
|
18902
|
+
cleanup();
|
|
17625
18903
|
if (this.#didDrag) {
|
|
17626
18904
|
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
17627
18905
|
this.#syncValueAttribute();
|
|
@@ -17646,6 +18924,17 @@ class FigHandle extends HTMLElement {
|
|
|
17646
18924
|
this.#didDrag = false;
|
|
17647
18925
|
};
|
|
17648
18926
|
|
|
18927
|
+
const cleanup = () => {
|
|
18928
|
+
window.removeEventListener("pointermove", onMove);
|
|
18929
|
+
window.removeEventListener("pointerup", onUp);
|
|
18930
|
+
this.#isDragging = false;
|
|
18931
|
+
this.style.cursor = "";
|
|
18932
|
+
this.classList.remove("dragging");
|
|
18933
|
+
if (this.#activeDragCleanup === cleanup) {
|
|
18934
|
+
this.#activeDragCleanup = null;
|
|
18935
|
+
}
|
|
18936
|
+
};
|
|
18937
|
+
this.#activeDragCleanup = cleanup;
|
|
17649
18938
|
window.addEventListener("pointermove", onMove);
|
|
17650
18939
|
window.addEventListener("pointerup", onUp);
|
|
17651
18940
|
}
|
|
@@ -18114,6 +19403,7 @@ class FigMenu extends HTMLElement {
|
|
|
18114
19403
|
|
|
18115
19404
|
set open(val) {
|
|
18116
19405
|
if (val) {
|
|
19406
|
+
if (this.#isDisabled()) return;
|
|
18117
19407
|
this.setAttribute("open", "");
|
|
18118
19408
|
} else {
|
|
18119
19409
|
this.removeAttribute("open");
|
|
@@ -18160,19 +19450,17 @@ class FigMenu extends HTMLElement {
|
|
|
18160
19450
|
if (newValue === null || newValue === "false") {
|
|
18161
19451
|
this.#closeMenu();
|
|
18162
19452
|
} else {
|
|
19453
|
+
if (this.#isDisabled()) {
|
|
19454
|
+
this.removeAttribute("open");
|
|
19455
|
+
return;
|
|
19456
|
+
}
|
|
18163
19457
|
this.#openMenu();
|
|
18164
19458
|
}
|
|
18165
19459
|
return;
|
|
18166
19460
|
}
|
|
18167
19461
|
|
|
18168
19462
|
if (name === "disabled") {
|
|
18169
|
-
|
|
18170
|
-
if (newValue !== null && newValue !== "false") {
|
|
18171
|
-
this.#trigger.setAttribute("disabled", "");
|
|
18172
|
-
} else {
|
|
18173
|
-
this.#trigger.removeAttribute("disabled");
|
|
18174
|
-
}
|
|
18175
|
-
}
|
|
19463
|
+
this.#syncDisabled();
|
|
18176
19464
|
return;
|
|
18177
19465
|
}
|
|
18178
19466
|
|
|
@@ -18314,8 +19602,12 @@ class FigMenu extends HTMLElement {
|
|
|
18314
19602
|
}
|
|
18315
19603
|
|
|
18316
19604
|
#syncDisabled() {
|
|
19605
|
+
const disabled = this.#isDisabled();
|
|
19606
|
+
if (disabled) {
|
|
19607
|
+
if (this.open) this.removeAttribute("open");
|
|
19608
|
+
else this.#closeMenu();
|
|
19609
|
+
}
|
|
18317
19610
|
if (!this.#trigger) return;
|
|
18318
|
-
const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
|
|
18319
19611
|
if (disabled) {
|
|
18320
19612
|
this.#trigger.setAttribute("disabled", "");
|
|
18321
19613
|
this.#trigger.setAttribute("aria-disabled", "true");
|
|
@@ -18326,9 +19618,15 @@ class FigMenu extends HTMLElement {
|
|
|
18326
19618
|
}
|
|
18327
19619
|
}
|
|
18328
19620
|
|
|
19621
|
+
#isDisabled() {
|
|
19622
|
+
return (
|
|
19623
|
+
this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
|
|
19624
|
+
);
|
|
19625
|
+
}
|
|
19626
|
+
|
|
18329
19627
|
#handleTriggerClick(e) {
|
|
18330
19628
|
if (this.#usesContextMenuTrigger()) return;
|
|
18331
|
-
if (this
|
|
19629
|
+
if (this.#isDisabled()) return;
|
|
18332
19630
|
e.stopPropagation();
|
|
18333
19631
|
const popupShowing = this.#popup?.matches?.(":open") ?? false;
|
|
18334
19632
|
if (this.open && !popupShowing) {
|
|
@@ -18347,7 +19645,7 @@ class FigMenu extends HTMLElement {
|
|
|
18347
19645
|
|
|
18348
19646
|
#handleTriggerContextMenu(e) {
|
|
18349
19647
|
if (!this.#usesContextMenuTrigger()) return;
|
|
18350
|
-
if (this
|
|
19648
|
+
if (this.#isDisabled()) return;
|
|
18351
19649
|
e.preventDefault();
|
|
18352
19650
|
e.stopPropagation();
|
|
18353
19651
|
this.#showAtAfterPointerRelease(e.clientX, e.clientY);
|
|
@@ -18381,6 +19679,7 @@ class FigMenu extends HTMLElement {
|
|
|
18381
19679
|
}
|
|
18382
19680
|
|
|
18383
19681
|
#handlePopupClick(e) {
|
|
19682
|
+
if (this.#isDisabled()) return;
|
|
18384
19683
|
const item = e.target.closest("fig-menu-item");
|
|
18385
19684
|
if (!item) return;
|
|
18386
19685
|
if (item.hasAttribute("disabled") && item.getAttribute("disabled") !== "false") return;
|
|
@@ -18389,6 +19688,7 @@ class FigMenu extends HTMLElement {
|
|
|
18389
19688
|
}
|
|
18390
19689
|
|
|
18391
19690
|
#handleMenuKeydown(e) {
|
|
19691
|
+
if (this.#isDisabled()) return;
|
|
18392
19692
|
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
18393
19693
|
if (e.currentTarget === this && this.#popup?.contains(e.target)) return;
|
|
18394
19694
|
if (!this.open || !this.#popup?.matches?.(":open")) {
|
|
@@ -18474,6 +19774,7 @@ class FigMenu extends HTMLElement {
|
|
|
18474
19774
|
}
|
|
18475
19775
|
|
|
18476
19776
|
showAt(x, y) {
|
|
19777
|
+
if (this.#isDisabled()) return;
|
|
18477
19778
|
this.#virtualAnchor = {
|
|
18478
19779
|
getBoundingClientRect: () => ({
|
|
18479
19780
|
width: 0,
|
|
@@ -18491,12 +19792,16 @@ class FigMenu extends HTMLElement {
|
|
|
18491
19792
|
}
|
|
18492
19793
|
if (this.open) this.open = false;
|
|
18493
19794
|
requestAnimationFrame(() => {
|
|
19795
|
+
if (this.#isDisabled()) return;
|
|
18494
19796
|
this.open = true;
|
|
18495
19797
|
});
|
|
18496
19798
|
}
|
|
18497
19799
|
|
|
18498
19800
|
#openMenu() {
|
|
18499
|
-
if (!this.#popup)
|
|
19801
|
+
if (!this.#popup || this.#isDisabled()) {
|
|
19802
|
+
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
19803
|
+
return;
|
|
19804
|
+
}
|
|
18500
19805
|
this.#popup.open = true;
|
|
18501
19806
|
document.addEventListener("keydown", this.#boundMenuKeydown, true);
|
|
18502
19807
|
if (this.#trigger) {
|