@rogieking/figui3 6.21.0 → 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 +63 -191
- 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/dist/propskit.js +1 -591
- package/fig-editor.js +193 -68
- package/fig-lab.css +32 -307
- package/fig-lab.js +743 -1288
- package/fig.js +1653 -342
- package/package.json +2 -2
package/fig.js
CHANGED
|
@@ -223,6 +223,12 @@ function figDefineCustomizedBuiltIn(name, constructor, options) {
|
|
|
223
223
|
});
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
function figDefineElement(name, constructor) {
|
|
227
|
+
if (!customElements.get(name)) {
|
|
228
|
+
customElements.define(name, constructor);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
226
232
|
function figUniqueId() {
|
|
227
233
|
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
|
228
234
|
}
|
|
@@ -324,9 +330,11 @@ function figSupportsPopover() {
|
|
|
324
330
|
class FigButton extends HTMLElement {
|
|
325
331
|
type;
|
|
326
332
|
#selected;
|
|
333
|
+
#slottedDisabledStates = new WeakMap();
|
|
327
334
|
#a11yAttributes = ["aria-label", "aria-labelledby", "aria-describedby", "title"];
|
|
328
335
|
#boundHandleControlKeydown = this.#handleControlKeydown.bind(this);
|
|
329
336
|
#boundHandleClick = this.#handleClick.bind(this);
|
|
337
|
+
#boundHandleSlotChange = () => this.#syncSlottedControlDisabled();
|
|
330
338
|
#boundHandleFocus = () => {
|
|
331
339
|
if (this.button?.matches(":focus-visible")) {
|
|
332
340
|
this.setAttribute("data-focus-visible", "");
|
|
@@ -392,6 +400,9 @@ class FigButton extends HTMLElement {
|
|
|
392
400
|
this.button.addEventListener("blur", this.#boundHandleBlur);
|
|
393
401
|
}
|
|
394
402
|
|
|
403
|
+
const slot = this.shadowRoot.querySelector("slot");
|
|
404
|
+
slot?.removeEventListener("slotchange", this.#boundHandleSlotChange);
|
|
405
|
+
slot?.addEventListener("slotchange", this.#boundHandleSlotChange);
|
|
395
406
|
this.removeEventListener("keydown", this.#boundHandleControlKeydown);
|
|
396
407
|
this.addEventListener("keydown", this.#boundHandleControlKeydown);
|
|
397
408
|
|
|
@@ -508,9 +519,30 @@ class FigButton extends HTMLElement {
|
|
|
508
519
|
this.button.type = "button";
|
|
509
520
|
this.button.setAttribute("type", "button");
|
|
510
521
|
}
|
|
522
|
+
this.#syncSlottedControlDisabled();
|
|
511
523
|
this.#syncA11yAttributes();
|
|
512
524
|
this.#syncPressedState();
|
|
513
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
|
+
}
|
|
514
546
|
static get observedAttributes() {
|
|
515
547
|
return [
|
|
516
548
|
"disabled",
|
|
@@ -549,9 +581,12 @@ class FigButton extends HTMLElement {
|
|
|
549
581
|
}
|
|
550
582
|
disconnectedCallback() {
|
|
551
583
|
this.removeEventListener("keydown", this.#boundHandleControlKeydown);
|
|
584
|
+
this.shadowRoot
|
|
585
|
+
?.querySelector("slot")
|
|
586
|
+
?.removeEventListener("slotchange", this.#boundHandleSlotChange);
|
|
552
587
|
}
|
|
553
588
|
}
|
|
554
|
-
|
|
589
|
+
figDefineElement("fig-button", FigButton);
|
|
555
590
|
|
|
556
591
|
/**
|
|
557
592
|
* A custom dropdown/select element.
|
|
@@ -564,8 +599,6 @@ class FigDropdown extends HTMLElement {
|
|
|
564
599
|
#boundHandleSelectInput;
|
|
565
600
|
#boundHandleSelectChange;
|
|
566
601
|
#boundHandleSelectKeydown;
|
|
567
|
-
#selectedContentEnabled = false;
|
|
568
|
-
#selectedContentEl = null;
|
|
569
602
|
|
|
570
603
|
get label() {
|
|
571
604
|
return this.#label;
|
|
@@ -585,54 +618,6 @@ class FigDropdown extends HTMLElement {
|
|
|
585
618
|
this.#boundSlotChange = this.slotChange.bind(this);
|
|
586
619
|
}
|
|
587
620
|
|
|
588
|
-
#supportsSelectedContent() {
|
|
589
|
-
if (typeof CSS === "undefined" || typeof CSS.supports !== "function")
|
|
590
|
-
return false;
|
|
591
|
-
try {
|
|
592
|
-
return (
|
|
593
|
-
CSS.supports("appearance: base-select") &&
|
|
594
|
-
CSS.supports("selector(::picker(select))")
|
|
595
|
-
);
|
|
596
|
-
} catch {
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
#enableSelectedContentIfNeeded() {
|
|
602
|
-
const experimental = this.getAttribute("experimental") || "";
|
|
603
|
-
const wantsModern = experimental
|
|
604
|
-
.split(/\s+/)
|
|
605
|
-
.filter(Boolean)
|
|
606
|
-
.includes("modern");
|
|
607
|
-
|
|
608
|
-
if (!wantsModern || !this.#supportsSelectedContent()) {
|
|
609
|
-
this.#selectedContentEnabled = false;
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const button = document.createElement("button");
|
|
614
|
-
button.setAttribute("type", "button");
|
|
615
|
-
button.setAttribute("aria-hidden", "true");
|
|
616
|
-
const selected = document.createElement("selectedcontent");
|
|
617
|
-
button.appendChild(selected);
|
|
618
|
-
this.select.appendChild(button);
|
|
619
|
-
this.#selectedContentEnabled = true;
|
|
620
|
-
this.#selectedContentEl = selected;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
#syncSelectedContent() {
|
|
624
|
-
if (!this.#selectedContentEl) return;
|
|
625
|
-
const selectedOption = this.select.selectedOptions?.[0];
|
|
626
|
-
if (!selectedOption) {
|
|
627
|
-
this.#selectedContentEl.textContent = "";
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
// Fallback mirror for browsers that don't auto-project selectedcontent reliably.
|
|
631
|
-
this.#selectedContentEl.replaceChildren(
|
|
632
|
-
...Array.from(selectedOption.childNodes, (node) => node.cloneNode(true)),
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
621
|
#addEventListeners() {
|
|
637
622
|
this.select.addEventListener("input", this.#boundHandleSelectInput);
|
|
638
623
|
this.select.addEventListener("change", this.#boundHandleSelectChange);
|
|
@@ -697,8 +682,6 @@ class FigDropdown extends HTMLElement {
|
|
|
697
682
|
this.select.firstChild.remove();
|
|
698
683
|
}
|
|
699
684
|
|
|
700
|
-
this.#enableSelectedContentIfNeeded();
|
|
701
|
-
|
|
702
685
|
if (this.type === "dropdown") {
|
|
703
686
|
const hiddenOption = document.createElement("option");
|
|
704
687
|
hiddenOption.setAttribute("hidden", "true");
|
|
@@ -714,7 +697,6 @@ class FigDropdown extends HTMLElement {
|
|
|
714
697
|
if (selectedValue !== null) {
|
|
715
698
|
this.#syncSelectedValue(selectedValue);
|
|
716
699
|
}
|
|
717
|
-
this.#syncSelectedContent();
|
|
718
700
|
if (this.type === "dropdown") {
|
|
719
701
|
this.select.selectedIndex = -1;
|
|
720
702
|
}
|
|
@@ -737,7 +719,6 @@ class FigDropdown extends HTMLElement {
|
|
|
737
719
|
this.#selectedValue = selectedValue;
|
|
738
720
|
}
|
|
739
721
|
this.setAttribute("value", selectedValue);
|
|
740
|
-
this.#syncSelectedContent();
|
|
741
722
|
this.dispatchEvent(
|
|
742
723
|
new CustomEvent("input", {
|
|
743
724
|
detail: selectedValue,
|
|
@@ -765,7 +746,6 @@ class FigDropdown extends HTMLElement {
|
|
|
765
746
|
if (this.type === "dropdown") {
|
|
766
747
|
this.select.selectedIndex = -1;
|
|
767
748
|
}
|
|
768
|
-
this.#syncSelectedContent();
|
|
769
749
|
this.dispatchEvent(
|
|
770
750
|
new CustomEvent("change", {
|
|
771
751
|
detail: selectedValue,
|
|
@@ -778,7 +758,6 @@ class FigDropdown extends HTMLElement {
|
|
|
778
758
|
#handleSelectKeydown(e) {
|
|
779
759
|
if (this.closest('fig-button[type="select"]')) return;
|
|
780
760
|
if (e.key !== "Enter" || e.defaultPrevented) return;
|
|
781
|
-
if (this.#selectedContentEnabled && this.select.matches(":open")) return;
|
|
782
761
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
783
762
|
if (this.select.disabled || this.select.multiple) return;
|
|
784
763
|
if (typeof this.select.showPicker !== "function") return;
|
|
@@ -811,7 +790,7 @@ class FigDropdown extends HTMLElement {
|
|
|
811
790
|
this.setAttribute("value", value);
|
|
812
791
|
}
|
|
813
792
|
static get observedAttributes() {
|
|
814
|
-
return ["value", "type", "
|
|
793
|
+
return ["value", "type", "label", "disabled"];
|
|
815
794
|
}
|
|
816
795
|
#syncDisabled() {
|
|
817
796
|
const disabled =
|
|
@@ -824,7 +803,6 @@ class FigDropdown extends HTMLElement {
|
|
|
824
803
|
return;
|
|
825
804
|
}
|
|
826
805
|
if (this.select) this.select.value = value ?? "";
|
|
827
|
-
this.#syncSelectedContent();
|
|
828
806
|
}
|
|
829
807
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
830
808
|
if (name === "value") {
|
|
@@ -834,9 +812,6 @@ class FigDropdown extends HTMLElement {
|
|
|
834
812
|
this.type = newValue || "select";
|
|
835
813
|
if (this.isConnected) this.slotChange();
|
|
836
814
|
}
|
|
837
|
-
if (name === "experimental") {
|
|
838
|
-
this.slotChange();
|
|
839
|
-
}
|
|
840
815
|
if (name === "label") {
|
|
841
816
|
this.#label = newValue || "Menu";
|
|
842
817
|
this.select.setAttribute("aria-label", this.#label);
|
|
@@ -854,7 +829,7 @@ class FigDropdown extends HTMLElement {
|
|
|
854
829
|
}
|
|
855
830
|
}
|
|
856
831
|
|
|
857
|
-
|
|
832
|
+
figDefineElement("fig-dropdown", FigDropdown);
|
|
858
833
|
|
|
859
834
|
/* Tooltip */
|
|
860
835
|
/**
|
|
@@ -881,6 +856,8 @@ class FigTooltip extends HTMLElement {
|
|
|
881
856
|
#boundHidePopupOutsideClick;
|
|
882
857
|
#boundShowDelayedPopup;
|
|
883
858
|
#boundHandlePointerLeave;
|
|
859
|
+
#boundHandleFocus;
|
|
860
|
+
#boundHandleBlur;
|
|
884
861
|
#boundHandleTouchStart;
|
|
885
862
|
#boundHandleTouchMove;
|
|
886
863
|
#boundHandleTouchEnd;
|
|
@@ -890,6 +867,7 @@ class FigTooltip extends HTMLElement {
|
|
|
890
867
|
#parentDialog = null;
|
|
891
868
|
#triggerEl = null;
|
|
892
869
|
#childObserver = null;
|
|
870
|
+
#suppressFocusOpen = false;
|
|
893
871
|
#touchTimeout;
|
|
894
872
|
#isTouching = false;
|
|
895
873
|
constructor() {
|
|
@@ -902,6 +880,13 @@ class FigTooltip extends HTMLElement {
|
|
|
902
880
|
this.#boundHidePopupOutsideClick = this.hidePopupOutsideClick.bind(this);
|
|
903
881
|
this.#boundShowDelayedPopup = this.showDelayedPopup.bind(this);
|
|
904
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
|
+
};
|
|
905
890
|
this.#boundHandleTouchStart = this.#handleTouchStart.bind(this);
|
|
906
891
|
this.#boundHandleTouchMove = this.#handleTouchMove.bind(this);
|
|
907
892
|
this.#boundHandleTouchEnd = this.#handleTouchEnd.bind(this);
|
|
@@ -996,6 +981,8 @@ class FigTooltip extends HTMLElement {
|
|
|
996
981
|
trigger.addEventListener("touchcancel", this.#boundHandleTouchCancel, {
|
|
997
982
|
passive: true,
|
|
998
983
|
});
|
|
984
|
+
trigger.addEventListener("focus", this.#boundHandleFocus);
|
|
985
|
+
trigger.addEventListener("blur", this.#boundHandleBlur);
|
|
999
986
|
} else if (this.action === "click") {
|
|
1000
987
|
trigger.addEventListener("click", this.#boundShowDelayedPopup);
|
|
1001
988
|
trigger.addEventListener("touchstart", this.#boundShowDelayedPopup, {
|
|
@@ -1014,6 +1001,8 @@ class FigTooltip extends HTMLElement {
|
|
|
1014
1001
|
trigger.removeEventListener("touchmove", this.#boundHandleTouchMove);
|
|
1015
1002
|
trigger.removeEventListener("touchend", this.#boundHandleTouchEnd);
|
|
1016
1003
|
trigger.removeEventListener("touchcancel", this.#boundHandleTouchCancel);
|
|
1004
|
+
trigger.removeEventListener("focus", this.#boundHandleFocus);
|
|
1005
|
+
trigger.removeEventListener("blur", this.#boundHandleBlur);
|
|
1017
1006
|
} else if (this.action === "click") {
|
|
1018
1007
|
trigger.removeEventListener("click", this.#boundShowDelayedPopup);
|
|
1019
1008
|
trigger.removeEventListener("touchstart", this.#boundShowDelayedPopup);
|
|
@@ -1415,6 +1404,16 @@ class FigTooltip extends HTMLElement {
|
|
|
1415
1404
|
if (!(node instanceof FigTooltip)) continue;
|
|
1416
1405
|
if (node.action !== "hover") continue;
|
|
1417
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);
|
|
1418
1417
|
if (node.isOpen || node.timeout) node.hidePopup();
|
|
1419
1418
|
}
|
|
1420
1419
|
for (const anchor of Array.from(FigTooltip.#programmaticAnchors)) {
|
|
@@ -1495,7 +1494,7 @@ class FigTooltip extends HTMLElement {
|
|
|
1495
1494
|
}
|
|
1496
1495
|
}
|
|
1497
1496
|
|
|
1498
|
-
|
|
1497
|
+
figDefineElement("fig-tooltip", FigTooltip);
|
|
1499
1498
|
|
|
1500
1499
|
/* Text Truncation */
|
|
1501
1500
|
class FigTruncate extends HTMLElement {
|
|
@@ -1574,7 +1573,7 @@ class FigTruncate extends HTMLElement {
|
|
|
1574
1573
|
FigTooltip.hide(this);
|
|
1575
1574
|
}
|
|
1576
1575
|
}
|
|
1577
|
-
|
|
1576
|
+
figDefineElement("fig-truncate", FigTruncate);
|
|
1578
1577
|
|
|
1579
1578
|
/* Dialog */
|
|
1580
1579
|
/**
|
|
@@ -3868,7 +3867,7 @@ class FigTab extends HTMLElement {
|
|
|
3868
3867
|
}
|
|
3869
3868
|
}
|
|
3870
3869
|
}
|
|
3871
|
-
|
|
3870
|
+
figDefineElement("fig-tab", FigTab);
|
|
3872
3871
|
|
|
3873
3872
|
/**
|
|
3874
3873
|
* A custom tabs container element.
|
|
@@ -4198,7 +4197,7 @@ class FigTabs extends HTMLElement {
|
|
|
4198
4197
|
}
|
|
4199
4198
|
}
|
|
4200
4199
|
}
|
|
4201
|
-
|
|
4200
|
+
figDefineElement("fig-tabs", FigTabs);
|
|
4202
4201
|
|
|
4203
4202
|
/* Segmented Control */
|
|
4204
4203
|
/**
|
|
@@ -4286,7 +4285,7 @@ class FigSegment extends HTMLElement {
|
|
|
4286
4285
|
}
|
|
4287
4286
|
}
|
|
4288
4287
|
}
|
|
4289
|
-
|
|
4288
|
+
figDefineElement("fig-segment", FigSegment);
|
|
4290
4289
|
|
|
4291
4290
|
/**
|
|
4292
4291
|
* A custom segmented control container element.
|
|
@@ -4770,7 +4769,7 @@ class FigSegmentedControl extends HTMLElement {
|
|
|
4770
4769
|
}
|
|
4771
4770
|
}
|
|
4772
4771
|
}
|
|
4773
|
-
|
|
4772
|
+
figDefineElement("fig-segmented-control", FigSegmentedControl);
|
|
4774
4773
|
|
|
4775
4774
|
/* Options */
|
|
4776
4775
|
/**
|
|
@@ -5029,93 +5028,1293 @@ class FigOptions extends HTMLElement {
|
|
|
5029
5028
|
);
|
|
5030
5029
|
});
|
|
5031
5030
|
|
|
5032
|
-
this.appendChild(dd);
|
|
5033
|
-
this.#childControl = dd;
|
|
5034
|
-
this.#currentMode = "dropdown";
|
|
5031
|
+
this.appendChild(dd);
|
|
5032
|
+
this.#childControl = dd;
|
|
5033
|
+
this.#currentMode = "dropdown";
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
#rebuildCurrentControl() {
|
|
5037
|
+
if (this.#currentMode === "segments") {
|
|
5038
|
+
this.#renderSegments();
|
|
5039
|
+
requestAnimationFrame(() => {
|
|
5040
|
+
requestAnimationFrame(() => this.#checkOverflow());
|
|
5041
|
+
});
|
|
5042
|
+
} else {
|
|
5043
|
+
this.#renderDropdown();
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
|
|
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;
|
|
6140
|
+
}
|
|
6141
|
+
|
|
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);
|
|
5035
6151
|
}
|
|
5036
6152
|
|
|
5037
|
-
#
|
|
5038
|
-
if (
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
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;
|
|
5045
6174
|
}
|
|
5046
|
-
}
|
|
5047
6175
|
|
|
5048
|
-
|
|
5049
|
-
if (!
|
|
5050
|
-
const val = this.getAttribute("value") || "";
|
|
5051
|
-
this.#childControl.value = val;
|
|
5052
|
-
}
|
|
6176
|
+
const options = this.#getOptions({ enabledOnly: true });
|
|
6177
|
+
if (!options.length) return;
|
|
5053
6178
|
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
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
|
+
}
|
|
5060
6212
|
}
|
|
5061
6213
|
}
|
|
5062
6214
|
|
|
5063
|
-
#
|
|
5064
|
-
this
|
|
5065
|
-
this.#
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
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;
|
|
5069
6220
|
}
|
|
5070
6221
|
|
|
5071
|
-
#
|
|
5072
|
-
const
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
const cs = getComputedStyle(seg);
|
|
5078
|
-
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
5079
|
-
const padR = parseFloat(cs.paddingRight) || 0;
|
|
5080
|
-
const contentWidth = segWidth - padL - padR;
|
|
5081
|
-
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;
|
|
5082
6228
|
}
|
|
5083
6229
|
|
|
5084
|
-
#
|
|
5085
|
-
|
|
5086
|
-
for (const seg of segments) {
|
|
5087
|
-
if (this.#isSegmentTruncated(seg)) return true;
|
|
5088
|
-
}
|
|
5089
|
-
return false;
|
|
6230
|
+
#getEnabledOptions() {
|
|
6231
|
+
return this.#getOptions({ enabledOnly: true });
|
|
5090
6232
|
}
|
|
5091
6233
|
|
|
5092
|
-
#
|
|
5093
|
-
|
|
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
|
+
}
|
|
5094
6269
|
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
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);
|
|
5114
6298
|
}
|
|
5115
|
-
|
|
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");
|
|
5116
6315
|
}
|
|
5117
6316
|
}
|
|
5118
|
-
|
|
6317
|
+
figDefineElement("fig-select", FigSelect);
|
|
5119
6318
|
|
|
5120
6319
|
/* Slider */
|
|
5121
6320
|
/**
|
|
@@ -5800,7 +6999,7 @@ class FigSlider extends HTMLElement {
|
|
|
5800
6999
|
}
|
|
5801
7000
|
}
|
|
5802
7001
|
}
|
|
5803
|
-
|
|
7002
|
+
figDefineElement("fig-slider", FigSlider);
|
|
5804
7003
|
|
|
5805
7004
|
/**
|
|
5806
7005
|
* A custom text input element.
|
|
@@ -5817,6 +7016,8 @@ customElements.define("fig-slider", FigSlider);
|
|
|
5817
7016
|
class FigInputText extends HTMLElement {
|
|
5818
7017
|
#isInteracting = false;
|
|
5819
7018
|
#passwordVisible = false;
|
|
7019
|
+
#value = "";
|
|
7020
|
+
#reflectingValue = false;
|
|
5820
7021
|
#boundMouseMove;
|
|
5821
7022
|
#boundMouseUp;
|
|
5822
7023
|
#boundWindowBlur;
|
|
@@ -5860,7 +7061,9 @@ class FigInputText extends HTMLElement {
|
|
|
5860
7061
|
new CustomEvent("input", { detail: this.value, bubbles: true }),
|
|
5861
7062
|
);
|
|
5862
7063
|
};
|
|
5863
|
-
this.#boundFocusControl =
|
|
7064
|
+
this.#boundFocusControl = () => {
|
|
7065
|
+
if (!this.disabled) this.focus();
|
|
7066
|
+
};
|
|
5864
7067
|
this.#boundAdornmentClick = this.#handleAdornmentClick.bind(this);
|
|
5865
7068
|
}
|
|
5866
7069
|
|
|
@@ -5901,6 +7104,7 @@ class FigInputText extends HTMLElement {
|
|
|
5901
7104
|
this.#syncSearchClear();
|
|
5902
7105
|
this.#syncSearchClearVisibility();
|
|
5903
7106
|
this.#syncPasswordToggle();
|
|
7107
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
5904
7108
|
figNormalizeTextOnlyInputSlots(this);
|
|
5905
7109
|
this.#startObserver();
|
|
5906
7110
|
|
|
@@ -5976,6 +7180,7 @@ class FigInputText extends HTMLElement {
|
|
|
5976
7180
|
#handleAdornmentClick(event) {
|
|
5977
7181
|
const adornment = event.target?.closest?.("[slot]");
|
|
5978
7182
|
if (!adornment || adornment.parentElement !== this) return;
|
|
7183
|
+
if (this.disabled) return;
|
|
5979
7184
|
this.focus();
|
|
5980
7185
|
}
|
|
5981
7186
|
#startObserver() {
|
|
@@ -6007,6 +7212,7 @@ class FigInputText extends HTMLElement {
|
|
|
6007
7212
|
this.#syncSearchClear();
|
|
6008
7213
|
this.#syncSearchClearVisibility();
|
|
6009
7214
|
this.#syncPasswordToggle();
|
|
7215
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6010
7216
|
figNormalizeTextOnlyInputSlots(this);
|
|
6011
7217
|
}
|
|
6012
7218
|
#syncInputA11yAttributes() {
|
|
@@ -6091,6 +7297,7 @@ class FigInputText extends HTMLElement {
|
|
|
6091
7297
|
button.addEventListener("click", (e) => {
|
|
6092
7298
|
e.preventDefault();
|
|
6093
7299
|
e.stopPropagation();
|
|
7300
|
+
if (this.disabled) return;
|
|
6094
7301
|
if (!this.input || this.input.value === "") {
|
|
6095
7302
|
this.focus();
|
|
6096
7303
|
return;
|
|
@@ -6149,6 +7356,7 @@ class FigInputText extends HTMLElement {
|
|
|
6149
7356
|
button.addEventListener("click", (e) => {
|
|
6150
7357
|
e.preventDefault();
|
|
6151
7358
|
e.stopPropagation();
|
|
7359
|
+
if (this.disabled) return;
|
|
6152
7360
|
this.#passwordVisible = !this.#passwordVisible;
|
|
6153
7361
|
if (this.input) {
|
|
6154
7362
|
this.input.type = this.#passwordVisible ? "text" : "password";
|
|
@@ -6166,6 +7374,14 @@ class FigInputText extends HTMLElement {
|
|
|
6166
7374
|
button?.setAttribute("aria-label", label);
|
|
6167
7375
|
icon?.setAttribute("name", this.#passwordVisible ? "visible" : "hidden");
|
|
6168
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
|
+
}
|
|
6169
7385
|
#transformNumber(value) {
|
|
6170
7386
|
if (value === "") return "";
|
|
6171
7387
|
let transformed = Number(value) * (this.transform || 1);
|
|
@@ -6269,15 +7485,29 @@ class FigInputText extends HTMLElement {
|
|
|
6269
7485
|
return Number.isInteger(rounded) ? rounded : rounded.toFixed(precision);
|
|
6270
7486
|
}
|
|
6271
7487
|
|
|
6272
|
-
/*
|
|
6273
7488
|
get value() {
|
|
6274
|
-
return this
|
|
7489
|
+
return this.#value;
|
|
6275
7490
|
}
|
|
6276
7491
|
|
|
6277
7492
|
set value(val) {
|
|
6278
|
-
|
|
6279
|
-
this
|
|
6280
|
-
|
|
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
|
+
}
|
|
6281
7511
|
|
|
6282
7512
|
static get observedAttributes() {
|
|
6283
7513
|
return [
|
|
@@ -6307,6 +7537,7 @@ class FigInputText extends HTMLElement {
|
|
|
6307
7537
|
case "disabled":
|
|
6308
7538
|
this.disabled = this.input.disabled =
|
|
6309
7539
|
newValue !== null && newValue !== "false";
|
|
7540
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6310
7541
|
break;
|
|
6311
7542
|
case "readonly":
|
|
6312
7543
|
this.readonly = newValue !== null && newValue !== "false";
|
|
@@ -6323,13 +7554,9 @@ class FigInputText extends HTMLElement {
|
|
|
6323
7554
|
let value = newValue;
|
|
6324
7555
|
if (this.type === "number") {
|
|
6325
7556
|
value = this.#sanitizeInput(value, false);
|
|
6326
|
-
this.value = value;
|
|
6327
|
-
this.input.value = this.#transformNumber(value);
|
|
6328
|
-
} else {
|
|
6329
|
-
this.value = value;
|
|
6330
|
-
this.input.value = value;
|
|
6331
7557
|
}
|
|
6332
|
-
this.#
|
|
7558
|
+
this.#value = value ?? "";
|
|
7559
|
+
if (!this.#reflectingValue) this.#syncRenderedValue(this.#value);
|
|
6333
7560
|
break;
|
|
6334
7561
|
case "min":
|
|
6335
7562
|
case "max":
|
|
@@ -6372,6 +7599,7 @@ class FigInputText extends HTMLElement {
|
|
|
6372
7599
|
this.#syncSearchClear();
|
|
6373
7600
|
this.#syncSearchClearVisibility();
|
|
6374
7601
|
this.#syncPasswordToggle();
|
|
7602
|
+
this.#syncGeneratedAdornmentDisabled();
|
|
6375
7603
|
break;
|
|
6376
7604
|
case "multiline": {
|
|
6377
7605
|
const next = newValue !== null && newValue !== "false";
|
|
@@ -6404,7 +7632,7 @@ class FigInputText extends HTMLElement {
|
|
|
6404
7632
|
}
|
|
6405
7633
|
}
|
|
6406
7634
|
}
|
|
6407
|
-
|
|
7635
|
+
figDefineElement("fig-input-text", FigInputText);
|
|
6408
7636
|
|
|
6409
7637
|
/**
|
|
6410
7638
|
* A custom numeric input element that uses type="text" with inputmode="decimal".
|
|
@@ -7109,7 +8337,7 @@ class FigInputNumber extends HTMLElement {
|
|
|
7109
8337
|
}
|
|
7110
8338
|
}
|
|
7111
8339
|
}
|
|
7112
|
-
|
|
8340
|
+
figDefineElement("fig-input-number", FigInputNumber);
|
|
7113
8341
|
|
|
7114
8342
|
/* Avatar */
|
|
7115
8343
|
class FigAvatar extends HTMLElement {
|
|
@@ -7161,7 +8389,7 @@ class FigAvatar extends HTMLElement {
|
|
|
7161
8389
|
}
|
|
7162
8390
|
}
|
|
7163
8391
|
}
|
|
7164
|
-
|
|
8392
|
+
figDefineElement("fig-avatar", FigAvatar);
|
|
7165
8393
|
|
|
7166
8394
|
/* Form Field */
|
|
7167
8395
|
class FigField extends HTMLElement {
|
|
@@ -7360,7 +8588,7 @@ class FigField extends HTMLElement {
|
|
|
7360
8588
|
}
|
|
7361
8589
|
}
|
|
7362
8590
|
}
|
|
7363
|
-
|
|
8591
|
+
figDefineElement("fig-field", FigField);
|
|
7364
8592
|
|
|
7365
8593
|
/* Color swatch */
|
|
7366
8594
|
class FigInputColor extends HTMLElement {
|
|
@@ -7399,10 +8627,12 @@ class FigInputColor extends HTMLElement {
|
|
|
7399
8627
|
|
|
7400
8628
|
#fillPickerAttrs() {
|
|
7401
8629
|
const attrs = {};
|
|
7402
|
-
const experimental = this.getAttribute("experimental");
|
|
7403
|
-
if (experimental) attrs["experimental"] = experimental;
|
|
7404
8630
|
for (const { name, value } of this.attributes) {
|
|
7405
|
-
if (
|
|
8631
|
+
if (
|
|
8632
|
+
name.startsWith("picker-") &&
|
|
8633
|
+
name !== "picker-anchor" &&
|
|
8634
|
+
name !== "picker-experimental"
|
|
8635
|
+
) {
|
|
7406
8636
|
attrs[name.slice(7)] = value;
|
|
7407
8637
|
}
|
|
7408
8638
|
}
|
|
@@ -7609,8 +8839,8 @@ class FigInputColor extends HTMLElement {
|
|
|
7609
8839
|
|
|
7610
8840
|
const picker = document.createElement("fig-fill-picker");
|
|
7611
8841
|
picker.innerHTML = "<span hidden></span>";
|
|
7612
|
-
picker.addEventListener("input", this.#
|
|
7613
|
-
picker.addEventListener("change", this.#
|
|
8842
|
+
picker.addEventListener("input", this.#boundFillPickerInput);
|
|
8843
|
+
picker.addEventListener("change", this.#boundChange);
|
|
7614
8844
|
this.appendChild(picker);
|
|
7615
8845
|
this.#fillPicker = picker;
|
|
7616
8846
|
this.#syncFillPicker();
|
|
@@ -7675,8 +8905,22 @@ class FigInputColor extends HTMLElement {
|
|
|
7675
8905
|
}
|
|
7676
8906
|
|
|
7677
8907
|
#setValues(hexValue) {
|
|
7678
|
-
|
|
7679
|
-
|
|
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;
|
|
7680
8924
|
this.value = this.rgbAlphaToHex(
|
|
7681
8925
|
{
|
|
7682
8926
|
r: isNaN(this.rgba.r) ? 0 : this.rgba.r,
|
|
@@ -7867,7 +9111,6 @@ class FigInputColor extends HTMLElement {
|
|
|
7867
9111
|
"value",
|
|
7868
9112
|
"style",
|
|
7869
9113
|
"mode",
|
|
7870
|
-
"experimental",
|
|
7871
9114
|
"alpha",
|
|
7872
9115
|
"text",
|
|
7873
9116
|
"disabled",
|
|
@@ -7985,6 +9228,7 @@ class FigInputColor extends HTMLElement {
|
|
|
7985
9228
|
}
|
|
7986
9229
|
|
|
7987
9230
|
convertToRGBA(color) {
|
|
9231
|
+
if (typeof color !== "string") return null;
|
|
7988
9232
|
let r,
|
|
7989
9233
|
g,
|
|
7990
9234
|
b,
|
|
@@ -8056,7 +9300,7 @@ class FigInputColor extends HTMLElement {
|
|
|
8056
9300
|
return { r, g, b, a };
|
|
8057
9301
|
}
|
|
8058
9302
|
}
|
|
8059
|
-
|
|
9303
|
+
figDefineElement("fig-input-color", FigInputColor);
|
|
8060
9304
|
|
|
8061
9305
|
/* Input Fill */
|
|
8062
9306
|
const GRADIENT_INTERPOLATION_SPACES = [
|
|
@@ -8409,7 +9653,6 @@ class FigInputFill extends HTMLElement {
|
|
|
8409
9653
|
"value",
|
|
8410
9654
|
"disabled",
|
|
8411
9655
|
"mode",
|
|
8412
|
-
"experimental",
|
|
8413
9656
|
"alpha",
|
|
8414
9657
|
"aria-label",
|
|
8415
9658
|
"aria-describedby",
|
|
@@ -8493,13 +9736,15 @@ class FigInputFill extends HTMLElement {
|
|
|
8493
9736
|
// Backward-compat: direct attributes forwarded to fill picker
|
|
8494
9737
|
const mode = this.getAttribute("mode");
|
|
8495
9738
|
if (mode) attrs["mode"] = mode;
|
|
8496
|
-
const experimental = this.getAttribute("experimental");
|
|
8497
|
-
if (experimental) attrs["experimental"] = experimental;
|
|
8498
9739
|
const alpha = this.getAttribute("alpha");
|
|
8499
9740
|
if (alpha) attrs["alpha"] = alpha;
|
|
8500
9741
|
// picker-* overrides (except anchor, handled programmatically)
|
|
8501
9742
|
for (const { name, value } of this.attributes) {
|
|
8502
|
-
if (
|
|
9743
|
+
if (
|
|
9744
|
+
name.startsWith("picker-") &&
|
|
9745
|
+
name !== "picker-anchor" &&
|
|
9746
|
+
name !== "picker-experimental"
|
|
9747
|
+
) {
|
|
8503
9748
|
attrs[name.slice(7)] = value;
|
|
8504
9749
|
}
|
|
8505
9750
|
}
|
|
@@ -8735,7 +9980,6 @@ class FigInputFill extends HTMLElement {
|
|
|
8735
9980
|
if (detail.video) this.#video = detail.video;
|
|
8736
9981
|
break;
|
|
8737
9982
|
}
|
|
8738
|
-
|
|
8739
9983
|
// Update controls (don't re-render to keep dialog open)
|
|
8740
9984
|
if (typeChanged) {
|
|
8741
9985
|
this.#updateControlsForType();
|
|
@@ -9135,7 +10379,6 @@ class FigInputFill extends HTMLElement {
|
|
|
9135
10379
|
this.#syncDisabled();
|
|
9136
10380
|
break;
|
|
9137
10381
|
case "mode":
|
|
9138
|
-
case "experimental":
|
|
9139
10382
|
// Pass through to internal fill picker
|
|
9140
10383
|
if (this.#fillPicker) {
|
|
9141
10384
|
if (newValue) {
|
|
@@ -9157,7 +10400,7 @@ class FigInputFill extends HTMLElement {
|
|
|
9157
10400
|
}
|
|
9158
10401
|
}
|
|
9159
10402
|
}
|
|
9160
|
-
|
|
10403
|
+
figDefineElement("fig-input-fill", FigInputFill);
|
|
9161
10404
|
|
|
9162
10405
|
/* Input Palette */
|
|
9163
10406
|
/**
|
|
@@ -9177,6 +10420,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9177
10420
|
#expandedPickers = [];
|
|
9178
10421
|
#renderRAF = null;
|
|
9179
10422
|
#boundHandleKeyDown = this.#handleKeyDown.bind(this);
|
|
10423
|
+
#boundHandleHostFocus = () => this.focus();
|
|
9180
10424
|
|
|
9181
10425
|
static get observedAttributes() {
|
|
9182
10426
|
return ["value", "disabled", "min", "max", "open", "fixed"];
|
|
@@ -9220,7 +10464,9 @@ class FigInputPalette extends HTMLElement {
|
|
|
9220
10464
|
}
|
|
9221
10465
|
|
|
9222
10466
|
connectedCallback() {
|
|
9223
|
-
|
|
10467
|
+
this.setAttribute("tabindex", "-1");
|
|
10468
|
+
this.removeEventListener("focus", this.#boundHandleHostFocus);
|
|
10469
|
+
this.addEventListener("focus", this.#boundHandleHostFocus);
|
|
9224
10470
|
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
9225
10471
|
this.addEventListener("keydown", this.#boundHandleKeyDown);
|
|
9226
10472
|
if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
|
|
@@ -9238,13 +10484,14 @@ class FigInputPalette extends HTMLElement {
|
|
|
9238
10484
|
this.#renderRAF = null;
|
|
9239
10485
|
}
|
|
9240
10486
|
this.removeEventListener("keydown", this.#boundHandleKeyDown);
|
|
10487
|
+
this.removeEventListener("focus", this.#boundHandleHostFocus);
|
|
9241
10488
|
this.#inlinePickers = [];
|
|
9242
10489
|
this.#expandedPickers = [];
|
|
9243
10490
|
}
|
|
9244
10491
|
|
|
9245
10492
|
#handleKeyDown(event) {
|
|
9246
10493
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
9247
|
-
if (event.target !== this
|
|
10494
|
+
if (event.target !== this.querySelector(".palette-colors-inline")) return;
|
|
9248
10495
|
if (this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") return;
|
|
9249
10496
|
event.preventDefault();
|
|
9250
10497
|
event.stopPropagation();
|
|
@@ -9269,7 +10516,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9269
10516
|
this.#render();
|
|
9270
10517
|
break;
|
|
9271
10518
|
case "open":
|
|
9272
|
-
|
|
10519
|
+
this.#syncTriggerState();
|
|
9273
10520
|
break;
|
|
9274
10521
|
}
|
|
9275
10522
|
}
|
|
@@ -9365,8 +10612,13 @@ class FigInputPalette extends HTMLElement {
|
|
|
9365
10612
|
const inlineWrap = document.createElement("div");
|
|
9366
10613
|
inlineWrap.className = "palette-colors-inline";
|
|
9367
10614
|
inlineWrap.setAttribute("role", "button");
|
|
10615
|
+
inlineWrap.setAttribute("tabindex", disabled ? "-1" : "+0");
|
|
9368
10616
|
inlineWrap.setAttribute("aria-expanded", String(this.open));
|
|
9369
10617
|
inlineWrap.setAttribute("aria-label", "Edit palette colors");
|
|
10618
|
+
inlineWrap.addEventListener("blur", () => {
|
|
10619
|
+
inlineWrap.style.removeProperty("outline");
|
|
10620
|
+
inlineWrap.style.removeProperty("outline-offset");
|
|
10621
|
+
});
|
|
9370
10622
|
const openPalette = () => {
|
|
9371
10623
|
if (
|
|
9372
10624
|
this.hasAttribute("disabled") &&
|
|
@@ -9377,12 +10629,6 @@ class FigInputPalette extends HTMLElement {
|
|
|
9377
10629
|
inlineWrap.setAttribute("aria-expanded", "true");
|
|
9378
10630
|
};
|
|
9379
10631
|
inlineWrap.addEventListener("click", openPalette);
|
|
9380
|
-
inlineWrap.addEventListener("keydown", (event) => {
|
|
9381
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
9382
|
-
event.preventDefault();
|
|
9383
|
-
event.stopPropagation();
|
|
9384
|
-
openPalette();
|
|
9385
|
-
});
|
|
9386
10632
|
|
|
9387
10633
|
const wrap = document.createElement("div");
|
|
9388
10634
|
wrap.className = "palette-colors";
|
|
@@ -9393,6 +10639,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9393
10639
|
});
|
|
9394
10640
|
inlineWrap.appendChild(wrap);
|
|
9395
10641
|
this.appendChild(inlineWrap);
|
|
10642
|
+
this.#syncTriggerState();
|
|
9396
10643
|
|
|
9397
10644
|
if (!this.#isFixed) this.#createAddButton(disabled, this);
|
|
9398
10645
|
|
|
@@ -9589,6 +10836,31 @@ class FigInputPalette extends HTMLElement {
|
|
|
9589
10836
|
else addBtn.removeAttribute("disabled");
|
|
9590
10837
|
}
|
|
9591
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();
|
|
9592
10864
|
}
|
|
9593
10865
|
|
|
9594
10866
|
#syncRemoveButtons(disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false") {
|
|
@@ -9617,7 +10889,7 @@ class FigInputPalette extends HTMLElement {
|
|
|
9617
10889
|
);
|
|
9618
10890
|
}
|
|
9619
10891
|
}
|
|
9620
|
-
|
|
10892
|
+
figDefineElement("fig-input-palette", FigInputPalette);
|
|
9621
10893
|
|
|
9622
10894
|
/* Input Gradient */
|
|
9623
10895
|
/**
|
|
@@ -9917,11 +11189,9 @@ class FigInputGradient extends HTMLElement {
|
|
|
9917
11189
|
const mode = this.#editMode;
|
|
9918
11190
|
|
|
9919
11191
|
if (mode === "picker" && hasFigFillPicker()) {
|
|
9920
|
-
const experimental = this.getAttribute("experimental");
|
|
9921
|
-
const expAttr = experimental ? ` experimental="${experimental}"` : "";
|
|
9922
11192
|
const gradientValue = JSON.stringify(this.value);
|
|
9923
11193
|
this.innerHTML = `
|
|
9924
|
-
<fig-fill-picker mode="gradient"
|
|
11194
|
+
<fig-fill-picker mode="gradient" value='${gradientValue}'${disabled ? " disabled" : ""}>
|
|
9925
11195
|
<fig-swatch background="${this.#buildGradientCSS()}"${this.#swatchSizeAttr()}${disabled ? " disabled" : ""}></fig-swatch>
|
|
9926
11196
|
</fig-fill-picker>`;
|
|
9927
11197
|
this.#swatch = this.querySelector("fig-swatch");
|
|
@@ -10529,7 +11799,7 @@ class FigInputGradient extends HTMLElement {
|
|
|
10529
11799
|
}
|
|
10530
11800
|
}
|
|
10531
11801
|
}
|
|
10532
|
-
|
|
11802
|
+
figDefineElement("fig-input-gradient", FigInputGradient);
|
|
10533
11803
|
|
|
10534
11804
|
/* Checkbox */
|
|
10535
11805
|
/**
|
|
@@ -10767,7 +12037,7 @@ class FigCheckbox extends HTMLElement {
|
|
|
10767
12037
|
);
|
|
10768
12038
|
}
|
|
10769
12039
|
}
|
|
10770
|
-
|
|
12040
|
+
figDefineElement("fig-checkbox", FigCheckbox);
|
|
10771
12041
|
|
|
10772
12042
|
/* Radio */
|
|
10773
12043
|
/**
|
|
@@ -10787,7 +12057,7 @@ class FigRadio extends FigCheckbox {
|
|
|
10787
12057
|
else this.input.removeAttribute("name");
|
|
10788
12058
|
}
|
|
10789
12059
|
}
|
|
10790
|
-
|
|
12060
|
+
figDefineElement("fig-radio", FigRadio);
|
|
10791
12061
|
|
|
10792
12062
|
/* Switch */
|
|
10793
12063
|
/**
|
|
@@ -10804,7 +12074,7 @@ class FigSwitch extends FigCheckbox {
|
|
|
10804
12074
|
this.input.setAttribute("role", "switch");
|
|
10805
12075
|
}
|
|
10806
12076
|
}
|
|
10807
|
-
|
|
12077
|
+
figDefineElement("fig-switch", FigSwitch);
|
|
10808
12078
|
|
|
10809
12079
|
/* Combo Input */
|
|
10810
12080
|
/**
|
|
@@ -10813,7 +12083,6 @@ customElements.define("fig-switch", FigSwitch);
|
|
|
10813
12083
|
* @attr {string} placeholder - Placeholder text for the input
|
|
10814
12084
|
* @attr {string} value - The current input value
|
|
10815
12085
|
* @attr {boolean} disabled - Disables the input and dropdown button
|
|
10816
|
-
* @attr {string} experimental - Feature flag passed to internal fig-dropdown
|
|
10817
12086
|
*/
|
|
10818
12087
|
class FigComboInput extends HTMLElement {
|
|
10819
12088
|
static observedAttributes = [
|
|
@@ -10821,7 +12090,6 @@ class FigComboInput extends HTMLElement {
|
|
|
10821
12090
|
"placeholder",
|
|
10822
12091
|
"value",
|
|
10823
12092
|
"disabled",
|
|
10824
|
-
"experimental",
|
|
10825
12093
|
"aria-label",
|
|
10826
12094
|
"aria-labelledby",
|
|
10827
12095
|
"aria-describedby",
|
|
@@ -10902,15 +12170,11 @@ class FigComboInput extends HTMLElement {
|
|
|
10902
12170
|
const options = this.#getOptions();
|
|
10903
12171
|
const placeholder = this.getAttribute("placeholder") || "";
|
|
10904
12172
|
const currentValue = this.value;
|
|
10905
|
-
const experimental = this.getAttribute("experimental");
|
|
10906
|
-
const expAttr = experimental
|
|
10907
|
-
? ` experimental="${figEscapeAttribute(experimental)}"`
|
|
10908
|
-
: "";
|
|
10909
12173
|
const dropdownLabel = this.#dropdownLabel();
|
|
10910
12174
|
|
|
10911
12175
|
const dropdownHTML = this.#usesCustomDropdown
|
|
10912
12176
|
? ""
|
|
10913
|
-
: `<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>`;
|
|
10914
12178
|
|
|
10915
12179
|
this.innerHTML = `<div class="input-combo">
|
|
10916
12180
|
<fig-input-text placeholder="${figEscapeAttribute(placeholder)}" value="${figEscapeAttribute(currentValue)}"></fig-input-text>
|
|
@@ -10932,9 +12196,6 @@ class FigComboInput extends HTMLElement {
|
|
|
10932
12196
|
if (!this.#customDropdown.hasAttribute("label")) {
|
|
10933
12197
|
this.#customDropdown.setAttribute("label", dropdownLabel);
|
|
10934
12198
|
}
|
|
10935
|
-
if (experimental) {
|
|
10936
|
-
this.#customDropdown.setAttribute("experimental", experimental);
|
|
10937
|
-
}
|
|
10938
12199
|
this.#button.append(this.#customDropdown);
|
|
10939
12200
|
}
|
|
10940
12201
|
|
|
@@ -11082,13 +12343,6 @@ class FigComboInput extends HTMLElement {
|
|
|
11082
12343
|
case "disabled":
|
|
11083
12344
|
this.#applyDisabled(newValue !== null && newValue !== "false");
|
|
11084
12345
|
break;
|
|
11085
|
-
case "experimental":
|
|
11086
|
-
if (this.#dropdown) {
|
|
11087
|
-
if (newValue) this.#dropdown.setAttribute("experimental", newValue);
|
|
11088
|
-
else if (!this.#usesCustomDropdown)
|
|
11089
|
-
this.#dropdown.removeAttribute("experimental");
|
|
11090
|
-
}
|
|
11091
|
-
break;
|
|
11092
12346
|
case "aria-label":
|
|
11093
12347
|
case "aria-labelledby":
|
|
11094
12348
|
case "aria-describedby":
|
|
@@ -11099,7 +12353,7 @@ class FigComboInput extends HTMLElement {
|
|
|
11099
12353
|
}
|
|
11100
12354
|
}
|
|
11101
12355
|
}
|
|
11102
|
-
|
|
12356
|
+
figDefineElement("fig-combo-input", FigComboInput);
|
|
11103
12357
|
|
|
11104
12358
|
/* Swatch */
|
|
11105
12359
|
/**
|
|
@@ -11315,7 +12569,7 @@ class FigSwatch extends HTMLElement {
|
|
|
11315
12569
|
}
|
|
11316
12570
|
}
|
|
11317
12571
|
}
|
|
11318
|
-
|
|
12572
|
+
figDefineElement("fig-swatch", FigSwatch);
|
|
11319
12573
|
|
|
11320
12574
|
/* Media */
|
|
11321
12575
|
/**
|
|
@@ -12071,21 +13325,21 @@ class FigMedia extends HTMLElement {
|
|
|
12071
13325
|
}
|
|
12072
13326
|
}
|
|
12073
13327
|
|
|
12074
|
-
|
|
13328
|
+
figDefineElement("fig-media", FigMedia);
|
|
12075
13329
|
|
|
12076
13330
|
class FigImage extends FigMedia {
|
|
12077
13331
|
get mediaKind() {
|
|
12078
13332
|
return "image";
|
|
12079
13333
|
}
|
|
12080
13334
|
}
|
|
12081
|
-
|
|
13335
|
+
figDefineElement("fig-image", FigImage);
|
|
12082
13336
|
|
|
12083
13337
|
class FigVideo extends FigMedia {
|
|
12084
13338
|
get mediaKind() {
|
|
12085
13339
|
return "video";
|
|
12086
13340
|
}
|
|
12087
13341
|
}
|
|
12088
|
-
|
|
13342
|
+
figDefineElement("fig-video", FigVideo);
|
|
12089
13343
|
|
|
12090
13344
|
/**
|
|
12091
13345
|
* <fig-card> — Media card with optional link, selection chrome, and truncated label.
|
|
@@ -12358,7 +13612,7 @@ class FigCard extends HTMLElement {
|
|
|
12358
13612
|
}
|
|
12359
13613
|
}
|
|
12360
13614
|
}
|
|
12361
|
-
|
|
13615
|
+
figDefineElement("fig-card", FigCard);
|
|
12362
13616
|
|
|
12363
13617
|
/**
|
|
12364
13618
|
* <fig-media-controls> — Standalone playback controls UI.
|
|
@@ -12601,7 +13855,7 @@ class FigMediaControls extends HTMLElement {
|
|
|
12601
13855
|
this.toggle();
|
|
12602
13856
|
}
|
|
12603
13857
|
}
|
|
12604
|
-
|
|
13858
|
+
figDefineElement("fig-media-controls", FigMediaControls);
|
|
12605
13859
|
|
|
12606
13860
|
/* File Upload Input */
|
|
12607
13861
|
class FigInputFile extends HTMLElement {
|
|
@@ -12865,7 +14119,7 @@ class FigInputFile extends HTMLElement {
|
|
|
12865
14119
|
}
|
|
12866
14120
|
}
|
|
12867
14121
|
}
|
|
12868
|
-
|
|
14122
|
+
figDefineElement("fig-input-file", FigInputFile);
|
|
12869
14123
|
|
|
12870
14124
|
/**
|
|
12871
14125
|
* A bezier / spring easing curve editor with draggable control points.
|
|
@@ -12886,9 +14140,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
12886
14140
|
#line2 = null;
|
|
12887
14141
|
#handle1 = null;
|
|
12888
14142
|
#handle2 = null;
|
|
12889
|
-
#
|
|
12890
|
-
#bezierEndpointEnd = null;
|
|
12891
|
-
#dropdown = null;
|
|
14143
|
+
#select = null;
|
|
12892
14144
|
#valueInput = null;
|
|
12893
14145
|
#presetName = null;
|
|
12894
14146
|
#targetLine = null;
|
|
@@ -12896,10 +14148,10 @@ class FigEasingCurve extends HTMLElement {
|
|
|
12896
14148
|
#drawWidth = 200;
|
|
12897
14149
|
#drawHeight = 200;
|
|
12898
14150
|
#bounds = null;
|
|
12899
|
-
#
|
|
14151
|
+
#boundaryTop = null;
|
|
14152
|
+
#boundaryBottom = null;
|
|
12900
14153
|
#resizeObserver = null;
|
|
12901
14154
|
#bezierHandleRadius = 5;
|
|
12902
|
-
#bezierEndpointRadius = 2;
|
|
12903
14155
|
#durationBarWidth = 10;
|
|
12904
14156
|
#durationBarHeight = 10;
|
|
12905
14157
|
#durationBarRadius = 3;
|
|
@@ -13017,7 +14269,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13017
14269
|
this.#render();
|
|
13018
14270
|
} else {
|
|
13019
14271
|
if (this.#svg) this.#updatePaths();
|
|
13020
|
-
this.#
|
|
14272
|
+
this.#syncSelect();
|
|
13021
14273
|
this.#syncValueInput();
|
|
13022
14274
|
}
|
|
13023
14275
|
}
|
|
@@ -13240,14 +14492,15 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13240
14492
|
.replace(/>/g, ">");
|
|
13241
14493
|
}
|
|
13242
14494
|
|
|
13243
|
-
#
|
|
14495
|
+
#getSelectHTML() {
|
|
13244
14496
|
let optionsHTML = "";
|
|
13245
14497
|
let currentGroup = undefined;
|
|
13246
14498
|
for (const p of FigEasingCurve.PRESETS) {
|
|
13247
14499
|
if (!this.#isEditEnabled() && !p.value && !p.spring) continue;
|
|
13248
14500
|
if (p.group !== currentGroup) {
|
|
13249
|
-
if (
|
|
13250
|
-
|
|
14501
|
+
if (p.group) {
|
|
14502
|
+
optionsHTML += `<fig-menu-separator label="${FigEasingCurve.#escapeAttribute(p.group)}"></fig-menu-separator>`;
|
|
14503
|
+
}
|
|
13251
14504
|
currentGroup = p.group;
|
|
13252
14505
|
}
|
|
13253
14506
|
let icon;
|
|
@@ -13263,40 +14516,37 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13263
14516
|
];
|
|
13264
14517
|
icon = FigEasingCurve.curveIcon(...v);
|
|
13265
14518
|
}
|
|
13266
|
-
const
|
|
13267
|
-
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>`;
|
|
13268
14521
|
}
|
|
13269
|
-
|
|
13270
|
-
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>`;
|
|
13271
14524
|
}
|
|
13272
14525
|
|
|
13273
14526
|
#getInnerHTML() {
|
|
13274
14527
|
const size = 200;
|
|
13275
|
-
const
|
|
13276
|
-
if (!this.#isEditEnabled()) return
|
|
14528
|
+
const select = this.#getSelectHTML();
|
|
14529
|
+
if (!this.#isEditEnabled()) return select;
|
|
13277
14530
|
const valueInput = `<fig-input-text class="fig-easing-curve-value-input" value="${FigEasingCurve.#escapeAttribute(this.value)}" full></fig-input-text>`;
|
|
13278
14531
|
|
|
13279
14532
|
if (this.#mode === "spring") {
|
|
13280
14533
|
const targetY = 40;
|
|
13281
|
-
|
|
13282
|
-
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">
|
|
13283
14535
|
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
13284
14536
|
<line class="fig-easing-curve-target" x1="0" y1="${targetY}" x2="${size}" y2="${targetY}"/>
|
|
13285
|
-
<line class="fig-easing-curve-diagonal" x1="0" y1="${startY}" x2="0" y2="${startY}"/>
|
|
13286
14537
|
<path class="fig-easing-curve-path"/>
|
|
13287
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>
|
|
13288
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>
|
|
13289
14540
|
</svg></div>${valueInput}`;
|
|
13290
14541
|
}
|
|
13291
14542
|
|
|
13292
|
-
return `${
|
|
14543
|
+
return `${select}<div class="fig-easing-curve-svg-container"><svg viewBox="0 0 ${size} ${size}" class="fig-easing-curve-svg">
|
|
13293
14544
|
<rect class="fig-easing-curve-bounds" x="0" y="0" width="${size}" height="${size}"/>
|
|
13294
|
-
<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"/>
|
|
13295
14548
|
<line class="fig-easing-curve-arm" data-arm="1"/>
|
|
13296
14549
|
<line class="fig-easing-curve-arm" data-arm="2"/>
|
|
13297
|
-
<path class="fig-easing-curve-path"/>
|
|
13298
|
-
<circle class="fig-easing-curve-endpoint" data-endpoint="start" r="${this.#bezierEndpointRadius}"/>
|
|
13299
|
-
<circle class="fig-easing-curve-endpoint" data-endpoint="end" r="${this.#bezierEndpointRadius}"/>
|
|
13300
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>
|
|
13301
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>
|
|
13302
14552
|
</svg></div>${valueInput}`;
|
|
@@ -13310,10 +14560,6 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13310
14560
|
}
|
|
13311
14561
|
|
|
13312
14562
|
#syncMetricsFromCSS() {
|
|
13313
|
-
this.#bezierEndpointRadius = this.#readCssNumber(
|
|
13314
|
-
"--easing-bezier-endpoint-radius",
|
|
13315
|
-
this.#bezierEndpointRadius,
|
|
13316
|
-
);
|
|
13317
14563
|
this.#durationBarRadius = this.#readCssNumber(
|
|
13318
14564
|
"--easing-duration-bar-radius",
|
|
13319
14565
|
this.#durationBarRadius,
|
|
@@ -13331,13 +14577,12 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13331
14577
|
this.#handle2 =
|
|
13332
14578
|
this.querySelector('[data-handle="2"]') ||
|
|
13333
14579
|
this.querySelector('[data-handle="duration"]');
|
|
13334
|
-
this.#
|
|
13335
|
-
this.#bezierEndpointEnd = this.querySelector('[data-endpoint="end"]');
|
|
13336
|
-
this.#dropdown = this.querySelector(".fig-easing-curve-dropdown");
|
|
14580
|
+
this.#select = this.querySelector(".fig-easing-curve-select");
|
|
13337
14581
|
this.#valueInput = this.querySelector(".fig-easing-curve-value-input");
|
|
13338
14582
|
this.#targetLine = this.querySelector(".fig-easing-curve-target");
|
|
13339
14583
|
this.#bounds = this.querySelector(".fig-easing-curve-bounds");
|
|
13340
|
-
this.#
|
|
14584
|
+
this.#boundaryTop = this.querySelector('[data-boundary="top"]');
|
|
14585
|
+
this.#boundaryBottom = this.querySelector('[data-boundary="bottom"]');
|
|
13341
14586
|
}
|
|
13342
14587
|
|
|
13343
14588
|
#syncHandleSizes() {
|
|
@@ -13386,12 +14631,37 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13386
14631
|
|
|
13387
14632
|
// --- Coordinate helpers ---
|
|
13388
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
|
+
|
|
13389
14651
|
#toSVG(nx, ny) {
|
|
13390
|
-
|
|
14652
|
+
const { maxVal, range, pad, draw } = this.#bezierDomain();
|
|
14653
|
+
return {
|
|
14654
|
+
x: nx * this.#drawWidth,
|
|
14655
|
+
y: pad + ((maxVal - ny) / range) * draw,
|
|
14656
|
+
};
|
|
13391
14657
|
}
|
|
13392
14658
|
|
|
13393
14659
|
#fromSVG(sx, sy) {
|
|
13394
|
-
|
|
14660
|
+
const { maxVal, range, pad, draw } = this.#bezierDomain();
|
|
14661
|
+
return {
|
|
14662
|
+
x: sx / this.#drawWidth,
|
|
14663
|
+
y: maxVal - ((sy - pad) / draw) * range,
|
|
14664
|
+
};
|
|
13395
14665
|
}
|
|
13396
14666
|
|
|
13397
14667
|
#springScale = { minVal: 0, maxVal: 1.2, totalTime: 1 };
|
|
@@ -13436,17 +14706,20 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13436
14706
|
this.#bounds.setAttribute("width", this.#drawWidth);
|
|
13437
14707
|
this.#bounds.setAttribute("height", this.#drawHeight);
|
|
13438
14708
|
}
|
|
13439
|
-
if (this.#diagonal) {
|
|
13440
|
-
this.#diagonal.setAttribute("x1", "0");
|
|
13441
|
-
this.#diagonal.setAttribute("y1", this.#drawHeight);
|
|
13442
|
-
this.#diagonal.setAttribute("x2", this.#drawWidth);
|
|
13443
|
-
this.#diagonal.setAttribute("y2", "0");
|
|
13444
|
-
}
|
|
13445
|
-
|
|
13446
14709
|
const p0 = this.#toSVG(0, 0);
|
|
13447
14710
|
const p1 = this.#toSVG(this.#cp1.x, this.#cp1.y);
|
|
13448
14711
|
const p2 = this.#toSVG(this.#cp2.x, this.#cp2.y);
|
|
13449
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
|
+
}
|
|
13450
14723
|
|
|
13451
14724
|
this.#curve.setAttribute(
|
|
13452
14725
|
"d",
|
|
@@ -13465,14 +14738,6 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13465
14738
|
this.#handle1.setAttribute("y", p1.y - hr);
|
|
13466
14739
|
this.#handle2.setAttribute("x", p2.x - hr);
|
|
13467
14740
|
this.#handle2.setAttribute("y", p2.y - hr);
|
|
13468
|
-
if (this.#bezierEndpointStart) {
|
|
13469
|
-
this.#bezierEndpointStart.setAttribute("cx", p0.x);
|
|
13470
|
-
this.#bezierEndpointStart.setAttribute("cy", p0.y);
|
|
13471
|
-
}
|
|
13472
|
-
if (this.#bezierEndpointEnd) {
|
|
13473
|
-
this.#bezierEndpointEnd.setAttribute("cx", p3.x);
|
|
13474
|
-
this.#bezierEndpointEnd.setAttribute("cy", p3.y);
|
|
13475
|
-
}
|
|
13476
14741
|
this.#syncBezierHandleTabOrder();
|
|
13477
14742
|
}
|
|
13478
14743
|
|
|
@@ -13565,11 +14830,11 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13565
14830
|
return peak;
|
|
13566
14831
|
}
|
|
13567
14832
|
|
|
13568
|
-
// ---
|
|
14833
|
+
// --- Select ---
|
|
13569
14834
|
|
|
13570
|
-
#
|
|
13571
|
-
if (!this.#
|
|
13572
|
-
this.#
|
|
14835
|
+
#syncSelect() {
|
|
14836
|
+
if (!this.#select) return;
|
|
14837
|
+
this.#select.value = this.#presetName;
|
|
13573
14838
|
this.#refreshCustomPresetIcons();
|
|
13574
14839
|
}
|
|
13575
14840
|
|
|
@@ -13608,23 +14873,24 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13608
14873
|
this.#render();
|
|
13609
14874
|
} else {
|
|
13610
14875
|
this.#updatePaths();
|
|
13611
|
-
this.#
|
|
14876
|
+
this.#syncSelect();
|
|
13612
14877
|
if (eventType === "change") this.#syncValueInput();
|
|
13613
14878
|
}
|
|
13614
14879
|
this.#emit(eventType);
|
|
13615
14880
|
}
|
|
13616
14881
|
|
|
13617
|
-
#setOptionIconByValue(
|
|
13618
|
-
if (!
|
|
13619
|
-
for (const option of
|
|
14882
|
+
#setOptionIconByValue(optionValue, icon) {
|
|
14883
|
+
if (!this.#select) return;
|
|
14884
|
+
for (const option of this.#select.querySelectorAll("fig-select-option")) {
|
|
13620
14885
|
if (option.value === optionValue) {
|
|
13621
|
-
option.
|
|
14886
|
+
const prepend = option.querySelector(':scope > [slot="prepend"]');
|
|
14887
|
+
if (prepend) prepend.innerHTML = icon;
|
|
13622
14888
|
}
|
|
13623
14889
|
}
|
|
13624
14890
|
}
|
|
13625
14891
|
|
|
13626
14892
|
#refreshCustomPresetIcons() {
|
|
13627
|
-
if (!this.#
|
|
14893
|
+
if (!this.#select) return;
|
|
13628
14894
|
if (!this.#isEditEnabled()) return;
|
|
13629
14895
|
const bezierIcon = FigEasingCurve.curveIcon(
|
|
13630
14896
|
this.#cp1.x,
|
|
@@ -13634,25 +14900,14 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13634
14900
|
);
|
|
13635
14901
|
const springIcon = FigEasingCurve.#springIcon(this.#spring);
|
|
13636
14902
|
|
|
13637
|
-
|
|
13638
|
-
this.#setOptionIconByValue(
|
|
13639
|
-
this.#setOptionIconByValue(this.#dropdown, "Custom spring", springIcon);
|
|
13640
|
-
this.#setOptionIconByValue(
|
|
13641
|
-
this.#dropdown.select,
|
|
13642
|
-
"Custom bezier",
|
|
13643
|
-
bezierIcon,
|
|
13644
|
-
);
|
|
13645
|
-
this.#setOptionIconByValue(
|
|
13646
|
-
this.#dropdown.select,
|
|
13647
|
-
"Custom spring",
|
|
13648
|
-
springIcon,
|
|
13649
|
-
);
|
|
14903
|
+
this.#setOptionIconByValue("Custom bezier", bezierIcon);
|
|
14904
|
+
this.#setOptionIconByValue("Custom spring", springIcon);
|
|
13650
14905
|
}
|
|
13651
14906
|
|
|
13652
14907
|
#syncAfterHandleInput(eventType) {
|
|
13653
14908
|
this.#updatePaths();
|
|
13654
14909
|
this.#presetName = this.#matchPreset();
|
|
13655
|
-
this.#
|
|
14910
|
+
this.#syncSelect();
|
|
13656
14911
|
this.#syncValueInput();
|
|
13657
14912
|
this.#emit(eventType);
|
|
13658
14913
|
}
|
|
@@ -13841,8 +15096,8 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13841
15096
|
}
|
|
13842
15097
|
}
|
|
13843
15098
|
|
|
13844
|
-
if (this.#
|
|
13845
|
-
this.#
|
|
15099
|
+
if (this.#select) {
|
|
15100
|
+
this.#select.addEventListener("change", (e) => {
|
|
13846
15101
|
const name = e.detail;
|
|
13847
15102
|
const preset = FigEasingCurve.PRESETS.find((p) => p.name === name);
|
|
13848
15103
|
if (!preset) return;
|
|
@@ -13915,11 +15170,30 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13915
15170
|
e.preventDefault();
|
|
13916
15171
|
this.#isDragging = handle;
|
|
13917
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));
|
|
13918
15188
|
|
|
13919
15189
|
const onMove = (e) => {
|
|
13920
15190
|
if (!this.#isDragging) return;
|
|
13921
|
-
const
|
|
13922
|
-
|
|
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
|
+
};
|
|
13923
15197
|
|
|
13924
15198
|
norm.x = Math.round(norm.x * 100) / 100;
|
|
13925
15199
|
norm.y = Math.round(norm.y * 100) / 100;
|
|
@@ -13934,7 +15208,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13934
15208
|
}
|
|
13935
15209
|
this.#updatePaths();
|
|
13936
15210
|
this.#presetName = this.#matchPreset();
|
|
13937
|
-
this.#
|
|
15211
|
+
this.#syncSelect();
|
|
13938
15212
|
this.#syncValueInput();
|
|
13939
15213
|
this.#emit("input");
|
|
13940
15214
|
};
|
|
@@ -13985,7 +15259,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
13985
15259
|
|
|
13986
15260
|
this.#updatePaths();
|
|
13987
15261
|
this.#presetName = this.#matchPreset();
|
|
13988
|
-
this.#
|
|
15262
|
+
this.#syncSelect();
|
|
13989
15263
|
this.#syncValueInput();
|
|
13990
15264
|
this.#emit("input");
|
|
13991
15265
|
};
|
|
@@ -14002,7 +15276,7 @@ class FigEasingCurve extends HTMLElement {
|
|
|
14002
15276
|
document.addEventListener("pointerup", onUp);
|
|
14003
15277
|
}
|
|
14004
15278
|
}
|
|
14005
|
-
|
|
15279
|
+
figDefineElement("fig-easing-curve", FigEasingCurve);
|
|
14006
15280
|
|
|
14007
15281
|
/**
|
|
14008
15282
|
* A 3D rotation control with an interactive cube preview.
|
|
@@ -14372,7 +15646,7 @@ class Fig3DRotate extends HTMLElement {
|
|
|
14372
15646
|
this.#container.addEventListener("lostpointercapture", onEnd);
|
|
14373
15647
|
}
|
|
14374
15648
|
}
|
|
14375
|
-
|
|
15649
|
+
figDefineElement("fig-3d-rotate", Fig3DRotate);
|
|
14376
15650
|
|
|
14377
15651
|
/**
|
|
14378
15652
|
* A transform-origin grid control with draggable handle.
|
|
@@ -14922,7 +16196,7 @@ class FigOriginGrid extends HTMLElement {
|
|
|
14922
16196
|
bindValueInput(this.#yInput, "y");
|
|
14923
16197
|
}
|
|
14924
16198
|
}
|
|
14925
|
-
|
|
16199
|
+
figDefineElement("fig-origin-grid", FigOriginGrid);
|
|
14926
16200
|
|
|
14927
16201
|
/**
|
|
14928
16202
|
* A custom joystick input element.
|
|
@@ -15347,7 +16621,7 @@ class FigInputJoystick extends HTMLElement {
|
|
|
15347
16621
|
}
|
|
15348
16622
|
}
|
|
15349
16623
|
|
|
15350
|
-
|
|
16624
|
+
figDefineElement("fig-joystick", FigInputJoystick);
|
|
15351
16625
|
|
|
15352
16626
|
|
|
15353
16627
|
// FigInputAngle moved to fig-lab.js
|
|
@@ -15404,7 +16678,7 @@ class FigShimmer extends HTMLElement {
|
|
|
15404
16678
|
}
|
|
15405
16679
|
}
|
|
15406
16680
|
}
|
|
15407
|
-
|
|
16681
|
+
figDefineElement("fig-shimmer", FigShimmer);
|
|
15408
16682
|
|
|
15409
16683
|
// FigSkeleton
|
|
15410
16684
|
class FigSkeleton extends FigShimmer {
|
|
@@ -15414,7 +16688,7 @@ class FigSkeleton extends FigShimmer {
|
|
|
15414
16688
|
this.setAttribute("inert", "");
|
|
15415
16689
|
}
|
|
15416
16690
|
}
|
|
15417
|
-
|
|
16691
|
+
figDefineElement("fig-skeleton", FigSkeleton);
|
|
15418
16692
|
|
|
15419
16693
|
// FigGroup
|
|
15420
16694
|
class FigGroup extends HTMLElement {
|
|
@@ -15560,7 +16834,7 @@ class FigGroup extends HTMLElement {
|
|
|
15560
16834
|
}
|
|
15561
16835
|
}
|
|
15562
16836
|
}
|
|
15563
|
-
|
|
16837
|
+
figDefineElement("fig-group", FigGroup);
|
|
15564
16838
|
|
|
15565
16839
|
/**
|
|
15566
16840
|
* A presentational header element used inside fig-dialog, fig-group, and other containers.
|
|
@@ -15570,7 +16844,7 @@ customElements.define("fig-group", FigGroup);
|
|
|
15570
16844
|
* @attr {boolean} dialog-header - Marks this as a dialog header (auto-generated by fig-dialog)
|
|
15571
16845
|
*/
|
|
15572
16846
|
class FigHeader extends HTMLElement {}
|
|
15573
|
-
|
|
16847
|
+
figDefineElement("fig-header", FigHeader);
|
|
15574
16848
|
|
|
15575
16849
|
/**
|
|
15576
16850
|
* fig-footer
|
|
@@ -15579,7 +16853,7 @@ customElements.define("fig-header", FigHeader);
|
|
|
15579
16853
|
* @attr {boolean} sticky - Pins the footer to the bottom of its scroll container
|
|
15580
16854
|
*/
|
|
15581
16855
|
class FigFooter extends HTMLElement {}
|
|
15582
|
-
|
|
16856
|
+
figDefineElement("fig-footer", FigFooter);
|
|
15583
16857
|
|
|
15584
16858
|
/* Presentational elements (CSS-only, no behavior) */
|
|
15585
16859
|
class FigSpinner extends HTMLElement {
|
|
@@ -15590,7 +16864,7 @@ class FigSpinner extends HTMLElement {
|
|
|
15590
16864
|
}
|
|
15591
16865
|
}
|
|
15592
16866
|
}
|
|
15593
|
-
|
|
16867
|
+
figDefineElement("fig-spinner", FigSpinner);
|
|
15594
16868
|
|
|
15595
16869
|
/**
|
|
15596
16870
|
* A styled visual preview layer for arbitrary content such as images, canvas,
|
|
@@ -15620,7 +16894,7 @@ class FigPreview extends HTMLElement {
|
|
|
15620
16894
|
}
|
|
15621
16895
|
}
|
|
15622
16896
|
}
|
|
15623
|
-
|
|
16897
|
+
figDefineElement("fig-preview", FigPreview);
|
|
15624
16898
|
|
|
15625
16899
|
/**
|
|
15626
16900
|
* Compact swatch previewing gradient color-space interpolation.
|
|
@@ -15928,7 +17202,7 @@ class FigInterpolationSwatch extends HTMLElement {
|
|
|
15928
17202
|
this.#fillEl.style.background = this.#previewBackground();
|
|
15929
17203
|
}
|
|
15930
17204
|
}
|
|
15931
|
-
|
|
17205
|
+
figDefineElement("fig-interpolation-swatch", FigInterpolationSwatch);
|
|
15932
17206
|
|
|
15933
17207
|
/** @type {Record<string, string | { medium: string, small: string }>} */
|
|
15934
17208
|
const FIG_ICON_TOKENS = {
|
|
@@ -16015,19 +17289,19 @@ class FigIcon extends HTMLElement {
|
|
|
16015
17289
|
}
|
|
16016
17290
|
}
|
|
16017
17291
|
}
|
|
16018
|
-
|
|
17292
|
+
figDefineElement("fig-icon", FigIcon);
|
|
16019
17293
|
|
|
16020
17294
|
class FigContent extends HTMLElement {}
|
|
16021
|
-
|
|
17295
|
+
figDefineElement("fig-content", FigContent);
|
|
16022
17296
|
|
|
16023
17297
|
class FigTabContent extends HTMLElement {}
|
|
16024
|
-
|
|
17298
|
+
figDefineElement("fig-tab-content", FigTabContent);
|
|
16025
17299
|
|
|
16026
17300
|
class FigButtonCombo extends HTMLElement {}
|
|
16027
|
-
|
|
17301
|
+
figDefineElement("fig-button-combo", FigButtonCombo);
|
|
16028
17302
|
|
|
16029
17303
|
class FigInputCombo extends HTMLElement {}
|
|
16030
|
-
|
|
17304
|
+
figDefineElement("fig-input-combo", FigInputCombo);
|
|
16031
17305
|
|
|
16032
17306
|
|
|
16033
17307
|
|
|
@@ -16405,7 +17679,7 @@ class FigColorTip extends HTMLElement {
|
|
|
16405
17679
|
this.toggleAttribute("disabled", Boolean(value));
|
|
16406
17680
|
}
|
|
16407
17681
|
}
|
|
16408
|
-
|
|
17682
|
+
figDefineElement("fig-color-tip", FigColorTip);
|
|
16409
17683
|
|
|
16410
17684
|
/* Choice */
|
|
16411
17685
|
/**
|
|
@@ -16454,7 +17728,7 @@ class FigChoice extends HTMLElement {
|
|
|
16454
17728
|
}
|
|
16455
17729
|
}
|
|
16456
17730
|
}
|
|
16457
|
-
|
|
17731
|
+
figDefineElement("fig-choice", FigChoice);
|
|
16458
17732
|
|
|
16459
17733
|
/* Chooser */
|
|
16460
17734
|
/**
|
|
@@ -16570,6 +17844,7 @@ class FigChooser extends HTMLElement {
|
|
|
16570
17844
|
this.#setupDrag();
|
|
16571
17845
|
this.#startObserver();
|
|
16572
17846
|
this.#startResizeObserver();
|
|
17847
|
+
this.#syncDisabledChoices();
|
|
16573
17848
|
|
|
16574
17849
|
figNextFrame(this, () => {
|
|
16575
17850
|
this.#syncSelection();
|
|
@@ -16623,17 +17898,7 @@ class FigChooser extends HTMLElement {
|
|
|
16623
17898
|
this.#selectByValue(newValue);
|
|
16624
17899
|
}
|
|
16625
17900
|
if (name === "disabled") {
|
|
16626
|
-
|
|
16627
|
-
const choices = this.choices;
|
|
16628
|
-
for (const choice of choices) {
|
|
16629
|
-
if (isDisabled) {
|
|
16630
|
-
choice.setAttribute("aria-disabled", "true");
|
|
16631
|
-
choice.setAttribute("tabindex", "-1");
|
|
16632
|
-
} else {
|
|
16633
|
-
choice.removeAttribute("aria-disabled");
|
|
16634
|
-
choice.setAttribute("tabindex", "0");
|
|
16635
|
-
}
|
|
16636
|
-
}
|
|
17901
|
+
this.#syncDisabledChoices();
|
|
16637
17902
|
}
|
|
16638
17903
|
if (name === "choice-element") {
|
|
16639
17904
|
requestAnimationFrame(() => this.#syncSelection());
|
|
@@ -16685,6 +17950,23 @@ class FigChooser extends HTMLElement {
|
|
|
16685
17950
|
this.selectedChoice = choices[0];
|
|
16686
17951
|
}
|
|
16687
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
|
+
|
|
16688
17970
|
#selectByValue(value) {
|
|
16689
17971
|
const choices = this.choices;
|
|
16690
17972
|
for (const choice of choices) {
|
|
@@ -17047,6 +18329,7 @@ class FigChooser extends HTMLElement {
|
|
|
17047
18329
|
if (this.#isUnwrapping) return;
|
|
17048
18330
|
this.#removeLegacyScroller();
|
|
17049
18331
|
this.#applyOverflowMode();
|
|
18332
|
+
this.#syncDisabledChoices();
|
|
17050
18333
|
const choices = this.choices;
|
|
17051
18334
|
if (this.#selectedChoice && !choices.includes(this.#selectedChoice)) {
|
|
17052
18335
|
this.#selectedChoice = null;
|
|
@@ -17059,7 +18342,7 @@ class FigChooser extends HTMLElement {
|
|
|
17059
18342
|
this.#mutationObserver.observe(this, { childList: true, subtree: false });
|
|
17060
18343
|
}
|
|
17061
18344
|
}
|
|
17062
|
-
|
|
18345
|
+
figDefineElement("fig-chooser", FigChooser);
|
|
17063
18346
|
|
|
17064
18347
|
/* Handle */
|
|
17065
18348
|
class FigHandle extends HTMLElement {
|
|
@@ -17083,6 +18366,7 @@ class FigHandle extends HTMLElement {
|
|
|
17083
18366
|
#isDragging = false;
|
|
17084
18367
|
#didDrag = false;
|
|
17085
18368
|
#boundPointerDown = null;
|
|
18369
|
+
#activeDragCleanup = null;
|
|
17086
18370
|
#applyingValue = false;
|
|
17087
18371
|
#colorTip = null;
|
|
17088
18372
|
#directColorPicker = null;
|
|
@@ -17521,11 +18805,14 @@ class FigHandle extends HTMLElement {
|
|
|
17521
18805
|
}
|
|
17522
18806
|
|
|
17523
18807
|
#teardownDrag() {
|
|
18808
|
+
this.#activeDragCleanup?.();
|
|
18809
|
+
this.#activeDragCleanup = null;
|
|
17524
18810
|
if (this.#boundPointerDown) {
|
|
17525
18811
|
this.removeEventListener("pointerdown", this.#boundPointerDown);
|
|
17526
18812
|
this.#boundPointerDown = null;
|
|
17527
18813
|
}
|
|
17528
18814
|
this.#isDragging = false;
|
|
18815
|
+
this.#didDrag = false;
|
|
17529
18816
|
}
|
|
17530
18817
|
|
|
17531
18818
|
#onPointerDown(e) {
|
|
@@ -17534,6 +18821,7 @@ class FigHandle extends HTMLElement {
|
|
|
17534
18821
|
const container = this.#getContainer();
|
|
17535
18822
|
if (!container) return;
|
|
17536
18823
|
|
|
18824
|
+
this.#activeDragCleanup?.();
|
|
17537
18825
|
this.#isDragging = true;
|
|
17538
18826
|
const axes = this.#axes;
|
|
17539
18827
|
let lastRect = null;
|
|
@@ -17611,11 +18899,7 @@ class FigHandle extends HTMLElement {
|
|
|
17611
18899
|
};
|
|
17612
18900
|
|
|
17613
18901
|
const onUp = (e) => {
|
|
17614
|
-
|
|
17615
|
-
this.style.cursor = "";
|
|
17616
|
-
this.classList.remove("dragging");
|
|
17617
|
-
window.removeEventListener("pointermove", onMove);
|
|
17618
|
-
window.removeEventListener("pointerup", onUp);
|
|
18902
|
+
cleanup();
|
|
17619
18903
|
if (this.#didDrag) {
|
|
17620
18904
|
clampAndApply(e.clientX, e.clientY, e.shiftKey);
|
|
17621
18905
|
this.#syncValueAttribute();
|
|
@@ -17640,6 +18924,17 @@ class FigHandle extends HTMLElement {
|
|
|
17640
18924
|
this.#didDrag = false;
|
|
17641
18925
|
};
|
|
17642
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;
|
|
17643
18938
|
window.addEventListener("pointermove", onMove);
|
|
17644
18939
|
window.addEventListener("pointerup", onUp);
|
|
17645
18940
|
}
|
|
@@ -17981,7 +19276,7 @@ class FigHandle extends HTMLElement {
|
|
|
17981
19276
|
return { x, y, px, py };
|
|
17982
19277
|
}
|
|
17983
19278
|
}
|
|
17984
|
-
|
|
19279
|
+
figDefineElement("fig-handle", FigHandle);
|
|
17985
19280
|
|
|
17986
19281
|
// ─── Menu ────────────────────────────────────────────────────────────────────
|
|
17987
19282
|
|
|
@@ -18029,7 +19324,7 @@ class FigMenuItem extends HTMLElement {
|
|
|
18029
19324
|
}
|
|
18030
19325
|
}
|
|
18031
19326
|
}
|
|
18032
|
-
|
|
19327
|
+
figDefineElement("fig-menu-item", FigMenuItem);
|
|
18033
19328
|
|
|
18034
19329
|
/**
|
|
18035
19330
|
* Visual divider between menu item groups.
|
|
@@ -18067,7 +19362,7 @@ class FigMenuSeparator extends HTMLElement {
|
|
|
18067
19362
|
else this.removeAttribute("aria-label");
|
|
18068
19363
|
}
|
|
18069
19364
|
}
|
|
18070
|
-
|
|
19365
|
+
figDefineElement("fig-menu-separator", FigMenuSeparator);
|
|
18071
19366
|
|
|
18072
19367
|
class FigMenu extends HTMLElement {
|
|
18073
19368
|
#popup = null;
|
|
@@ -18108,6 +19403,7 @@ class FigMenu extends HTMLElement {
|
|
|
18108
19403
|
|
|
18109
19404
|
set open(val) {
|
|
18110
19405
|
if (val) {
|
|
19406
|
+
if (this.#isDisabled()) return;
|
|
18111
19407
|
this.setAttribute("open", "");
|
|
18112
19408
|
} else {
|
|
18113
19409
|
this.removeAttribute("open");
|
|
@@ -18154,19 +19450,17 @@ class FigMenu extends HTMLElement {
|
|
|
18154
19450
|
if (newValue === null || newValue === "false") {
|
|
18155
19451
|
this.#closeMenu();
|
|
18156
19452
|
} else {
|
|
19453
|
+
if (this.#isDisabled()) {
|
|
19454
|
+
this.removeAttribute("open");
|
|
19455
|
+
return;
|
|
19456
|
+
}
|
|
18157
19457
|
this.#openMenu();
|
|
18158
19458
|
}
|
|
18159
19459
|
return;
|
|
18160
19460
|
}
|
|
18161
19461
|
|
|
18162
19462
|
if (name === "disabled") {
|
|
18163
|
-
|
|
18164
|
-
if (newValue !== null && newValue !== "false") {
|
|
18165
|
-
this.#trigger.setAttribute("disabled", "");
|
|
18166
|
-
} else {
|
|
18167
|
-
this.#trigger.removeAttribute("disabled");
|
|
18168
|
-
}
|
|
18169
|
-
}
|
|
19463
|
+
this.#syncDisabled();
|
|
18170
19464
|
return;
|
|
18171
19465
|
}
|
|
18172
19466
|
|
|
@@ -18308,8 +19602,12 @@ class FigMenu extends HTMLElement {
|
|
|
18308
19602
|
}
|
|
18309
19603
|
|
|
18310
19604
|
#syncDisabled() {
|
|
19605
|
+
const disabled = this.#isDisabled();
|
|
19606
|
+
if (disabled) {
|
|
19607
|
+
if (this.open) this.removeAttribute("open");
|
|
19608
|
+
else this.#closeMenu();
|
|
19609
|
+
}
|
|
18311
19610
|
if (!this.#trigger) return;
|
|
18312
|
-
const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
|
|
18313
19611
|
if (disabled) {
|
|
18314
19612
|
this.#trigger.setAttribute("disabled", "");
|
|
18315
19613
|
this.#trigger.setAttribute("aria-disabled", "true");
|
|
@@ -18320,9 +19618,15 @@ class FigMenu extends HTMLElement {
|
|
|
18320
19618
|
}
|
|
18321
19619
|
}
|
|
18322
19620
|
|
|
19621
|
+
#isDisabled() {
|
|
19622
|
+
return (
|
|
19623
|
+
this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false"
|
|
19624
|
+
);
|
|
19625
|
+
}
|
|
19626
|
+
|
|
18323
19627
|
#handleTriggerClick(e) {
|
|
18324
19628
|
if (this.#usesContextMenuTrigger()) return;
|
|
18325
|
-
if (this
|
|
19629
|
+
if (this.#isDisabled()) return;
|
|
18326
19630
|
e.stopPropagation();
|
|
18327
19631
|
const popupShowing = this.#popup?.matches?.(":open") ?? false;
|
|
18328
19632
|
if (this.open && !popupShowing) {
|
|
@@ -18341,7 +19645,7 @@ class FigMenu extends HTMLElement {
|
|
|
18341
19645
|
|
|
18342
19646
|
#handleTriggerContextMenu(e) {
|
|
18343
19647
|
if (!this.#usesContextMenuTrigger()) return;
|
|
18344
|
-
if (this
|
|
19648
|
+
if (this.#isDisabled()) return;
|
|
18345
19649
|
e.preventDefault();
|
|
18346
19650
|
e.stopPropagation();
|
|
18347
19651
|
this.#showAtAfterPointerRelease(e.clientX, e.clientY);
|
|
@@ -18375,6 +19679,7 @@ class FigMenu extends HTMLElement {
|
|
|
18375
19679
|
}
|
|
18376
19680
|
|
|
18377
19681
|
#handlePopupClick(e) {
|
|
19682
|
+
if (this.#isDisabled()) return;
|
|
18378
19683
|
const item = e.target.closest("fig-menu-item");
|
|
18379
19684
|
if (!item) return;
|
|
18380
19685
|
if (item.hasAttribute("disabled") && item.getAttribute("disabled") !== "false") return;
|
|
@@ -18383,6 +19688,7 @@ class FigMenu extends HTMLElement {
|
|
|
18383
19688
|
}
|
|
18384
19689
|
|
|
18385
19690
|
#handleMenuKeydown(e) {
|
|
19691
|
+
if (this.#isDisabled()) return;
|
|
18386
19692
|
if (e.currentTarget === document && e.key !== "Escape") return;
|
|
18387
19693
|
if (e.currentTarget === this && this.#popup?.contains(e.target)) return;
|
|
18388
19694
|
if (!this.open || !this.#popup?.matches?.(":open")) {
|
|
@@ -18468,6 +19774,7 @@ class FigMenu extends HTMLElement {
|
|
|
18468
19774
|
}
|
|
18469
19775
|
|
|
18470
19776
|
showAt(x, y) {
|
|
19777
|
+
if (this.#isDisabled()) return;
|
|
18471
19778
|
this.#virtualAnchor = {
|
|
18472
19779
|
getBoundingClientRect: () => ({
|
|
18473
19780
|
width: 0,
|
|
@@ -18485,12 +19792,16 @@ class FigMenu extends HTMLElement {
|
|
|
18485
19792
|
}
|
|
18486
19793
|
if (this.open) this.open = false;
|
|
18487
19794
|
requestAnimationFrame(() => {
|
|
19795
|
+
if (this.#isDisabled()) return;
|
|
18488
19796
|
this.open = true;
|
|
18489
19797
|
});
|
|
18490
19798
|
}
|
|
18491
19799
|
|
|
18492
19800
|
#openMenu() {
|
|
18493
|
-
if (!this.#popup)
|
|
19801
|
+
if (!this.#popup || this.#isDisabled()) {
|
|
19802
|
+
if (this.hasAttribute("open")) this.removeAttribute("open");
|
|
19803
|
+
return;
|
|
19804
|
+
}
|
|
18494
19805
|
this.#popup.open = true;
|
|
18495
19806
|
document.addEventListener("keydown", this.#boundMenuKeydown, true);
|
|
18496
19807
|
if (this.#trigger) {
|
|
@@ -18510,4 +19821,4 @@ class FigMenu extends HTMLElement {
|
|
|
18510
19821
|
this.#trigger?.setAttribute("aria-expanded", "false");
|
|
18511
19822
|
}
|
|
18512
19823
|
}
|
|
18513
|
-
|
|
19824
|
+
figDefineElement("fig-menu", FigMenu);
|