@rogieking/figui3 3.8.1 → 3.9.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/fig.js CHANGED
@@ -2442,18 +2442,25 @@ customElements.define("fig-segment", FigSegment);
2442
2442
  /**
2443
2443
  * A custom segmented control container element.
2444
2444
  * @attr {string} name - Identifier for the segmented control group
2445
+ * @attr {string} value - Selected segment value
2446
+ * @attr {boolean} animated - Enables animated selection indicator
2447
+ * @attr {"equal"|"auto"} sizing - Segment sizing mode
2445
2448
  */
2446
2449
  class FigSegmentedControl extends HTMLElement {
2447
2450
  #selectedSegment = null;
2448
2451
  #boundHandleClick = this.handleClick.bind(this);
2449
2452
  #mutationObserver = null;
2453
+ #resizeObserver = null;
2454
+ #indicatorFrame = 0;
2455
+ #indicatorSyncInstant = false;
2456
+ #hasRenderedIndicator = false;
2450
2457
 
2451
2458
  constructor() {
2452
2459
  super();
2453
2460
  }
2454
2461
 
2455
2462
  static get observedAttributes() {
2456
- return ["disabled", "value"];
2463
+ return ["disabled", "value", "animated", "sizing"];
2457
2464
  }
2458
2465
 
2459
2466
  connectedCallback() {
@@ -2464,10 +2471,13 @@ class FigSegmentedControl extends HTMLElement {
2464
2471
  this.getAttribute("disabled") !== "false",
2465
2472
  );
2466
2473
  this.#startSegmentObserver();
2474
+ this.#startResizeObserver();
2467
2475
 
2468
2476
  // Defer initial selection so child segments are available.
2469
2477
  requestAnimationFrame(() => {
2470
2478
  this.#syncSelectionFromAttributes({ enforceFallback: true });
2479
+ this.#refreshResizeObserverTargets();
2480
+ this.#queueIndicatorSync({ forceInstant: true });
2471
2481
  });
2472
2482
  }
2473
2483
 
@@ -2475,6 +2485,14 @@ class FigSegmentedControl extends HTMLElement {
2475
2485
  this.removeEventListener("click", this.#boundHandleClick);
2476
2486
  this.#mutationObserver?.disconnect();
2477
2487
  this.#mutationObserver = null;
2488
+ this.#resizeObserver?.disconnect();
2489
+ this.#resizeObserver = null;
2490
+ if (this.#indicatorFrame) {
2491
+ cancelAnimationFrame(this.#indicatorFrame);
2492
+ this.#indicatorFrame = 0;
2493
+ }
2494
+ this.#indicatorSyncInstant = false;
2495
+ this.#hasRenderedIndicator = false;
2478
2496
  }
2479
2497
 
2480
2498
  get selectedSegment() {
@@ -2492,7 +2510,9 @@ class FigSegmentedControl extends HTMLElement {
2492
2510
  seg.removeAttribute("selected");
2493
2511
  }
2494
2512
  }
2495
- this.#selectedSegment = segment;
2513
+ this.#selectedSegment =
2514
+ segment instanceof HTMLElement && this.contains(segment) ? segment : null;
2515
+ this.#queueIndicatorSync();
2496
2516
  }
2497
2517
 
2498
2518
  get value() {
@@ -2559,10 +2579,98 @@ class FigSegmentedControl extends HTMLElement {
2559
2579
  return false;
2560
2580
  }
2561
2581
 
2582
+ #isAnimatedEnabled() {
2583
+ const rawAnimated = this.getAttribute("animated");
2584
+ if (rawAnimated === null) return false;
2585
+ if (rawAnimated === "") return true;
2586
+ return rawAnimated.trim().toLowerCase() === "true";
2587
+ }
2588
+
2589
+ #queueIndicatorSync({ forceInstant = false } = {}) {
2590
+ this.#indicatorSyncInstant = this.#indicatorSyncInstant || forceInstant;
2591
+ if (this.#indicatorFrame) return;
2592
+
2593
+ this.#indicatorFrame = requestAnimationFrame(() => {
2594
+ this.#indicatorFrame = 0;
2595
+ const nextForceInstant = this.#indicatorSyncInstant;
2596
+ this.#indicatorSyncInstant = false;
2597
+ this.#syncIndicator({ forceInstant: nextForceInstant });
2598
+ });
2599
+ }
2600
+
2601
+ #syncIndicator({ forceInstant = false } = {}) {
2602
+ const isDisabled =
2603
+ this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
2604
+ const isAnimated = this.#isAnimatedEnabled();
2605
+ const activeSegment =
2606
+ this.#selectedSegment && this.contains(this.#selectedSegment)
2607
+ ? this.#selectedSegment
2608
+ : this.#getFirstSelectedSegment();
2609
+
2610
+ if (isDisabled || !isAnimated) {
2611
+ this.style.setProperty("--seg-indicator-opacity", "0");
2612
+ this.style.setProperty("--seg-indicator-transition-duration", "0ms");
2613
+ this.removeAttribute("data-indicator-ready");
2614
+ if (!isAnimated || isDisabled) {
2615
+ this.#hasRenderedIndicator = false;
2616
+ }
2617
+ return;
2618
+ }
2619
+
2620
+ if (!activeSegment) {
2621
+ // During transient mutation/paint windows, keep the previous indicator
2622
+ // state to avoid flicker while the next selected segment resolves.
2623
+ if (this.#hasRenderedIndicator) return;
2624
+ this.style.setProperty("--seg-indicator-opacity", "0");
2625
+ this.style.setProperty("--seg-indicator-transition-duration", "0ms");
2626
+ this.removeAttribute("data-indicator-ready");
2627
+ return;
2628
+ }
2629
+
2630
+ const hostRect = this.getBoundingClientRect();
2631
+ const segmentRect = activeSegment.getBoundingClientRect();
2632
+ if (hostRect.width <= 0 || segmentRect.width <= 0) {
2633
+ if (this.#hasRenderedIndicator) return;
2634
+ this.style.setProperty("--seg-indicator-opacity", "0");
2635
+ this.style.setProperty("--seg-indicator-transition-duration", "0ms");
2636
+ this.removeAttribute("data-indicator-ready");
2637
+ return;
2638
+ }
2639
+
2640
+ const x = Math.max(0, segmentRect.left - hostRect.left);
2641
+ this.style.setProperty("--seg-indicator-x", `${x}px`);
2642
+ this.style.setProperty("--seg-indicator-w", `${segmentRect.width}px`);
2643
+ this.style.setProperty("--seg-indicator-opacity", "1");
2644
+ this.style.setProperty(
2645
+ "--seg-indicator-transition-duration",
2646
+ !this.#hasRenderedIndicator || forceInstant ? "0ms" : "150ms",
2647
+ );
2648
+ this.setAttribute("data-indicator-ready", "true");
2649
+ this.#hasRenderedIndicator = true;
2650
+ }
2651
+
2652
+ #startResizeObserver() {
2653
+ this.#resizeObserver?.disconnect();
2654
+ this.#resizeObserver = new ResizeObserver(() => {
2655
+ this.#queueIndicatorSync();
2656
+ });
2657
+ this.#refreshResizeObserverTargets();
2658
+ }
2659
+
2660
+ #refreshResizeObserverTargets() {
2661
+ if (!this.#resizeObserver) return;
2662
+ this.#resizeObserver.disconnect();
2663
+ this.#resizeObserver.observe(this);
2664
+ this.querySelectorAll("fig-segment").forEach((segment) => {
2665
+ this.#resizeObserver?.observe(segment);
2666
+ });
2667
+ }
2668
+
2562
2669
  #syncSelectionFromAttributes({ enforceFallback = false } = {}) {
2563
2670
  const segments = this.querySelectorAll("fig-segment");
2564
2671
  if (segments.length === 0) {
2565
2672
  this.#selectedSegment = null;
2673
+ this.#queueIndicatorSync({ forceInstant: true });
2566
2674
  return;
2567
2675
  }
2568
2676
 
@@ -2623,6 +2731,7 @@ class FigSegmentedControl extends HTMLElement {
2623
2731
  this.hasAttribute("disabled") &&
2624
2732
  this.getAttribute("disabled") !== "false",
2625
2733
  );
2734
+ this.#refreshResizeObserverTargets();
2626
2735
  this.#syncSelectionFromAttributes({ enforceFallback: true });
2627
2736
  }
2628
2737
  });
@@ -2682,11 +2791,25 @@ class FigSegmentedControl extends HTMLElement {
2682
2791
 
2683
2792
  if (name === "disabled") {
2684
2793
  this.#applyDisabled(newValue !== null && newValue !== "false");
2794
+ this.#queueIndicatorSync({ forceInstant: true });
2685
2795
  return;
2686
2796
  }
2687
2797
 
2688
2798
  if (name === "value") {
2689
2799
  this.#syncSelectionFromAttributes({ enforceFallback: false });
2800
+ return;
2801
+ }
2802
+
2803
+ if (name === "animated") {
2804
+ if (!this.#isAnimatedEnabled()) {
2805
+ this.#hasRenderedIndicator = false;
2806
+ }
2807
+ this.#queueIndicatorSync({ forceInstant: true });
2808
+ return;
2809
+ }
2810
+
2811
+ if (name === "sizing") {
2812
+ this.#queueIndicatorSync({ forceInstant: true });
2690
2813
  }
2691
2814
  }
2692
2815
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "MIT",