@rogieking/figui3 3.2.0 → 3.4.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.
Files changed (4) hide show
  1. package/components.css +280 -35
  2. package/dist/fig.js +38 -38
  3. package/fig.js +628 -65
  4. package/package.json +1 -1
package/fig.js CHANGED
@@ -2253,15 +2253,15 @@ class FigTabs extends HTMLElement {
2253
2253
  this.setAttribute("role", "tablist");
2254
2254
  this.addEventListener("click", this.handleClick.bind(this));
2255
2255
  this.addEventListener("keydown", this.#handleKeyDown.bind(this));
2256
- // Set initial selected tab based on value
2257
- const value = this.getAttribute("value");
2258
- if (value) {
2259
- this.#selectByValue(value);
2260
- }
2261
- // Apply disabled state
2262
- if (this.hasAttribute("disabled")) {
2263
- this.#applyDisabled(true);
2264
- }
2256
+ requestAnimationFrame(() => {
2257
+ const value = this.getAttribute("value");
2258
+ if (value) {
2259
+ this.#selectByValue(value);
2260
+ }
2261
+ if (this.hasAttribute("disabled")) {
2262
+ this.#applyDisabled(true);
2263
+ }
2264
+ });
2265
2265
  }
2266
2266
 
2267
2267
  #applyDisabled(disabled) {
@@ -2333,6 +2333,7 @@ class FigTabs extends HTMLElement {
2333
2333
  for (const tab of tabs) {
2334
2334
  if (tab.getAttribute("value") === value) {
2335
2335
  this.selectedTab = tab;
2336
+ tab.setAttribute("selected", "true");
2336
2337
  } else {
2337
2338
  tab.removeAttribute("selected");
2338
2339
  }
@@ -2353,7 +2354,6 @@ class FigTabs extends HTMLElement {
2353
2354
  }
2354
2355
 
2355
2356
  handleClick(event) {
2356
- // Ignore clicks when disabled
2357
2357
  if (this.hasAttribute("disabled")) return;
2358
2358
  const target = event.target;
2359
2359
  if (target.nodeName.toLowerCase() === "fig-tab") {
@@ -2361,6 +2361,7 @@ class FigTabs extends HTMLElement {
2361
2361
  for (const tab of tabs) {
2362
2362
  if (tab === target) {
2363
2363
  this.selectedTab = tab;
2364
+ tab.setAttribute("selected", "true");
2364
2365
  this.setAttribute("value", tab.getAttribute("value") || "");
2365
2366
  } else {
2366
2367
  tab.removeAttribute("selected");
@@ -3459,20 +3460,46 @@ class FigInputNumber extends HTMLElement {
3459
3460
  #unitPosition;
3460
3461
  #precision;
3461
3462
  #isInteracting = false;
3463
+ #stepperEl = null;
3464
+
3465
+ #syncSteppers(hasSteppers) {
3466
+ if (hasSteppers && !this.#stepperEl) {
3467
+ this.#stepperEl = document.createElement("span");
3468
+ this.#stepperEl.className = "fig-steppers";
3469
+ this.#stepperEl.innerHTML =
3470
+ `<button class="fig-stepper-up" tabindex="-1" aria-label="Increase"></button>` +
3471
+ `<button class="fig-stepper-down" tabindex="-1" aria-label="Decrease"></button>`;
3472
+ this.#stepperEl.addEventListener("pointerdown", (e) => {
3473
+ e.preventDefault();
3474
+ e.stopPropagation();
3475
+ const btn = e.target.closest("button");
3476
+ if (!btn || this.disabled) return;
3477
+ const dir = btn.classList.contains("fig-stepper-up") ? 1 : -1;
3478
+ this.#stepValue(dir);
3479
+ this.input.focus();
3480
+ });
3481
+ this.append(this.#stepperEl);
3482
+ } else if (!hasSteppers && this.#stepperEl) {
3483
+ this.#stepperEl.remove();
3484
+ this.#stepperEl = null;
3485
+ }
3486
+ }
3462
3487
 
3463
- #syncNativeNumberAttributes() {
3464
- if (!this.input || this.input.type !== "number") return;
3465
-
3466
- ["min", "max", "step"].forEach((name) => {
3467
- const attrValue = this.getAttribute(name);
3468
- if (attrValue === null || attrValue === "") {
3469
- this.input.removeAttribute(name);
3470
- this.input[name] = "";
3471
- return;
3472
- }
3473
- this.input.setAttribute(name, attrValue);
3474
- this.input[name] = attrValue;
3475
- });
3488
+ #stepValue(direction) {
3489
+ const step = this.step || 1;
3490
+ let numericValue = this.#getNumericValue(this.input.value);
3491
+ let value =
3492
+ (numericValue !== "" ? Number(numericValue) / (this.transform || 1) : 0) +
3493
+ step * direction;
3494
+ value = this.#sanitizeInput(value, false);
3495
+ this.value = value;
3496
+ this.input.value = this.#formatWithUnit(this.value);
3497
+ this.dispatchEvent(
3498
+ new CustomEvent("input", { detail: this.value, bubbles: true }),
3499
+ );
3500
+ this.dispatchEvent(
3501
+ new CustomEvent("change", { detail: this.value, bubbles: true }),
3502
+ );
3476
3503
  }
3477
3504
 
3478
3505
  constructor() {
@@ -3528,19 +3555,12 @@ class FigInputNumber extends HTMLElement {
3528
3555
  this.hasAttribute("steppers") &&
3529
3556
  this.getAttribute("steppers") !== "false";
3530
3557
 
3531
- // Use type="number" when steppers are enabled (for native spin buttons)
3532
- const inputType = hasSteppers ? "number" : "text";
3533
- const inputMode = hasSteppers ? "" : 'inputmode="decimal"';
3534
- const inputValue = hasSteppers
3535
- ? this.#transformNumber(this.value)
3536
- : this.#formatWithUnit(this.value);
3537
-
3538
3558
  let html = `<input
3539
- type="${inputType}"
3540
- ${inputMode}
3559
+ type="text"
3560
+ inputmode="decimal"
3541
3561
  ${this.name ? `name="${this.name}"` : ""}
3542
3562
  placeholder="${this.placeholder}"
3543
- value="${inputValue}" />`;
3563
+ value="${this.#formatWithUnit(this.value)}" />`;
3544
3564
 
3545
3565
  //child nodes hack
3546
3566
  requestAnimationFrame(() => {
@@ -3569,7 +3589,8 @@ class FigInputNumber extends HTMLElement {
3569
3589
  if (this.getAttribute("step")) {
3570
3590
  this.step = Number(this.getAttribute("step"));
3571
3591
  }
3572
- this.#syncNativeNumberAttributes();
3592
+
3593
+ this.#syncSteppers(hasSteppers);
3573
3594
 
3574
3595
  // Set disabled state if present
3575
3596
  if (this.hasAttribute("disabled")) {
@@ -3845,24 +3866,15 @@ class FigInputNumber extends HTMLElement {
3845
3866
  break;
3846
3867
  case "units":
3847
3868
  this.#units = newValue || "";
3848
- this.input.value =
3849
- this.input.type === "number"
3850
- ? this.#transformNumber(this.value)
3851
- : this.#formatWithUnit(this.value);
3869
+ this.input.value = this.#formatWithUnit(this.value);
3852
3870
  break;
3853
3871
  case "unit-position":
3854
3872
  this.#unitPosition = newValue || "suffix";
3855
- this.input.value =
3856
- this.input.type === "number"
3857
- ? this.#transformNumber(this.value)
3858
- : this.#formatWithUnit(this.value);
3873
+ this.input.value = this.#formatWithUnit(this.value);
3859
3874
  break;
3860
3875
  case "transform":
3861
3876
  this.transform = Number(newValue) || 1;
3862
- this.input.value =
3863
- this.input.type === "number"
3864
- ? this.#transformNumber(this.value)
3865
- : this.#formatWithUnit(this.value);
3877
+ this.input.value = this.#formatWithUnit(this.value);
3866
3878
  break;
3867
3879
  case "value":
3868
3880
  if (this.#isInteracting) break;
@@ -3872,41 +3884,25 @@ class FigInputNumber extends HTMLElement {
3872
3884
  value = this.#sanitizeInput(value, false);
3873
3885
  }
3874
3886
  this.value = value;
3875
- this.input.value =
3876
- this.input.type === "number"
3877
- ? this.#transformNumber(this.value)
3878
- : this.#formatWithUnit(this.value);
3887
+ this.input.value = this.#formatWithUnit(this.value);
3879
3888
  break;
3880
3889
  case "min":
3881
3890
  case "max":
3882
3891
  case "step":
3883
3892
  if (newValue === null || newValue === "") {
3884
3893
  this[name] = undefined;
3885
- this.#syncNativeNumberAttributes();
3886
3894
  break;
3887
3895
  }
3888
3896
  this[name] = Number(newValue);
3889
- this.#syncNativeNumberAttributes();
3890
3897
  break;
3891
3898
  case "steppers": {
3892
3899
  const hasSteppers = newValue !== null && newValue !== "false";
3893
- this.input.type = hasSteppers ? "number" : "text";
3894
- if (hasSteppers) {
3895
- this.input.removeAttribute("inputmode");
3896
- this.#syncNativeNumberAttributes();
3897
- this.input.value = this.#transformNumber(this.value);
3898
- } else {
3899
- this.input.setAttribute("inputmode", "decimal");
3900
- this.input.value = this.#formatWithUnit(this.value);
3901
- }
3900
+ this.#syncSteppers(hasSteppers);
3902
3901
  break;
3903
3902
  }
3904
3903
  case "precision":
3905
3904
  this.#precision = newValue !== null ? Number(newValue) : 2;
3906
- this.input.value =
3907
- this.input.type === "number"
3908
- ? this.#transformNumber(this.value)
3909
- : this.#formatWithUnit(this.value);
3905
+ this.input.value = this.#formatWithUnit(this.value);
3910
3906
  break;
3911
3907
  case "name":
3912
3908
  this[name] = this.input[name] = newValue;
@@ -5425,10 +5421,18 @@ class FigToast extends HTMLDialogElement {
5425
5421
  }
5426
5422
  }
5427
5423
 
5424
+ #resolveAutoTheme() {
5425
+ if (this.getAttribute("theme") !== "auto") return;
5426
+ const cs = getComputedStyle(document.documentElement).colorScheme || "";
5427
+ const isDark = cs.includes("dark");
5428
+ this.style.colorScheme = isDark ? "light" : "dark";
5429
+ }
5430
+
5428
5431
  /**
5429
5432
  * Show the toast notification (non-modal)
5430
5433
  */
5431
5434
  showToast() {
5435
+ this.#resolveAutoTheme();
5432
5436
  this.show(); // Non-modal show
5433
5437
  this.applyPosition();
5434
5438
  this.startAutoClose();
@@ -5460,6 +5464,14 @@ class FigToast extends HTMLDialogElement {
5460
5464
  this.hideToast();
5461
5465
  }
5462
5466
  }
5467
+
5468
+ if (name === "theme") {
5469
+ if (newValue === "auto") {
5470
+ this.#resolveAutoTheme();
5471
+ } else {
5472
+ this.style.removeProperty("color-scheme");
5473
+ }
5474
+ }
5463
5475
  }
5464
5476
  }
5465
5477
  figDefineCustomizedBuiltIn("fig-toast", FigToast, { extends: "dialog" });
@@ -10390,3 +10402,554 @@ class FigFillPicker extends HTMLElement {
10390
10402
  }
10391
10403
  }
10392
10404
  customElements.define("fig-fill-picker", FigFillPicker);
10405
+
10406
+ /* Choice */
10407
+ /**
10408
+ * A generic choice container for use within FigChooser.
10409
+ * @attr {string} value - Identifier for this choice
10410
+ * @attr {boolean} selected - Whether this choice is currently selected
10411
+ * @attr {boolean} disabled - Whether this choice is disabled
10412
+ */
10413
+ class FigChoice extends HTMLElement {
10414
+ static get observedAttributes() {
10415
+ return ["selected", "disabled"];
10416
+ }
10417
+
10418
+ connectedCallback() {
10419
+ this.setAttribute("role", "option");
10420
+ if (!this.hasAttribute("tabindex")) {
10421
+ this.setAttribute("tabindex", "0");
10422
+ }
10423
+ this.setAttribute(
10424
+ "aria-selected",
10425
+ this.hasAttribute("selected") ? "true" : "false",
10426
+ );
10427
+ if (this.hasAttribute("disabled")) {
10428
+ this.setAttribute("aria-disabled", "true");
10429
+ }
10430
+ }
10431
+
10432
+ attributeChangedCallback(name) {
10433
+ if (name === "selected") {
10434
+ this.setAttribute(
10435
+ "aria-selected",
10436
+ this.hasAttribute("selected") ? "true" : "false",
10437
+ );
10438
+ }
10439
+ if (name === "disabled") {
10440
+ const isDisabled =
10441
+ this.hasAttribute("disabled") &&
10442
+ this.getAttribute("disabled") !== "false";
10443
+ if (isDisabled) {
10444
+ this.setAttribute("aria-disabled", "true");
10445
+ this.setAttribute("tabindex", "-1");
10446
+ } else {
10447
+ this.removeAttribute("aria-disabled");
10448
+ this.setAttribute("tabindex", "0");
10449
+ }
10450
+ }
10451
+ }
10452
+ }
10453
+ customElements.define("fig-choice", FigChoice);
10454
+
10455
+ /* Chooser */
10456
+ /**
10457
+ * A selection controller for a list of choice elements.
10458
+ * @attr {string} choice-element - CSS selector for child choices (default: "fig-choice")
10459
+ * @attr {string} layout - Layout mode: "vertical" (default), "horizontal", "grid"
10460
+ * @attr {string} value - Value of the currently selected choice
10461
+ * @attr {boolean} disabled - Whether the chooser is disabled
10462
+ */
10463
+ class FigChooser extends HTMLElement {
10464
+ #selectedChoice = null;
10465
+ #boundHandleClick = this.#handleClick.bind(this);
10466
+ #boundHandleKeyDown = this.#handleKeyDown.bind(this);
10467
+ #boundSyncOverflow = this.#syncOverflow.bind(this);
10468
+ #mutationObserver = null;
10469
+ #resizeObserver = null;
10470
+ #navStart = null;
10471
+ #navEnd = null;
10472
+ #dragState = null;
10473
+
10474
+ constructor() {
10475
+ super();
10476
+ }
10477
+
10478
+ static get observedAttributes() {
10479
+ return ["value", "disabled", "choice-element", "drag", "overflow", "loop"];
10480
+ }
10481
+
10482
+ get #overflowMode() {
10483
+ return this.getAttribute("overflow") === "scrollbar" ? "scrollbar" : "buttons";
10484
+ }
10485
+
10486
+ get #dragEnabled() {
10487
+ const attr = this.getAttribute("drag");
10488
+ return attr === null || attr !== "false";
10489
+ }
10490
+
10491
+ get #choiceSelector() {
10492
+ return this.getAttribute("choice-element") || "fig-choice";
10493
+ }
10494
+
10495
+ get choices() {
10496
+ return Array.from(this.querySelectorAll(this.#choiceSelector));
10497
+ }
10498
+
10499
+ get selectedChoice() {
10500
+ return this.#selectedChoice;
10501
+ }
10502
+
10503
+ set selectedChoice(element) {
10504
+ if (element && !this.contains(element)) return;
10505
+ const choices = this.choices;
10506
+ for (const choice of choices) {
10507
+ const shouldSelect = choice === element;
10508
+ const isSelected = choice.hasAttribute("selected");
10509
+ if (shouldSelect && !isSelected) {
10510
+ choice.setAttribute("selected", "");
10511
+ } else if (!shouldSelect && isSelected) {
10512
+ choice.removeAttribute("selected");
10513
+ }
10514
+ }
10515
+ this.#selectedChoice = element;
10516
+ const val = element?.getAttribute("value") ?? "";
10517
+ if (this.getAttribute("value") !== val) {
10518
+ if (val) {
10519
+ this.setAttribute("value", val);
10520
+ }
10521
+ }
10522
+ this.#scrollToChoice(element);
10523
+ }
10524
+
10525
+ get value() {
10526
+ return this.#selectedChoice?.getAttribute("value") ?? "";
10527
+ }
10528
+
10529
+ set value(val) {
10530
+ if (val === null || val === undefined || val === "") return;
10531
+ this.setAttribute("value", String(val));
10532
+ }
10533
+
10534
+ connectedCallback() {
10535
+ this.setAttribute("role", "listbox");
10536
+ this.addEventListener("click", this.#boundHandleClick);
10537
+ this.addEventListener("keydown", this.#boundHandleKeyDown);
10538
+ this.addEventListener("scroll", this.#boundSyncOverflow);
10539
+ this.#applyOverflowMode();
10540
+ this.#setupDrag();
10541
+ this.#startObserver();
10542
+ this.#startResizeObserver();
10543
+
10544
+ requestAnimationFrame(() => {
10545
+ this.#syncSelection();
10546
+ this.#syncOverflow();
10547
+ });
10548
+ }
10549
+
10550
+ disconnectedCallback() {
10551
+ this.removeEventListener("click", this.#boundHandleClick);
10552
+ this.removeEventListener("keydown", this.#boundHandleKeyDown);
10553
+ this.removeEventListener("scroll", this.#boundSyncOverflow);
10554
+ this.#teardownDrag();
10555
+ this.#mutationObserver?.disconnect();
10556
+ this.#mutationObserver = null;
10557
+ this.#resizeObserver?.disconnect();
10558
+ this.#resizeObserver = null;
10559
+ this.#removeNavButtons();
10560
+ }
10561
+
10562
+ attributeChangedCallback(name, oldValue, newValue) {
10563
+ if (name === "value" && newValue !== oldValue && newValue) {
10564
+ this.#selectByValue(newValue);
10565
+ }
10566
+ if (name === "disabled") {
10567
+ const isDisabled = newValue !== null && newValue !== "false";
10568
+ const choices = this.choices;
10569
+ for (const choice of choices) {
10570
+ if (isDisabled) {
10571
+ choice.setAttribute("aria-disabled", "true");
10572
+ choice.setAttribute("tabindex", "-1");
10573
+ } else {
10574
+ choice.removeAttribute("aria-disabled");
10575
+ choice.setAttribute("tabindex", "0");
10576
+ }
10577
+ }
10578
+ }
10579
+ if (name === "choice-element") {
10580
+ requestAnimationFrame(() => this.#syncSelection());
10581
+ }
10582
+ if (name === "drag") {
10583
+ if (this.#dragEnabled) {
10584
+ this.#setupDrag();
10585
+ } else {
10586
+ this.#teardownDrag();
10587
+ }
10588
+ }
10589
+ if (name === "overflow") {
10590
+ this.#applyOverflowMode();
10591
+ }
10592
+ }
10593
+
10594
+ #syncSelection() {
10595
+ const choices = this.choices;
10596
+ if (!choices.length) {
10597
+ this.#selectedChoice = null;
10598
+ return;
10599
+ }
10600
+
10601
+ const valueAttr = this.getAttribute("value");
10602
+ if (valueAttr && this.#selectByValue(valueAttr)) return;
10603
+
10604
+ const alreadySelected = choices.find((c) => c.hasAttribute("selected"));
10605
+ if (alreadySelected) {
10606
+ this.selectedChoice = alreadySelected;
10607
+ return;
10608
+ }
10609
+
10610
+ this.selectedChoice = choices[0];
10611
+ }
10612
+
10613
+ #selectByValue(value) {
10614
+ const choices = this.choices;
10615
+ for (const choice of choices) {
10616
+ if (choice.getAttribute("value") === value) {
10617
+ this.selectedChoice = choice;
10618
+ return true;
10619
+ }
10620
+ }
10621
+ return false;
10622
+ }
10623
+
10624
+ #findChoiceFromTarget(target) {
10625
+ const selector = this.#choiceSelector;
10626
+ let el = target;
10627
+ while (el && el !== this) {
10628
+ if (el.matches(selector)) return el;
10629
+ el = el.parentElement;
10630
+ }
10631
+ return null;
10632
+ }
10633
+
10634
+ #handleClick(event) {
10635
+ if (
10636
+ this.hasAttribute("disabled") &&
10637
+ this.getAttribute("disabled") !== "false"
10638
+ )
10639
+ return;
10640
+ const choice = this.#findChoiceFromTarget(event.target);
10641
+ if (!choice) return;
10642
+ if (
10643
+ choice.hasAttribute("disabled") &&
10644
+ choice.getAttribute("disabled") !== "false"
10645
+ )
10646
+ return;
10647
+ this.selectedChoice = choice;
10648
+ this.#emitEvents();
10649
+ }
10650
+
10651
+ #handleKeyDown(event) {
10652
+ if (
10653
+ this.hasAttribute("disabled") &&
10654
+ this.getAttribute("disabled") !== "false"
10655
+ )
10656
+ return;
10657
+ const choices = this.choices.filter(
10658
+ (c) =>
10659
+ !c.hasAttribute("disabled") || c.getAttribute("disabled") === "false",
10660
+ );
10661
+ if (!choices.length) return;
10662
+ const currentIndex = choices.indexOf(this.#selectedChoice);
10663
+ let nextIndex = currentIndex;
10664
+
10665
+ const loop = this.hasAttribute("loop");
10666
+
10667
+ switch (event.key) {
10668
+ case "ArrowDown":
10669
+ case "ArrowRight":
10670
+ event.preventDefault();
10671
+ if (currentIndex < choices.length - 1) {
10672
+ nextIndex = currentIndex + 1;
10673
+ } else if (loop) {
10674
+ nextIndex = 0;
10675
+ }
10676
+ break;
10677
+ case "ArrowUp":
10678
+ case "ArrowLeft":
10679
+ event.preventDefault();
10680
+ if (currentIndex > 0) {
10681
+ nextIndex = currentIndex - 1;
10682
+ } else if (loop) {
10683
+ nextIndex = choices.length - 1;
10684
+ }
10685
+ break;
10686
+ case "Home":
10687
+ event.preventDefault();
10688
+ nextIndex = 0;
10689
+ break;
10690
+ case "End":
10691
+ event.preventDefault();
10692
+ nextIndex = choices.length - 1;
10693
+ break;
10694
+ case "Enter":
10695
+ case " ":
10696
+ event.preventDefault();
10697
+ if (document.activeElement?.matches(this.#choiceSelector)) {
10698
+ const focused = this.#findChoiceFromTarget(document.activeElement);
10699
+ if (focused && focused !== this.#selectedChoice) {
10700
+ this.selectedChoice = focused;
10701
+ this.#emitEvents();
10702
+ }
10703
+ }
10704
+ return;
10705
+ default:
10706
+ return;
10707
+ }
10708
+
10709
+ if (nextIndex !== currentIndex && choices[nextIndex]) {
10710
+ this.selectedChoice = choices[nextIndex];
10711
+ choices[nextIndex].focus();
10712
+ this.#emitEvents();
10713
+ }
10714
+ }
10715
+
10716
+ #emitEvents() {
10717
+ const val = this.value;
10718
+ this.dispatchEvent(
10719
+ new CustomEvent("input", { detail: val, bubbles: true }),
10720
+ );
10721
+ this.dispatchEvent(
10722
+ new CustomEvent("change", { detail: val, bubbles: true }),
10723
+ );
10724
+ }
10725
+
10726
+ #syncOverflow() {
10727
+ if (this.#overflowMode === "scrollbar") return;
10728
+ const isHorizontal = this.getAttribute("layout") === "horizontal";
10729
+ const threshold = 2;
10730
+
10731
+ if (isHorizontal) {
10732
+ const atStart = this.scrollLeft <= threshold;
10733
+ const atEnd = this.scrollLeft + this.clientWidth >= this.scrollWidth - threshold;
10734
+ this.classList.toggle("overflow-start", !atStart);
10735
+ this.classList.toggle("overflow-end", !atEnd);
10736
+ } else {
10737
+ const atStart = this.scrollTop <= threshold;
10738
+ const atEnd = this.scrollTop + this.clientHeight >= this.scrollHeight - threshold;
10739
+ this.classList.toggle("overflow-start", !atStart);
10740
+ this.classList.toggle("overflow-end", !atEnd);
10741
+ }
10742
+ }
10743
+
10744
+ #startResizeObserver() {
10745
+ this.#resizeObserver?.disconnect();
10746
+ this.#resizeObserver = new ResizeObserver(() => {
10747
+ this.#syncOverflow();
10748
+ });
10749
+ this.#resizeObserver.observe(this);
10750
+ }
10751
+
10752
+ #setupDrag() {
10753
+ if (this.#dragState?.bound) return;
10754
+ if (!this.#dragEnabled) return;
10755
+
10756
+ const onPointerDown = (e) => {
10757
+ if (e.button !== 0) return;
10758
+ const isHorizontal = this.getAttribute("layout") === "horizontal";
10759
+ const hasOverflow = isHorizontal
10760
+ ? this.scrollWidth > this.clientWidth
10761
+ : this.scrollHeight > this.clientHeight;
10762
+ if (!hasOverflow) return;
10763
+
10764
+ this.#dragState.active = true;
10765
+ this.#dragState.didDrag = false;
10766
+ this.#dragState.startX = e.clientX;
10767
+ this.#dragState.startY = e.clientY;
10768
+ this.#dragState.scrollLeft = this.scrollLeft;
10769
+ this.#dragState.scrollTop = this.scrollTop;
10770
+ this.style.cursor = "grab";
10771
+ this.style.userSelect = "none";
10772
+ };
10773
+
10774
+ const onPointerMove = (e) => {
10775
+ if (!this.#dragState.active) return;
10776
+ const isHorizontal = this.getAttribute("layout") === "horizontal";
10777
+ const dx = e.clientX - this.#dragState.startX;
10778
+ const dy = e.clientY - this.#dragState.startY;
10779
+
10780
+ if (!this.#dragState.didDrag && Math.abs(isHorizontal ? dx : dy) > 3) {
10781
+ this.#dragState.didDrag = true;
10782
+ this.style.cursor = "grabbing";
10783
+ this.setPointerCapture(e.pointerId);
10784
+ }
10785
+
10786
+ if (!this.#dragState.didDrag) return;
10787
+
10788
+ if (isHorizontal) {
10789
+ this.scrollLeft = this.#dragState.scrollLeft - dx;
10790
+ } else {
10791
+ this.scrollTop = this.#dragState.scrollTop - dy;
10792
+ }
10793
+ };
10794
+
10795
+ const onPointerUp = (e) => {
10796
+ if (!this.#dragState.active) return;
10797
+ const wasDrag = this.#dragState.didDrag;
10798
+ this.#dragState.active = false;
10799
+ this.#dragState.didDrag = false;
10800
+ this.style.cursor = "";
10801
+ this.style.userSelect = "";
10802
+ if (e.pointerId !== undefined) {
10803
+ try { this.releasePointerCapture(e.pointerId); } catch {}
10804
+ }
10805
+ if (wasDrag) {
10806
+ e.preventDefault();
10807
+ e.stopPropagation();
10808
+ }
10809
+ };
10810
+
10811
+ const onClick = (e) => {
10812
+ if (this.#dragState?.suppressClick) {
10813
+ e.stopPropagation();
10814
+ e.preventDefault();
10815
+ this.#dragState.suppressClick = false;
10816
+ }
10817
+ };
10818
+
10819
+ const onPointerUpCapture = (e) => {
10820
+ if (this.#dragState?.didDrag) {
10821
+ this.#dragState.suppressClick = true;
10822
+ setTimeout(() => {
10823
+ if (this.#dragState) this.#dragState.suppressClick = false;
10824
+ }, 0);
10825
+ }
10826
+ };
10827
+
10828
+ this.#dragState = {
10829
+ active: false,
10830
+ didDrag: false,
10831
+ suppressClick: false,
10832
+ startX: 0,
10833
+ startY: 0,
10834
+ scrollLeft: 0,
10835
+ scrollTop: 0,
10836
+ bound: true,
10837
+ onPointerDown,
10838
+ onPointerMove,
10839
+ onPointerUp,
10840
+ onClick,
10841
+ onPointerUpCapture,
10842
+ };
10843
+
10844
+ this.addEventListener("pointerdown", onPointerDown);
10845
+ window.addEventListener("pointermove", onPointerMove);
10846
+ window.addEventListener("pointerup", onPointerUp);
10847
+ this.addEventListener("pointerup", onPointerUpCapture, true);
10848
+ this.addEventListener("click", onClick, true);
10849
+ }
10850
+
10851
+ #teardownDrag() {
10852
+ if (!this.#dragState?.bound) return;
10853
+ this.removeEventListener("pointerdown", this.#dragState.onPointerDown);
10854
+ window.removeEventListener("pointermove", this.#dragState.onPointerMove);
10855
+ window.removeEventListener("pointerup", this.#dragState.onPointerUp);
10856
+ this.removeEventListener("pointerup", this.#dragState.onPointerUpCapture, true);
10857
+ this.removeEventListener("click", this.#dragState.onClick, true);
10858
+ this.style.cursor = "";
10859
+ this.style.userSelect = "";
10860
+ this.#dragState = null;
10861
+ }
10862
+
10863
+ #applyOverflowMode() {
10864
+ if (this.#overflowMode === "scrollbar") {
10865
+ this.#removeNavButtons();
10866
+ } else {
10867
+ this.#createNavButtons();
10868
+ }
10869
+ }
10870
+
10871
+ #removeNavButtons() {
10872
+ this.#navStart?.remove();
10873
+ this.#navEnd?.remove();
10874
+ this.#navStart = null;
10875
+ this.#navEnd = null;
10876
+ this.classList.remove("overflow-start", "overflow-end");
10877
+ }
10878
+
10879
+ #createNavButtons() {
10880
+ if (this.#navStart) return;
10881
+
10882
+ this.#navStart = document.createElement("button");
10883
+ this.#navStart.className = "fig-chooser-nav-start";
10884
+ this.#navStart.setAttribute("tabindex", "-1");
10885
+ this.#navStart.setAttribute("aria-label", "Scroll back");
10886
+
10887
+ this.#navEnd = document.createElement("button");
10888
+ this.#navEnd.className = "fig-chooser-nav-end";
10889
+ this.#navEnd.setAttribute("tabindex", "-1");
10890
+ this.#navEnd.setAttribute("aria-label", "Scroll forward");
10891
+
10892
+ this.#navStart.addEventListener("pointerdown", (e) => {
10893
+ e.stopPropagation();
10894
+ this.#scrollByPage(-1);
10895
+ });
10896
+
10897
+ this.#navEnd.addEventListener("pointerdown", (e) => {
10898
+ e.stopPropagation();
10899
+ this.#scrollByPage(1);
10900
+ });
10901
+
10902
+ this.prepend(this.#navStart);
10903
+ this.append(this.#navEnd);
10904
+ }
10905
+
10906
+ #scrollByPage(direction) {
10907
+ const isHorizontal =
10908
+ this.getAttribute("layout") === "horizontal";
10909
+ const pageSize = isHorizontal ? this.clientWidth : this.clientHeight;
10910
+ const scrollAmount = pageSize * 0.8 * direction;
10911
+
10912
+ this.scrollBy({
10913
+ [isHorizontal ? "left" : "top"]: scrollAmount,
10914
+ behavior: "smooth",
10915
+ });
10916
+ }
10917
+
10918
+ #scrollToChoice(el) {
10919
+ if (!el) return;
10920
+ requestAnimationFrame(() => {
10921
+ const overflowY = this.scrollHeight > this.clientHeight;
10922
+ const overflowX = this.scrollWidth > this.clientWidth;
10923
+ if (!overflowX && !overflowY) return;
10924
+
10925
+ const options = { behavior: "smooth" };
10926
+
10927
+ if (overflowY) {
10928
+ const target = el.offsetTop - this.clientHeight / 2 + el.offsetHeight / 2;
10929
+ options.top = target;
10930
+ }
10931
+
10932
+ if (overflowX) {
10933
+ const target = el.offsetLeft - this.clientWidth / 2 + el.offsetWidth / 2;
10934
+ options.left = target;
10935
+ }
10936
+
10937
+ this.scrollTo(options);
10938
+ });
10939
+ }
10940
+
10941
+ #startObserver() {
10942
+ this.#mutationObserver?.disconnect();
10943
+ this.#mutationObserver = new MutationObserver(() => {
10944
+ const choices = this.choices;
10945
+ if (this.#selectedChoice && !choices.includes(this.#selectedChoice)) {
10946
+ this.#selectedChoice = null;
10947
+ this.#syncSelection();
10948
+ } else if (!this.#selectedChoice && choices.length) {
10949
+ this.#syncSelection();
10950
+ }
10951
+ });
10952
+ this.#mutationObserver.observe(this, { childList: true, subtree: true });
10953
+ }
10954
+ }
10955
+ customElements.define("fig-chooser", FigChooser);