@rogieking/figui3 6.21.1 → 6.23.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-editor.js CHANGED
@@ -111,10 +111,13 @@ function parseGradientInterpolationSelectValue(val) {
111
111
  * @attr {boolean} alpha - Whether to show alpha/opacity controls (default: true)
112
112
  * @attr {string} dialog-position - Position of the popup (default: "left")
113
113
  */
114
+ let figFillPickerDialogId = 0;
115
+
114
116
  class FigFillPicker extends HTMLElement {
115
117
  #trigger = null;
116
118
  #swatch = null;
117
119
  #dialog = null;
120
+ #dialogId = `fig-fill-picker-dialog-${++figFillPickerDialogId}`;
118
121
  #activeTab = "solid";
119
122
  anchorElement = null;
120
123
 
@@ -153,10 +156,13 @@ class FigFillPicker extends HTMLElement {
153
156
  #teardownColorAreaEvents = null;
154
157
  #gradientInterpolationOpenObserver = null;
155
158
  #valueAtOpen = null;
159
+ #lastChangeValue = null;
156
160
  #webcamStart = null;
157
161
  #webcamRequestId = 0;
158
162
  #boundTriggerClick = null;
159
163
  #boundTriggerKeydown = null;
164
+ #rafIds = new Set();
165
+ #ownedBlobUrls = new Set();
160
166
 
161
167
  constructor() {
162
168
  super();
@@ -170,7 +176,6 @@ class FigFillPicker extends HTMLElement {
170
176
  "disabled",
171
177
  "alpha",
172
178
  "mode",
173
- "experimental",
174
179
  "aria-label",
175
180
  "aria-labelledby",
176
181
  "aria-describedby",
@@ -181,7 +186,7 @@ class FigFillPicker extends HTMLElement {
181
186
  // Use display: contents
182
187
  this.style.display = "contents";
183
188
 
184
- requestAnimationFrame(() => {
189
+ this.#scheduleFrame(() => {
185
190
  this.#setupTrigger();
186
191
  this.#parseValue();
187
192
  this.#updateSwatch();
@@ -190,19 +195,45 @@ class FigFillPicker extends HTMLElement {
190
195
 
191
196
  disconnectedCallback() {
192
197
  this.#discardDialog();
193
- if (this.#webcam.snapshot?.startsWith("blob:")) {
194
- URL.revokeObjectURL(this.#webcam.snapshot);
195
- if (this.#image.url === this.#webcam.snapshot) this.#image.url = null;
196
- this.#webcam.snapshot = null;
197
- }
198
- if (this.#video.url && this.#video.url.startsWith("blob:")) {
199
- URL.revokeObjectURL(this.#video.url);
200
- }
198
+ this.#cancelFrames();
199
+ this.#revokeOwnedBlobUrls();
201
200
  if (this.#swatch) this.#swatch.removeAttribute("selected");
202
201
  if (this.#trigger) {
203
202
  this.#trigger.removeEventListener("click", this.#boundTriggerClick);
204
203
  this.#trigger.removeEventListener("keydown", this.#boundTriggerKeydown);
205
204
  }
205
+ this.#trigger = null;
206
+ this.#swatch = null;
207
+ }
208
+
209
+ #isDisabled() {
210
+ return (
211
+ this.hasAttribute("disabled") &&
212
+ this.getAttribute("disabled") !== "false"
213
+ );
214
+ }
215
+
216
+ #scheduleFrame(callback) {
217
+ const id = requestAnimationFrame(() => {
218
+ this.#rafIds.delete(id);
219
+ if (this.isConnected) callback();
220
+ });
221
+ this.#rafIds.add(id);
222
+ return id;
223
+ }
224
+
225
+ #cancelFrames() {
226
+ this.#rafIds.forEach((id) => cancelAnimationFrame(id));
227
+ this.#rafIds.clear();
228
+ }
229
+
230
+ #revokeOwnedBlobUrls() {
231
+ this.#ownedBlobUrls.forEach((url) => URL.revokeObjectURL(url));
232
+ this.#ownedBlobUrls.clear();
233
+ if (this.#webcam.snapshot?.startsWith("blob:")) {
234
+ if (this.#image.url === this.#webcam.snapshot) this.#image.url = null;
235
+ this.#webcam.snapshot = null;
236
+ }
206
237
  }
207
238
 
208
239
  #setupTrigger() {
@@ -234,7 +265,7 @@ class FigFillPicker extends HTMLElement {
234
265
 
235
266
  // Prevent the swatch's internal color input from opening system picker
236
267
  if (this.#swatch) {
237
- requestAnimationFrame(() => {
268
+ this.#scheduleFrame(() => {
238
269
  const input = this.#swatch.querySelector('input[type="color"]');
239
270
  if (input) {
240
271
  input.remove();
@@ -250,11 +281,15 @@ class FigFillPicker extends HTMLElement {
250
281
 
251
282
  #syncTriggerA11y() {
252
283
  if (!this.#trigger) return;
253
- const disabled = this.hasAttribute("disabled") && this.getAttribute("disabled") !== "false";
284
+ const disabled = this.#isDisabled();
254
285
  const labelledBy = this.getAttribute("aria-labelledby");
255
286
  if (!this.#trigger.hasAttribute("role")) this.#trigger.setAttribute("role", "button");
256
287
  this.#trigger.setAttribute("tabindex", disabled ? "-1" : "0");
257
288
  this.#trigger.setAttribute("aria-disabled", disabled ? "true" : "false");
289
+ this.#trigger.setAttribute("aria-haspopup", "dialog");
290
+ this.#trigger.setAttribute("aria-expanded", this.#dialog?.open ? "true" : "false");
291
+ this.#trigger.setAttribute("aria-controls", this.#dialogId);
292
+ this.#trigger.removeAttribute("aria-hidden");
258
293
  if (labelledBy) {
259
294
  this.#trigger.setAttribute("aria-labelledby", labelledBy);
260
295
  this.#trigger.removeAttribute("aria-label");
@@ -273,11 +308,7 @@ class FigFillPicker extends HTMLElement {
273
308
  }
274
309
 
275
310
  #handleTriggerClick(e) {
276
- if (
277
- this.hasAttribute("disabled") &&
278
- this.getAttribute("disabled") !== "false"
279
- )
280
- return;
311
+ if (this.#isDisabled()) return;
281
312
  e.stopPropagation();
282
313
  e.preventDefault();
283
314
  this.#openDialog();
@@ -285,11 +316,7 @@ class FigFillPicker extends HTMLElement {
285
316
 
286
317
  #handleTriggerKeydown(e) {
287
318
  if (e.key !== "Enter" && e.key !== " ") return;
288
- if (
289
- this.hasAttribute("disabled") &&
290
- this.getAttribute("disabled") !== "false"
291
- )
292
- return;
319
+ if (this.#isDisabled()) return;
293
320
  e.preventDefault();
294
321
  e.stopPropagation();
295
322
  this.#openDialog();
@@ -315,9 +342,15 @@ class FigFillPicker extends HTMLElement {
315
342
  this.#color = parsed.color;
316
343
  }
317
344
  }
318
- // Parse opacity (0-100) and convert to alpha (0-1)
319
- if (parsed.opacity !== undefined) {
320
- this.#color.a = parsed.opacity / 100;
345
+ // `alpha` is canonical (0-1); retain `opacity` (0-100) compatibility.
346
+ const parsedAlpha =
347
+ parsed.alpha !== undefined
348
+ ? Number(parsed.alpha)
349
+ : parsed.opacity !== undefined
350
+ ? Number(parsed.opacity) / 100
351
+ : undefined;
352
+ if (Number.isFinite(parsedAlpha)) {
353
+ this.#color.a = Math.max(0, Math.min(1, parsedAlpha));
321
354
  }
322
355
  // Gamut UI hidden for now — lock to sRGB.
323
356
  this.#gamut = "srgb";
@@ -416,19 +449,26 @@ class FigFillPicker extends HTMLElement {
416
449
  }
417
450
 
418
451
  #openDialog() {
452
+ if (this.#isDisabled()) return;
419
453
  if (!this.#dialog) {
420
454
  this.#createDialog();
421
455
  }
422
456
 
423
457
  this.#valueAtOpen = JSON.stringify(this.value);
458
+ this.#lastChangeValue = this.#valueAtOpen;
424
459
  this.#switchTab(this.#fillType, { emit: false });
425
460
 
426
461
  if (this.#swatch) this.#swatch.setAttribute("selected", "true");
427
462
 
428
463
  this.#dialog.open = true;
464
+ this.#syncTriggerA11y();
429
465
 
430
- requestAnimationFrame(() => {
431
- requestAnimationFrame(() => {
466
+ this.#scheduleFrame(() => {
467
+ const focusTarget = this.#dialog?.querySelector(
468
+ ".fig-fill-picker-close, .fig-fill-picker-type, fig-button, button, input, [tabindex='0']",
469
+ );
470
+ focusTarget?.focus?.();
471
+ this.#scheduleFrame(() => {
432
472
  this.#drawColorArea();
433
473
  this.#updateHandlePosition();
434
474
  });
@@ -440,7 +480,10 @@ class FigFillPicker extends HTMLElement {
440
480
  }
441
481
 
442
482
  close() {
443
- if (this.#dialog) this.#dialog.open = false;
483
+ if (this.#dialog) {
484
+ this.#dialog.open = false;
485
+ this.#syncTriggerA11y();
486
+ }
444
487
  }
445
488
 
446
489
  #restoreCustomSlotContent() {
@@ -468,6 +511,13 @@ class FigFillPicker extends HTMLElement {
468
511
  this.#dialog = null;
469
512
  this.#webcamStart = null;
470
513
  this.#valueAtOpen = null;
514
+ this.#lastChangeValue = null;
515
+ this.#colorArea = null;
516
+ this.#colorAreaHandle = null;
517
+ this.#hueSlider = null;
518
+ this.#opacitySlider = null;
519
+ this.#syncingGradientBar = false;
520
+ this.#syncTriggerA11y();
471
521
  }
472
522
 
473
523
  #stopWebcam() {
@@ -497,6 +547,8 @@ class FigFillPicker extends HTMLElement {
497
547
 
498
548
  this.#dialog = document.createElement("dialog", { is: "fig-popup" });
499
549
  this.#dialog.setAttribute("is", "fig-popup");
550
+ this.#dialog.id = this.#dialogId;
551
+ this.#dialog.setAttribute("aria-label", this.#triggerLabel());
500
552
  this.#dialog.setAttribute("drag", "true");
501
553
  this.#dialog.setAttribute("handle", "fig-header");
502
554
  this.#dialog.setAttribute("autoresize", "false");
@@ -619,13 +671,19 @@ class FigFillPicker extends HTMLElement {
619
671
  const onDialogClose = () => {
620
672
  if (this.#swatch) this.#swatch.removeAttribute("selected");
621
673
  this.#stopWebcam();
622
- if (
623
- this.#valueAtOpen !== null &&
624
- this.#valueAtOpen !== JSON.stringify(this.value)
625
- ) {
674
+ const closingValue = JSON.stringify(this.value);
675
+ if (this.#lastChangeValue !== null && this.#lastChangeValue !== closingValue) {
626
676
  this.#emitChange();
627
677
  }
628
678
  this.#valueAtOpen = null;
679
+ this.#lastChangeValue = null;
680
+ this.#syncTriggerA11y();
681
+ const returnTarget = this.#trigger;
682
+ this.#scheduleFrame(() => {
683
+ if (returnTarget?.isConnected) {
684
+ HTMLElement.prototype.focus.call(returnTarget);
685
+ }
686
+ });
629
687
  this.dispatchEvent(new CustomEvent("close"));
630
688
  };
631
689
  this.#dialog.addEventListener("close", onDialogClose);
@@ -703,11 +761,11 @@ class FigFillPicker extends HTMLElement {
703
761
  // Update tab-specific UI after visibility change
704
762
  if (tabName === "gradient") {
705
763
  // Use RAF to ensure layout is complete before updating angle input
706
- requestAnimationFrame(() => {
764
+ this.#scheduleFrame(() => {
707
765
  this.#updateGradientUI();
708
766
  const barInput = tab.querySelector(".fig-fill-picker-gradient-bar-input");
709
767
  barInput?.refreshLayout?.();
710
- requestAnimationFrame(() => {
768
+ this.#scheduleFrame(() => {
711
769
  barInput?.refreshLayout?.();
712
770
  });
713
771
  });
@@ -719,6 +777,40 @@ class FigFillPicker extends HTMLElement {
719
777
  if (emit) this.#emitInput();
720
778
  }
721
779
 
780
+ #refreshDialogUI() {
781
+ if (!this.#dialog?.open) return;
782
+ this.#switchTab(this.#fillType, { emit: false });
783
+
784
+ this.#drawColorArea();
785
+ this.#updateHandlePosition();
786
+ this.#updateColorInputs();
787
+ if (this.#hueSlider) this.#hueSlider.setAttribute("value", this.#color.h);
788
+ if (this.#opacitySlider) {
789
+ this.#opacitySlider.setAttribute("value", this.#color.a * 100);
790
+ this.#opacitySlider.setAttribute("color", this.#hsvToHex(this.#color));
791
+ }
792
+
793
+ this.#updateGradientUI();
794
+
795
+ const imageTab = this.#dialog.querySelector('[data-tab="image"]');
796
+ const imageMode = imageTab?.querySelector(".fig-fill-picker-scale-mode");
797
+ const imageScale = imageTab?.querySelector(".fig-fill-picker-scale");
798
+ const imagePreview = imageTab?.querySelector(".fig-fill-picker-image-preview");
799
+ if (imageMode) imageMode.value = this.#image.scaleMode;
800
+ if (imageScale) {
801
+ imageScale.setAttribute("value", this.#image.scale);
802
+ imageScale.style.display =
803
+ this.#image.scaleMode === "tile" ? "block" : "none";
804
+ }
805
+ if (imagePreview) this.#updateImagePreview(imagePreview);
806
+
807
+ const videoTab = this.#dialog.querySelector('[data-tab="video"]');
808
+ const videoMode = videoTab?.querySelector(".fig-fill-picker-scale-mode");
809
+ const videoPreview = videoTab?.querySelector(".fig-fill-picker-video-preview");
810
+ if (videoMode) videoMode.value = this.#video.scaleMode;
811
+ if (videoPreview) this.#updateVideoPreviewStyle(videoPreview);
812
+ }
813
+
722
814
  // ============ SOLID TAB ============
723
815
  #initSolidTab() {
724
816
  const container = this.#dialog.querySelector('[data-tab="solid"]');
@@ -729,6 +821,9 @@ class FigFillPicker extends HTMLElement {
729
821
  <canvas width="200" height="200"></canvas>
730
822
  <fig-handle
731
823
  aria-label="Color saturation and brightness"
824
+ role="slider"
825
+ aria-valuemin="0"
826
+ aria-valuemax="100"
732
827
  type="color"
733
828
  color="${this.#hsvToHex({ ...this.#color, a: 1 })}"
734
829
  data-no-color-picker
@@ -770,6 +865,7 @@ class FigFillPicker extends HTMLElement {
770
865
  // Setup color area
771
866
  this.#colorArea = container.querySelector("canvas");
772
867
  this.#colorAreaHandle = container.querySelector("fig-handle");
868
+ this.#syncColorAreaA11y();
773
869
  this.#drawColorArea();
774
870
  this.#updateHandlePosition();
775
871
  this.#setupColorAreaEvents();
@@ -900,12 +996,13 @@ class FigFillPicker extends HTMLElement {
900
996
 
901
997
  #updateHandlePosition(retryCount = 0) {
902
998
  if (!this.#colorAreaHandle || !this.#colorArea) return;
999
+ this.#syncColorAreaA11y();
903
1000
 
904
1001
  const rect = this.#colorArea.getBoundingClientRect();
905
1002
 
906
1003
  // If the canvas isn't visible yet (0 dimensions), schedule a retry (max 5 attempts)
907
1004
  if ((rect.width === 0 || rect.height === 0) && retryCount < 5) {
908
- requestAnimationFrame(() => this.#updateHandlePosition(retryCount + 1));
1005
+ this.#scheduleFrame(() => this.#updateHandlePosition(retryCount + 1));
909
1006
  return;
910
1007
  }
911
1008
 
@@ -919,6 +1016,19 @@ class FigFillPicker extends HTMLElement {
919
1016
  );
920
1017
  }
921
1018
 
1019
+ #syncColorAreaA11y() {
1020
+ if (!this.#colorAreaHandle) return;
1021
+ this.#colorAreaHandle.setAttribute(
1022
+ "aria-valuenow",
1023
+ String(Math.round(this.#color.v)),
1024
+ );
1025
+ this.#colorAreaHandle.setAttribute(
1026
+ "aria-valuetext",
1027
+ `Saturation ${Math.round(this.#color.s)}%, brightness ${Math.round(this.#color.v)}%`,
1028
+ );
1029
+ this.#colorAreaHandle.removeAttribute("aria-pressed");
1030
+ }
1031
+
922
1032
  #updateColorFromAreaPosition(x, y, opts = {}) {
923
1033
  const { updateHandle = true, emitInput = true, emitChange = false } = opts;
924
1034
  this.#color.s = Math.max(0, Math.min(100, x * 100));
@@ -928,6 +1038,7 @@ class FigFillPicker extends HTMLElement {
928
1038
  "color",
929
1039
  this.#hsvToHex({ ...this.#color, a: 1 }),
930
1040
  );
1041
+ this.#syncColorAreaA11y();
931
1042
  }
932
1043
  if (updateHandle) this.#updateHandlePosition();
933
1044
  this.#updateColorInputs();
@@ -1110,7 +1221,7 @@ class FigFillPicker extends HTMLElement {
1110
1221
 
1111
1222
  container.innerHTML = html;
1112
1223
  this.#wireColorInputEvents();
1113
- requestAnimationFrame(() => this.#updateColorInputs());
1224
+ this.#scheduleFrame(() => this.#updateColorInputs());
1114
1225
  }
1115
1226
 
1116
1227
  #wireColorInputEvents() {
@@ -1919,7 +2030,7 @@ class FigFillPicker extends HTMLElement {
1919
2030
  if (stopFillPicker) {
1920
2031
  stopFillPicker.anchorElement = this.#dialog;
1921
2032
  } else {
1922
- requestAnimationFrame(() => {
2033
+ this.#scheduleFrame(() => {
1923
2034
  const fp = stopColor.querySelector("fig-fill-picker");
1924
2035
  if (fp) fp.anchorElement = this.#dialog;
1925
2036
  });
@@ -2024,9 +2135,6 @@ class FigFillPicker extends HTMLElement {
2024
2135
  }" units="%" ${
2025
2136
  this.#image.scaleMode === "tile" ? "" : 'style="display: none;"'
2026
2137
  }></fig-input-number>
2027
- <fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
2028
- <fig-icon name="rotate"></fig-icon>
2029
- </fig-button>
2030
2138
  </fig-field>
2031
2139
  <fig-image class="fig-fill-picker-media-preview fig-fill-picker-image-preview" upload="true" label="Upload from computer" alt="Image fill preview" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true"></fig-image>
2032
2140
  `;
@@ -2187,9 +2295,6 @@ class FigFillPicker extends HTMLElement {
2187
2295
  <fig-select-option value="crop">Crop</fig-select-option>
2188
2296
  </fig-select-options>
2189
2297
  </fig-select>
2190
- <fig-button class="fig-fill-picker-media-rotate" icon variant="ghost" aria-label="Rotate">
2191
- <fig-icon name="rotate"></fig-icon>
2192
- </fig-button>
2193
2298
  </fig-field>
2194
2299
  <fig-media class="fig-fill-picker-media-preview fig-fill-picker-video-preview" type="video" upload="true" label="Upload from computer" aria-label="Video fill preview" size="auto" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" controls muted="true" loop="true"></fig-media>
2195
2300
  `;
@@ -2246,12 +2351,12 @@ class FigFillPicker extends HTMLElement {
2246
2351
  </fig-field>
2247
2352
  <fig-video class="fig-fill-picker-webcam-preview" aria-label="Webcam preview" aspect-ratio="1/1" fit="cover" checkerboard="true" autoplay="true" muted="true">
2248
2353
  <video class="fig-fill-picker-webcam-video" autoplay muted playsinline></video>
2249
- <div class="fig-fill-picker-webcam-status">
2354
+ <div class="fig-fill-picker-webcam-status" role="status" aria-live="polite">
2250
2355
  <span>Camera access required</span>
2251
2356
  </div>
2252
2357
  </fig-video>
2253
2358
  <div class="fig-fill-picker-webcam-controls">
2254
- <fig-button class="fig-fill-picker-webcam-capture" variant="secondary" full>
2359
+ <fig-button class="fig-fill-picker-webcam-capture" variant="secondary" full disabled>
2255
2360
  Capture
2256
2361
  </fig-button>
2257
2362
  </div>
@@ -2272,10 +2377,31 @@ class FigFillPicker extends HTMLElement {
2272
2377
  const cameraSelect = container.querySelector(
2273
2378
  ".fig-fill-picker-camera-select",
2274
2379
  );
2380
+ const setCaptureReady = (ready) => {
2381
+ if (ready) captureBtn.removeAttribute("disabled");
2382
+ else captureBtn.setAttribute("disabled", "");
2383
+ };
2384
+ const updateFrameReadiness = () => {
2385
+ const ready =
2386
+ !!this.#webcam.stream &&
2387
+ video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA &&
2388
+ video.videoWidth > 0 &&
2389
+ video.videoHeight > 0;
2390
+ setCaptureReady(ready);
2391
+ if (ready) {
2392
+ status.querySelector("span").textContent = "Camera ready";
2393
+ status.style.display = "none";
2394
+ }
2395
+ };
2396
+ video.addEventListener("loadedmetadata", updateFrameReadiness);
2397
+ video.addEventListener("canplay", updateFrameReadiness);
2275
2398
 
2276
2399
  const startWebcam = async (deviceId = null) => {
2277
2400
  this.#stopWebcam();
2278
2401
  const requestId = this.#webcamRequestId;
2402
+ setCaptureReady(false);
2403
+ status.querySelector("span").textContent = "Starting camera";
2404
+ status.style.display = "flex";
2279
2405
  try {
2280
2406
  const constraints = {
2281
2407
  video: deviceId ? { deviceId: { exact: deviceId } } : true,
@@ -2294,7 +2420,7 @@ class FigFillPicker extends HTMLElement {
2294
2420
  this.#webcam.stream = stream;
2295
2421
  video.srcObject = stream;
2296
2422
  video.style.display = "block";
2297
- status.style.display = "none";
2423
+ updateFrameReadiness();
2298
2424
 
2299
2425
  // Enumerate cameras
2300
2426
  const devices = await navigator.mediaDevices.enumerateDevices();
@@ -2353,6 +2479,7 @@ class FigFillPicker extends HTMLElement {
2353
2479
  status.appendChild(messageElement);
2354
2480
  status.style.display = "flex";
2355
2481
  video.style.display = "none";
2482
+ setCaptureReady(false);
2356
2483
  }
2357
2484
  };
2358
2485
  this.#webcamStart = startWebcam;
@@ -2379,8 +2506,10 @@ class FigFillPicker extends HTMLElement {
2379
2506
 
2380
2507
  if (this.#webcam.snapshot?.startsWith("blob:")) {
2381
2508
  URL.revokeObjectURL(this.#webcam.snapshot);
2509
+ this.#ownedBlobUrls.delete(this.#webcam.snapshot);
2382
2510
  }
2383
2511
  this.#webcam.snapshot = URL.createObjectURL(blob);
2512
+ this.#ownedBlobUrls.add(this.#webcam.snapshot);
2384
2513
  this.#image.url = this.#webcam.snapshot;
2385
2514
 
2386
2515
  const imagePreview = this.#dialog.querySelector(
@@ -2707,6 +2836,7 @@ class FigFillPicker extends HTMLElement {
2707
2836
 
2708
2837
  // ============ EVENT EMITTERS ============
2709
2838
  #emitInput() {
2839
+ if (this.#isDisabled()) return;
2710
2840
  this.#updateSwatch();
2711
2841
  this.dispatchEvent(
2712
2842
  new CustomEvent("input", {
@@ -2717,6 +2847,8 @@ class FigFillPicker extends HTMLElement {
2717
2847
  }
2718
2848
 
2719
2849
  #emitChange() {
2850
+ if (this.#isDisabled()) return;
2851
+ this.#lastChangeValue = JSON.stringify(this.value);
2720
2852
  this.dispatchEvent(
2721
2853
  new CustomEvent("change", {
2722
2854
  bubbles: true,
@@ -2778,33 +2910,17 @@ class FigFillPicker extends HTMLElement {
2778
2910
  case "value":
2779
2911
  this.#parseValue();
2780
2912
  this.#updateSwatch();
2781
- if (this.#dialog) {
2782
- // Update dialog UI if open - but don't rebuild if user is dragging
2783
- if (!this.#isDraggingColor) {
2784
- // Just update the handle position and color inputs without rebuilding
2785
- this.#updateHandlePosition();
2786
- this.#updateColorInputs();
2787
- // Update hue slider
2788
- if (this.#hueSlider) {
2789
- this.#hueSlider.setAttribute("value", this.#color.h);
2790
- }
2791
- // Update opacity slider
2792
- if (this.#opacitySlider) {
2793
- this.#opacitySlider.setAttribute("value", this.#color.a * 100);
2794
- this.#opacitySlider.setAttribute(
2795
- "color",
2796
- this.#hsvToHex(this.#color),
2797
- );
2798
- }
2799
- }
2913
+ if (this.#dialog?.open && !this.#isDraggingColor) {
2914
+ this.#refreshDialogUI();
2915
+ this.#lastChangeValue = JSON.stringify(this.value);
2800
2916
  }
2801
2917
  break;
2802
2918
  case "disabled":
2803
2919
  this.#syncTriggerA11y();
2920
+ if (this.#isDisabled() && this.#dialog?.open) this.close();
2804
2921
  break;
2805
2922
  case "alpha":
2806
- case "mode":
2807
- case "experimental": {
2923
+ case "mode": {
2808
2924
  if (!this.#dialog) break;
2809
2925
  const wasOpen = this.#dialog.open;
2810
2926
  this.#discardDialog();
@@ -2812,6 +2928,9 @@ class FigFillPicker extends HTMLElement {
2812
2928
  break;
2813
2929
  }
2814
2930
  case "aria-label":
2931
+ if (this.#dialog) {
2932
+ this.#dialog.setAttribute("aria-label", this.#triggerLabel());
2933
+ }
2815
2934
  case "aria-labelledby":
2816
2935
  case "aria-describedby":
2817
2936
  this.#syncTriggerA11y();