@rogieking/figui3 6.15.0 → 6.16.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-lab.js CHANGED
@@ -22,6 +22,7 @@ class PropskitSwitch extends HTMLElement {
22
22
  #managedSwitchAttrs = new Set();
23
23
  #boundHandleInput = null;
24
24
  #boundHandleChange = null;
25
+ #boundHandleClick = this.#handleClick.bind(this);
25
26
 
26
27
  static get observedAttributes() {
27
28
  return ["label", "direction"];
@@ -32,6 +33,8 @@ class PropskitSwitch extends HTMLElement {
32
33
  this.#syncField();
33
34
  this.#syncSwitchAttributes();
34
35
  this.#bindSwitchEvents();
36
+ this.removeEventListener("click", this.#boundHandleClick);
37
+ this.addEventListener("click", this.#boundHandleClick);
35
38
 
36
39
  if (!this.#observer) {
37
40
  this.#observer = new MutationObserver((mutations) => {
@@ -61,6 +64,7 @@ class PropskitSwitch extends HTMLElement {
61
64
  disconnectedCallback() {
62
65
  this.#observer?.disconnect();
63
66
  this.#unbindSwitchEvents();
67
+ this.removeEventListener("click", this.#boundHandleClick);
64
68
  }
65
69
 
66
70
  attributeChangedCallback(name, oldValue, newValue) {
@@ -193,6 +197,14 @@ class PropskitSwitch extends HTMLElement {
193
197
  );
194
198
  }
195
199
 
200
+ #handleClick(event) {
201
+ if (event.target instanceof Element && event.target.closest("fig-segmented-control")) {
202
+ return;
203
+ }
204
+ const value = this.#switch?.value === "on" ? "off" : "on";
205
+ this.#switch?.querySelector(`fig-segment[value="${value}"]`)?.click();
206
+ }
207
+
196
208
  get checked() {
197
209
  return this.#switch
198
210
  ? this.#switch.value === "on"
@@ -220,6 +232,216 @@ class PropskitSwitch extends HTMLElement {
220
232
  }
221
233
  customElements.define("propskit-switch", PropskitSwitch);
222
234
 
235
+ /* Field + Color wrapper */
236
+ class PropskitColor extends HTMLElement {
237
+ #field = null;
238
+ #label = null;
239
+ #input = null;
240
+ #hasCustomLabel = false;
241
+ #observer = null;
242
+ #managedInputAttrs = new Set();
243
+ #boundHandleInput = null;
244
+ #boundHandleChange = null;
245
+ #boundHandleClick = this.#handleClick.bind(this);
246
+
247
+ static get observedAttributes() {
248
+ return ["label", "direction", "aria-label"];
249
+ }
250
+
251
+ connectedCallback() {
252
+ if (!this.#field) this.#initialize();
253
+ this.#syncField();
254
+ this.#syncInputAttributes();
255
+ this.#bindInputEvents();
256
+ this.removeEventListener("click", this.#boundHandleClick);
257
+ this.addEventListener("click", this.#boundHandleClick);
258
+
259
+ if (!this.#observer) {
260
+ this.#observer = new MutationObserver((mutations) => {
261
+ let syncField = false;
262
+ let syncInput = false;
263
+
264
+ for (const mutation of mutations) {
265
+ if (mutation.type !== "attributes") continue;
266
+ if (
267
+ mutation.attributeName === "label" ||
268
+ mutation.attributeName === "direction" ||
269
+ mutation.attributeName === "aria-label"
270
+ ) {
271
+ syncField = true;
272
+ } else {
273
+ syncInput = true;
274
+ }
275
+ }
276
+
277
+ if (syncField) this.#syncField();
278
+ if (syncInput) this.#syncInputAttributes();
279
+ });
280
+ }
281
+
282
+ this.#observer.observe(this, { attributes: true });
283
+ }
284
+
285
+ disconnectedCallback() {
286
+ this.#observer?.disconnect();
287
+ this.#unbindInputEvents();
288
+ this.removeEventListener("click", this.#boundHandleClick);
289
+ }
290
+
291
+ attributeChangedCallback(name, oldValue, newValue) {
292
+ if (oldValue === newValue || !this.#field) return;
293
+ if (name === "label" || name === "direction" || name === "aria-label") {
294
+ this.#syncField();
295
+ }
296
+ }
297
+
298
+ #initialize() {
299
+ const initialChildren = Array.from(this.childNodes).filter(
300
+ (node) =>
301
+ node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()),
302
+ );
303
+ const customLabel = initialChildren.find(
304
+ (node) => node.nodeType === Node.ELEMENT_NODE && node.matches("label"),
305
+ );
306
+ const field = document.createElement("fig-field");
307
+ const label = customLabel || document.createElement("label");
308
+ const input = document.createElement("fig-input-color");
309
+
310
+ for (const node of initialChildren) {
311
+ if (node !== customLabel) input.appendChild(node);
312
+ }
313
+ field.append(label, input);
314
+ this.#field = field;
315
+ this.#label = label;
316
+ this.#input = input;
317
+ this.#hasCustomLabel = Boolean(customLabel);
318
+ this.replaceChildren(field);
319
+ }
320
+
321
+ #syncField() {
322
+ if (!this.#field || !this.#label || !this.#input) return;
323
+ const hasLabelAttr = this.hasAttribute("label");
324
+ const rawLabel = this.getAttribute("label");
325
+ const isBlankLabel = hasLabelAttr && (rawLabel ?? "").trim() === "";
326
+
327
+ if (isBlankLabel) {
328
+ this.#label.remove();
329
+ } else {
330
+ if (!this.#hasCustomLabel) {
331
+ this.#label.textContent = hasLabelAttr ? (rawLabel ?? "") : "Label";
332
+ }
333
+ if (this.#label.parentElement !== this.#field) {
334
+ this.#field.prepend(this.#label);
335
+ }
336
+ }
337
+
338
+ this.#field.setAttribute(
339
+ "direction",
340
+ this.getAttribute("direction") || "horizontal",
341
+ );
342
+ this.#input.setAttribute(
343
+ "aria-label",
344
+ this.getAttribute("aria-label") ||
345
+ this.#label.textContent?.trim() ||
346
+ "Color",
347
+ );
348
+ }
349
+
350
+ #getForwardedInputAttrNames() {
351
+ const reserved = new Set([
352
+ "label",
353
+ "direction",
354
+ "oninput",
355
+ "onchange",
356
+ "class",
357
+ "style",
358
+ "id",
359
+ "size",
360
+ "aria-label",
361
+ "text",
362
+ ]);
363
+ return this.getAttributeNames().filter(
364
+ (name) => !reserved.has(name) && !name.startsWith("data-"),
365
+ );
366
+ }
367
+
368
+ #syncInputAttributes() {
369
+ if (!this.#input) return;
370
+ const inputAttrs = this.#getForwardedInputAttrNames();
371
+ const nextManaged = new Set(inputAttrs);
372
+
373
+ for (const attrName of this.#managedInputAttrs) {
374
+ if (!nextManaged.has(attrName)) this.#input.removeAttribute(attrName);
375
+ }
376
+ for (const attrName of inputAttrs) {
377
+ this.#input.setAttribute(attrName, this.getAttribute(attrName) ?? "");
378
+ }
379
+
380
+ this.#input.setAttribute("text", "true");
381
+ this.#managedInputAttrs = nextManaged;
382
+ }
383
+
384
+ #bindInputEvents() {
385
+ if (!this.#input) return;
386
+ this.#boundHandleInput ??= this.#forwardInputEvent.bind(this, "input");
387
+ this.#boundHandleChange ??= this.#forwardInputEvent.bind(this, "change");
388
+ this.#input.addEventListener("input", this.#boundHandleInput);
389
+ this.#input.addEventListener("change", this.#boundHandleChange);
390
+ }
391
+
392
+ #unbindInputEvents() {
393
+ if (!this.#input) return;
394
+ if (this.#boundHandleInput) {
395
+ this.#input.removeEventListener("input", this.#boundHandleInput);
396
+ }
397
+ if (this.#boundHandleChange) {
398
+ this.#input.removeEventListener("change", this.#boundHandleChange);
399
+ }
400
+ }
401
+
402
+ #forwardInputEvent(type, event) {
403
+ event.stopImmediatePropagation();
404
+ const value = this.#input?.getAttribute("value") ?? "";
405
+ this.setAttribute("value", value);
406
+ const detail =
407
+ event instanceof CustomEvent && event.detail !== undefined
408
+ ? event.detail
409
+ : value;
410
+ this.dispatchEvent(
411
+ new CustomEvent(type, {
412
+ detail,
413
+ bubbles: true,
414
+ cancelable: true,
415
+ composed: true,
416
+ }),
417
+ );
418
+ }
419
+
420
+ #handleClick(event) {
421
+ if (event.target instanceof Element && event.target.closest("fig-input-color")) {
422
+ return;
423
+ }
424
+ this.focus();
425
+ }
426
+
427
+ get value() {
428
+ return this.#input?.getAttribute("value") ?? this.getAttribute("value") ?? "";
429
+ }
430
+
431
+ set value(nextValue) {
432
+ if (nextValue === null || nextValue === undefined || nextValue === "") {
433
+ this.removeAttribute("value");
434
+ } else {
435
+ this.setAttribute("value", String(nextValue));
436
+ }
437
+ }
438
+
439
+ focus(options) {
440
+ this.#input?.querySelector("input:not([tabindex='-1'])")?.focus(options);
441
+ }
442
+ }
443
+ customElements.define("propskit-color", PropskitColor);
444
+
223
445
  /* Field + Select wrapper */
224
446
  class PropskitSelect extends HTMLElement {
225
447
  #field = null;
@@ -230,6 +452,7 @@ class PropskitSelect extends HTMLElement {
230
452
  #managedSelectAttrs = new Set();
231
453
  #boundHandleInput = null;
232
454
  #boundHandleChange = null;
455
+ #boundHandleClick = this.#handleClick.bind(this);
233
456
 
234
457
  static get observedAttributes() {
235
458
  return ["label", "direction", "aria-label"];
@@ -240,6 +463,8 @@ class PropskitSelect extends HTMLElement {
240
463
  this.#syncField();
241
464
  this.#syncSelectAttributes();
242
465
  this.#bindSelectEvents();
466
+ this.removeEventListener("click", this.#boundHandleClick);
467
+ this.addEventListener("click", this.#boundHandleClick);
243
468
 
244
469
  if (!this.#observer) {
245
470
  this.#observer = new MutationObserver((mutations) => {
@@ -270,6 +495,7 @@ class PropskitSelect extends HTMLElement {
270
495
  disconnectedCallback() {
271
496
  this.#observer?.disconnect();
272
497
  this.#unbindSelectEvents();
498
+ this.removeEventListener("click", this.#boundHandleClick);
273
499
  }
274
500
 
275
501
  attributeChangedCallback(name, oldValue, newValue) {
@@ -399,6 +625,21 @@ class PropskitSelect extends HTMLElement {
399
625
  );
400
626
  }
401
627
 
628
+ #handleClick(event) {
629
+ if (event.target instanceof Element && event.target.closest("fig-dropdown")) {
630
+ return;
631
+ }
632
+ const select = this.#select?.querySelector("select");
633
+ select?.focus();
634
+ if (typeof select?.showPicker === "function") {
635
+ try {
636
+ select.showPicker();
637
+ } catch {
638
+ // Browser may reject showPicker when no user activation is available.
639
+ }
640
+ }
641
+ }
642
+
402
643
  get value() {
403
644
  return this.#select?.value ?? this.getAttribute("value") ?? "";
404
645
  }
@@ -427,6 +668,7 @@ class PropskitText extends HTMLElement {
427
668
  #managedInputAttrs = new Set();
428
669
  #boundHandleInput = null;
429
670
  #boundHandleChange = null;
671
+ #boundHandleClick = this.#handleClick.bind(this);
430
672
 
431
673
  static get observedAttributes() {
432
674
  return ["label", "direction", "aria-label"];
@@ -437,6 +679,8 @@ class PropskitText extends HTMLElement {
437
679
  this.#syncField();
438
680
  this.#syncInputAttributes();
439
681
  this.#bindInputEvents();
682
+ this.removeEventListener("click", this.#boundHandleClick);
683
+ this.addEventListener("click", this.#boundHandleClick);
440
684
 
441
685
  if (!this.#observer) {
442
686
  this.#observer = new MutationObserver((mutations) => {
@@ -467,6 +711,7 @@ class PropskitText extends HTMLElement {
467
711
  disconnectedCallback() {
468
712
  this.#observer?.disconnect();
469
713
  this.#unbindInputEvents();
714
+ this.removeEventListener("click", this.#boundHandleClick);
470
715
  }
471
716
 
472
717
  attributeChangedCallback(name, oldValue, newValue) {
@@ -598,6 +843,13 @@ class PropskitText extends HTMLElement {
598
843
  );
599
844
  }
600
845
 
846
+ #handleClick(event) {
847
+ if (event.target instanceof Element && event.target.closest("fig-input-text")) {
848
+ return;
849
+ }
850
+ this.focus();
851
+ }
852
+
601
853
  get value() {
602
854
  return this.#input?.value ?? this.getAttribute("value") ?? "";
603
855
  }
@@ -626,6 +878,7 @@ class PropskitNumber extends HTMLElement {
626
878
  #managedInputAttrs = new Set();
627
879
  #boundHandleInput = null;
628
880
  #boundHandleChange = null;
881
+ #boundHandleClick = this.#handleClick.bind(this);
629
882
 
630
883
  static get observedAttributes() {
631
884
  return ["label", "direction"];
@@ -636,6 +889,8 @@ class PropskitNumber extends HTMLElement {
636
889
  this.#syncField();
637
890
  this.#syncInputAttributes();
638
891
  this.#bindInputEvents();
892
+ this.removeEventListener("click", this.#boundHandleClick);
893
+ this.addEventListener("click", this.#boundHandleClick);
639
894
 
640
895
  if (!this.#observer) {
641
896
  this.#observer = new MutationObserver((mutations) => {
@@ -665,6 +920,7 @@ class PropskitNumber extends HTMLElement {
665
920
  disconnectedCallback() {
666
921
  this.#observer?.disconnect();
667
922
  this.#unbindInputEvents();
923
+ this.removeEventListener("click", this.#boundHandleClick);
668
924
  }
669
925
 
670
926
  attributeChangedCallback(name, oldValue, newValue) {
@@ -786,6 +1042,13 @@ class PropskitNumber extends HTMLElement {
786
1042
  );
787
1043
  }
788
1044
 
1045
+ #handleClick(event) {
1046
+ if (event.target instanceof Element && event.target.closest("fig-input-number")) {
1047
+ return;
1048
+ }
1049
+ this.focus();
1050
+ }
1051
+
789
1052
  get value() {
790
1053
  return this.#input?.value ?? this.getAttribute("value") ?? "";
791
1054
  }
@@ -4051,6 +4314,7 @@ class FigReorder extends HTMLElement {
4051
4314
  "fig-slider",
4052
4315
  "fig-input-number",
4053
4316
  "fig-input-text",
4317
+ "propskit-color",
4054
4318
  "propskit-number",
4055
4319
  "propskit-select",
4056
4320
  "propskit-slider",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.15.0",
3
+ "version": "6.16.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",