@rogieking/figui3 3.0.2 → 3.1.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
@@ -2436,49 +2436,195 @@ customElements.define("fig-segment", FigSegment);
2436
2436
  */
2437
2437
  class FigSegmentedControl extends HTMLElement {
2438
2438
  #selectedSegment = null;
2439
+ #boundHandleClick = this.handleClick.bind(this);
2440
+ #mutationObserver = null;
2439
2441
 
2440
2442
  constructor() {
2441
2443
  super();
2442
2444
  }
2443
2445
 
2444
2446
  static get observedAttributes() {
2445
- return ["disabled"];
2447
+ return ["disabled", "value"];
2446
2448
  }
2447
2449
 
2448
2450
  connectedCallback() {
2449
2451
  this.name = this.getAttribute("name") || "segmented-control";
2450
- this.addEventListener("click", this.handleClick.bind(this));
2452
+ this.addEventListener("click", this.#boundHandleClick);
2451
2453
  this.#applyDisabled(
2452
2454
  this.hasAttribute("disabled") &&
2453
2455
  this.getAttribute("disabled") !== "false",
2454
2456
  );
2457
+ this.#startSegmentObserver();
2455
2458
 
2456
- // Ensure at least one segment is selected (default to first)
2459
+ // Defer initial selection so child segments are available.
2457
2460
  requestAnimationFrame(() => {
2458
- const segments = this.querySelectorAll("fig-segment");
2459
- const hasSelected = Array.from(segments).some((s) =>
2460
- s.hasAttribute("selected"),
2461
- );
2462
- if (!hasSelected && segments.length > 0) {
2463
- this.selectedSegment = segments[0];
2464
- }
2461
+ this.#syncSelectionFromAttributes({ enforceFallback: true });
2465
2462
  });
2466
2463
  }
2467
2464
 
2465
+ disconnectedCallback() {
2466
+ this.removeEventListener("click", this.#boundHandleClick);
2467
+ this.#mutationObserver?.disconnect();
2468
+ this.#mutationObserver = null;
2469
+ }
2470
+
2468
2471
  get selectedSegment() {
2469
2472
  return this.#selectedSegment;
2470
2473
  }
2471
2474
 
2472
2475
  set selectedSegment(segment) {
2473
- // Deselect previous
2474
- if (this.#selectedSegment) {
2475
- this.#selectedSegment.removeAttribute("selected");
2476
+ const segments = this.querySelectorAll("fig-segment");
2477
+ for (const seg of segments) {
2478
+ const shouldBeSelected = seg === segment;
2479
+ const isSelected = seg.hasAttribute("selected");
2480
+ if (shouldBeSelected && !isSelected) {
2481
+ seg.setAttribute("selected", "true");
2482
+ } else if (!shouldBeSelected && isSelected) {
2483
+ seg.removeAttribute("selected");
2484
+ }
2476
2485
  }
2477
- // Select new
2478
2486
  this.#selectedSegment = segment;
2479
- if (segment) {
2480
- segment.setAttribute("selected", "true");
2487
+ }
2488
+
2489
+ get value() {
2490
+ return this.getAttribute("value") || "";
2491
+ }
2492
+
2493
+ set value(val) {
2494
+ if (val === null || val === undefined) {
2495
+ this.removeAttribute("value");
2496
+ return;
2497
+ }
2498
+ this.setAttribute("value", String(val));
2499
+ }
2500
+
2501
+ #emitSelectionEvents(value) {
2502
+ this.dispatchEvent(
2503
+ new CustomEvent("input", {
2504
+ detail: value,
2505
+ bubbles: true,
2506
+ }),
2507
+ );
2508
+ this.dispatchEvent(
2509
+ new CustomEvent("change", {
2510
+ detail: value,
2511
+ bubbles: true,
2512
+ }),
2513
+ );
2514
+ }
2515
+
2516
+ #resolveSegmentValue(segment) {
2517
+ const explicitValue = segment.getAttribute("value");
2518
+ if (explicitValue !== null) {
2519
+ const trimmedExplicitValue = explicitValue.trim();
2520
+ if (trimmedExplicitValue.length > 0) {
2521
+ return trimmedExplicitValue;
2522
+ }
2523
+ }
2524
+
2525
+ return segment.textContent?.trim() || "";
2526
+ }
2527
+
2528
+ #getFirstSelectedSegment() {
2529
+ const segments = this.querySelectorAll("fig-segment");
2530
+ for (const segment of segments) {
2531
+ if (segment.hasAttribute("selected")) return segment;
2532
+ }
2533
+ return null;
2534
+ }
2535
+
2536
+ #selectByValue(value) {
2537
+ const normalizedValue = String(value ?? "").trim();
2538
+ if (!normalizedValue) return false;
2539
+
2540
+ const segments = this.querySelectorAll("fig-segment");
2541
+ for (const segment of segments) {
2542
+ const segmentValue = this.#resolveSegmentValue(segment);
2543
+ if (!segmentValue) continue;
2544
+ if (segmentValue === normalizedValue) {
2545
+ this.selectedSegment = segment;
2546
+ return true;
2547
+ }
2481
2548
  }
2549
+
2550
+ return false;
2551
+ }
2552
+
2553
+ #syncSelectionFromAttributes({ enforceFallback = false } = {}) {
2554
+ const segments = this.querySelectorAll("fig-segment");
2555
+ if (segments.length === 0) {
2556
+ this.#selectedSegment = null;
2557
+ return;
2558
+ }
2559
+
2560
+ const rawValue = this.getAttribute("value");
2561
+ const normalizedValue = rawValue?.trim() ?? "";
2562
+ if (rawValue !== null) {
2563
+ if (normalizedValue !== rawValue) {
2564
+ this.setAttribute("value", normalizedValue);
2565
+ return;
2566
+ }
2567
+
2568
+ if (normalizedValue && this.#selectByValue(normalizedValue)) {
2569
+ return;
2570
+ }
2571
+ }
2572
+
2573
+ const selected = this.#getFirstSelectedSegment();
2574
+ if (selected) {
2575
+ this.selectedSegment = selected;
2576
+ return;
2577
+ }
2578
+
2579
+ if (enforceFallback) {
2580
+ this.selectedSegment = segments[0];
2581
+ }
2582
+ }
2583
+
2584
+ #startSegmentObserver() {
2585
+ this.#mutationObserver?.disconnect();
2586
+ this.#mutationObserver = new MutationObserver((mutations) => {
2587
+ let shouldResync = false;
2588
+
2589
+ for (const mutation of mutations) {
2590
+ if (mutation.type === "childList") {
2591
+ shouldResync = true;
2592
+ break;
2593
+ }
2594
+
2595
+ if (
2596
+ mutation.type === "attributes" &&
2597
+ mutation.target instanceof HTMLElement &&
2598
+ mutation.target.tagName.toLowerCase() === "fig-segment" &&
2599
+ (mutation.attributeName === "value" ||
2600
+ mutation.attributeName === "selected")
2601
+ ) {
2602
+ shouldResync = true;
2603
+ break;
2604
+ }
2605
+
2606
+ if (mutation.type === "characterData") {
2607
+ shouldResync = true;
2608
+ break;
2609
+ }
2610
+ }
2611
+
2612
+ if (shouldResync) {
2613
+ this.#applyDisabled(
2614
+ this.hasAttribute("disabled") &&
2615
+ this.getAttribute("disabled") !== "false",
2616
+ );
2617
+ this.#syncSelectionFromAttributes({ enforceFallback: true });
2618
+ }
2619
+ });
2620
+
2621
+ this.#mutationObserver.observe(this, {
2622
+ childList: true,
2623
+ subtree: true,
2624
+ characterData: true,
2625
+ attributes: true,
2626
+ attributeFilter: ["value", "selected"],
2627
+ });
2482
2628
  }
2483
2629
 
2484
2630
  handleClick(event) {
@@ -2489,15 +2635,23 @@ class FigSegmentedControl extends HTMLElement {
2489
2635
  return;
2490
2636
  }
2491
2637
  const segment = event.target.closest("fig-segment");
2492
- if (segment) {
2493
- const segments = this.querySelectorAll("fig-segment");
2494
- for (const seg of segments) {
2495
- if (seg === segment) {
2496
- this.selectedSegment = seg;
2497
- } else {
2498
- seg.removeAttribute("selected");
2499
- }
2500
- }
2638
+ if (!segment || !this.contains(segment)) return;
2639
+
2640
+ const previousSegment = this.selectedSegment;
2641
+ const previousValue = this.value;
2642
+
2643
+ this.selectedSegment = segment;
2644
+ const resolvedValue = this.#resolveSegmentValue(segment);
2645
+
2646
+ if (resolvedValue) {
2647
+ this.setAttribute("value", resolvedValue);
2648
+ } else {
2649
+ this.removeAttribute("value");
2650
+ }
2651
+
2652
+ const nextValue = this.value;
2653
+ if (previousSegment !== segment || previousValue !== nextValue) {
2654
+ this.#emitSelectionEvents(nextValue);
2501
2655
  }
2502
2656
  }
2503
2657
 
@@ -2515,8 +2669,15 @@ class FigSegmentedControl extends HTMLElement {
2515
2669
  }
2516
2670
 
2517
2671
  attributeChangedCallback(name, oldValue, newValue) {
2518
- if (name === "disabled" && oldValue !== newValue) {
2672
+ if (oldValue === newValue) return;
2673
+
2674
+ if (name === "disabled") {
2519
2675
  this.#applyDisabled(newValue !== null && newValue !== "false");
2676
+ return;
2677
+ }
2678
+
2679
+ if (name === "value") {
2680
+ this.#syncSelectionFromAttributes({ enforceFallback: false });
2520
2681
  }
2521
2682
  }
2522
2683
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "3.0.2",
3
+ "version": "3.1.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",
@@ -72,6 +72,9 @@
72
72
  ],
73
73
  "devDependencies": {
74
74
  "@types/bun": "latest",
75
+ "clean-css-cli": "^5.6.3",
76
+ "lightningcss": "^1.32.0",
77
+ "lightningcss-cli": "^1.32.0",
75
78
  "playwright": "^1.58.2"
76
79
  },
77
80
  "peerDependencies": {