@rogieking/figui3 6.6.0 → 6.6.2

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 (3) hide show
  1. package/dist/fig.js +26 -26
  2. package/fig.js +588 -213
  3. package/package.json +1 -1
package/fig.js CHANGED
@@ -27,6 +27,14 @@ function createFigIcon(name, options = {}) {
27
27
  return icon;
28
28
  }
29
29
 
30
+ /** Run callback on the next frame; skip if the host disconnected first. */
31
+ function figNextFrame(host, callback) {
32
+ requestAnimationFrame(() => {
33
+ if (host && !host.isConnected) return;
34
+ callback();
35
+ });
36
+ }
37
+
30
38
  function createFigOverflowButtons({
31
39
  owner,
32
40
  onStart,
@@ -202,16 +210,26 @@ class FigButton extends HTMLElement {
202
210
  #selected;
203
211
  #a11yAttributes = ["aria-label", "aria-labelledby", "aria-describedby", "title"];
204
212
  #boundHandleControlKeydown = this.#handleControlKeydown.bind(this);
213
+ #boundHandleClick = this.#handleClick.bind(this);
214
+ #boundHandleFocus = () => {
215
+ if (this.button?.matches(":focus-visible")) {
216
+ this.setAttribute("data-focus-visible", "");
217
+ }
218
+ };
219
+ #boundHandleBlur = () => {
220
+ this.removeAttribute("data-focus-visible");
221
+ };
205
222
  constructor() {
206
223
  super();
207
224
  this.attachShadow({ mode: "open", delegatesFocus: true });
208
225
  }
209
226
  connectedCallback() {
210
227
  this.type = this.getAttribute("type") || "button";
211
- const isControlWrapper = this.type === "select" || this.type === "upload";
212
- const controlTag = isControlWrapper ? "span" : "button";
213
- const typeAttr = isControlWrapper ? "" : ` type="${this.type}"`;
214
- this.shadowRoot.innerHTML = `
228
+ if (!this.button) {
229
+ const isControlWrapper = this.type === "select" || this.type === "upload";
230
+ const controlTag = isControlWrapper ? "span" : "button";
231
+ const typeAttr = isControlWrapper ? "" : ` type="${this.type}"`;
232
+ this.shadowRoot.innerHTML = `
215
233
  <style>
216
234
  button, button:hover, button:active, .fig-button-control {
217
235
  padding: 0 var(--spacer-2);
@@ -249,24 +267,18 @@ class FigButton extends HTMLElement {
249
267
  </${controlTag}>
250
268
  `;
251
269
 
270
+ this.button = this.shadowRoot.querySelector("button, .fig-button-control");
271
+ this.button.addEventListener("click", this.#boundHandleClick);
272
+ this.button.addEventListener("focus", this.#boundHandleFocus);
273
+ this.button.addEventListener("blur", this.#boundHandleBlur);
274
+ this.addEventListener("keydown", this.#boundHandleControlKeydown);
275
+ }
276
+
252
277
  this.#selected =
253
278
  this.hasAttribute("selected") &&
254
279
  this.getAttribute("selected") !== "false";
255
280
 
256
- this.button = this.shadowRoot.querySelector("button, .fig-button-control");
257
281
  this.#syncButtonAttributes();
258
- this.button.addEventListener("click", this.#handleClick.bind(this));
259
-
260
- // Forward focus-visible state to host element
261
- this.button.addEventListener("focus", () => {
262
- if (this.button.matches(":focus-visible")) {
263
- this.setAttribute("data-focus-visible", "");
264
- }
265
- });
266
- this.button.addEventListener("blur", () => {
267
- this.removeAttribute("data-focus-visible");
268
- });
269
- this.addEventListener("keydown", this.#boundHandleControlKeydown);
270
282
  }
271
283
 
272
284
  get type() {
@@ -390,8 +402,17 @@ class FigButton extends HTMLElement {
390
402
  attributeChangedCallback(name, oldValue, newValue) {
391
403
  if (oldValue === newValue) return;
392
404
  switch (name) {
405
+ case "type": {
406
+ const isWrapper = (type) => type === "select" || type === "upload";
407
+ if (isWrapper(oldValue || "button") !== isWrapper(newValue || "button")) {
408
+ this.button = null;
409
+ this.connectedCallback();
410
+ break;
411
+ }
412
+ this.#syncButtonAttributes();
413
+ break;
414
+ }
393
415
  case "disabled":
394
- case "type":
395
416
  this.#syncButtonAttributes();
396
417
  break;
397
418
  case "selected":
@@ -520,11 +541,19 @@ class FigDropdown extends HTMLElement {
520
541
  this.select.setAttribute("aria-label", this.#label);
521
542
  this.#syncDisabled();
522
543
 
523
- this.appendChild(this.select);
524
- this.shadowRoot.appendChild(this.optionsSlot);
544
+ if (!this.select.isConnected) {
545
+ this.appendChild(this.select);
546
+ }
547
+ if (!this.optionsSlot.isConnected) {
548
+ this.shadowRoot.appendChild(this.optionsSlot);
549
+ }
525
550
 
551
+ this.optionsSlot.removeEventListener("slotchange", this.#boundSlotChange);
526
552
  this.optionsSlot.addEventListener("slotchange", this.#boundSlotChange);
527
553
 
554
+ this.select.removeEventListener("input", this.#boundHandleSelectInput);
555
+ this.select.removeEventListener("change", this.#boundHandleSelectChange);
556
+ this.select.removeEventListener("keydown", this.#boundHandleSelectKeydown);
528
557
  this.#addEventListeners();
529
558
  }
530
559
 
@@ -1152,7 +1181,7 @@ class FigTruncate extends HTMLElement {
1152
1181
 
1153
1182
  connectedCallback() {
1154
1183
  this.#originalText = this.textContent;
1155
- requestAnimationFrame(() => {
1184
+ figNextFrame(this, () => {
1156
1185
  this.#render();
1157
1186
  this.#setupTooltip();
1158
1187
  });
@@ -1284,7 +1313,7 @@ class FigDialog extends HTMLDialogElement {
1284
1313
 
1285
1314
  this._ensureHeader();
1286
1315
 
1287
- requestAnimationFrame(() => {
1316
+ figNextFrame(this, () => {
1288
1317
  this._addCloseListeners();
1289
1318
  this._setupDragListeners();
1290
1319
  this._applyPosition();
@@ -3111,7 +3140,7 @@ class FigTab extends HTMLElement {
3111
3140
  if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "-1");
3112
3141
  this.addEventListener("click", this.#boundHandleClick);
3113
3142
 
3114
- requestAnimationFrame(() => {
3143
+ figNextFrame(this, () => {
3115
3144
  if (typeof this.getAttribute("content") === "string") {
3116
3145
  this.content = document.querySelector(this.getAttribute("content"));
3117
3146
  if (this.content) {
@@ -3211,7 +3240,7 @@ class FigTabs extends HTMLElement {
3211
3240
  this.#createNavButtons();
3212
3241
  this.#startObserver();
3213
3242
  this.#startResizeObserver();
3214
- requestAnimationFrame(() => {
3243
+ figNextFrame(this, () => {
3215
3244
  const value = this.getAttribute("value");
3216
3245
  if (value) {
3217
3246
  this.#selectByValue(value);
@@ -3453,26 +3482,45 @@ class FigTabs extends HTMLElement {
3453
3482
  this.querySelectorAll("fig-tab").forEach((tab) => tab.removeAttribute("selected"));
3454
3483
  this.selectedTab = tabs[newIndex];
3455
3484
  tabs[newIndex].setAttribute("selected", "true");
3456
- const val = tabs[newIndex].getAttribute("value");
3485
+ const val = this.#resolveTabValue(tabs[newIndex]);
3457
3486
  if (val) this.setAttribute("value", val);
3487
+ else this.removeAttribute("value");
3458
3488
  tabs[newIndex].focus();
3459
3489
  this.#syncTabIndexes();
3460
3490
  this.#scrollSelectedTabIntoView(tabs[newIndex]);
3491
+ this.#emitSelectionEvents();
3461
3492
  }
3462
3493
  }
3463
3494
 
3464
3495
  get value() {
3465
- return this.selectedTab?.getAttribute("value") || "";
3496
+ return this.#resolveTabValue(this.selectedTab);
3466
3497
  }
3467
3498
 
3468
3499
  set value(val) {
3469
3500
  this.setAttribute("value", val);
3470
3501
  }
3471
3502
 
3503
+ #emitSelectionEvents() {
3504
+ const val = this.value;
3505
+ this.dispatchEvent(
3506
+ new CustomEvent("input", { detail: val, bubbles: true }),
3507
+ );
3508
+ this.dispatchEvent(
3509
+ new CustomEvent("change", { detail: val, bubbles: true }),
3510
+ );
3511
+ }
3512
+
3513
+ #resolveTabValue(tab) {
3514
+ if (!tab) return "";
3515
+ const attrValue = tab.getAttribute("value");
3516
+ if (attrValue !== null) return attrValue;
3517
+ return tab.textContent?.trim() || "";
3518
+ }
3519
+
3472
3520
  #selectByValue(value) {
3473
3521
  const tabs = this.querySelectorAll("fig-tab");
3474
3522
  for (const tab of tabs) {
3475
- if (tab.getAttribute("value") === value) {
3523
+ if (this.#resolveTabValue(tab) === value) {
3476
3524
  this.selectedTab = tab;
3477
3525
  tab.setAttribute("selected", "true");
3478
3526
  } else {
@@ -3500,6 +3548,8 @@ class FigTabs extends HTMLElement {
3500
3548
  if (this.hasAttribute("disabled")) return;
3501
3549
  const target = event.target.closest("fig-tab");
3502
3550
  if (!target || !this.contains(target)) return;
3551
+ const previousTab = this.selectedTab;
3552
+ const previousValue = this.value;
3503
3553
  const tabs = this.querySelectorAll("fig-tab");
3504
3554
  for (const tab of tabs) {
3505
3555
  if (tab === target) {
@@ -3509,10 +3559,14 @@ class FigTabs extends HTMLElement {
3509
3559
  tab.removeAttribute("selected");
3510
3560
  }
3511
3561
  }
3512
- const val = target.getAttribute("value");
3562
+ const val = this.#resolveTabValue(target);
3513
3563
  if (val) this.setAttribute("value", val);
3564
+ else this.removeAttribute("value");
3514
3565
  this.#syncTabIndexes();
3515
3566
  this.#scrollSelectedTabIntoView(target);
3567
+ if (previousTab !== target || previousValue !== this.value) {
3568
+ this.#emitSelectionEvents();
3569
+ }
3516
3570
  }
3517
3571
  }
3518
3572
  customElements.define("fig-tabs", FigTabs);
@@ -3637,7 +3691,7 @@ class FigSegmentedControl extends HTMLElement {
3637
3691
  this.#startResizeObserver();
3638
3692
 
3639
3693
  // Defer initial selection so child segments are available.
3640
- requestAnimationFrame(() => {
3694
+ figNextFrame(this, () => {
3641
3695
  this.#syncSelectionFromAttributes({ enforceFallback: true });
3642
3696
  this.#refreshResizeObserverTargets();
3643
3697
  this.#queueIndicatorSync({ forceInstant: true });
@@ -4093,13 +4147,52 @@ class FigOptions extends HTMLElement {
4093
4147
  return String(fallbackValue);
4094
4148
  }
4095
4149
 
4150
+ #canReuseControl() {
4151
+ if (this.#parsedOptions.length === 0) return false;
4152
+ const segments = this.querySelector(":scope > fig-segmented-control");
4153
+ if (segments) {
4154
+ const opts = segments.querySelectorAll("fig-segment");
4155
+ if (opts.length !== this.#parsedOptions.length) return false;
4156
+ return Array.from(opts).every(
4157
+ (seg, i) => seg.getAttribute("value") === this.#parsedOptions[i],
4158
+ );
4159
+ }
4160
+ const dropdown = this.querySelector(":scope > fig-dropdown");
4161
+ if (dropdown) {
4162
+ const opts = dropdown.querySelectorAll("option");
4163
+ return (
4164
+ opts.length === this.#parsedOptions.length &&
4165
+ Array.from(opts).every(
4166
+ (opt, i) => opt.textContent?.trim() === this.#parsedOptions[i],
4167
+ )
4168
+ );
4169
+ }
4170
+ return false;
4171
+ }
4172
+
4173
+ #reuseControl() {
4174
+ const segments = this.querySelector(":scope > fig-segmented-control");
4175
+ if (segments) {
4176
+ this.#childControl = segments;
4177
+ this.#currentMode = "segments";
4178
+ } else {
4179
+ this.#childControl = this.querySelector(":scope > fig-dropdown");
4180
+ this.#currentMode = "dropdown";
4181
+ }
4182
+ this.#syncValueToChild();
4183
+ }
4184
+
4096
4185
  connectedCallback() {
4097
4186
  this.#parseOptions();
4187
+ if (this.#canReuseControl()) {
4188
+ this.#reuseControl();
4189
+ this.#startResizeObserver();
4190
+ figNextFrame(this, () => figNextFrame(this, () => this.#checkOverflow()));
4191
+ return;
4192
+ }
4098
4193
  this.#renderSegments();
4099
4194
  this.#startResizeObserver();
4100
- requestAnimationFrame(() => {
4101
- requestAnimationFrame(() => this.#checkOverflow());
4102
- });
4195
+ figNextFrame(this, () => figNextFrame(this, () => this.#checkOverflow()));
4103
4196
  }
4104
4197
 
4105
4198
  disconnectedCallback() {
@@ -4451,7 +4544,7 @@ class FigSlider extends HTMLElement {
4451
4544
  };
4452
4545
  }
4453
4546
 
4454
- #regenerateInnerHTML() {
4547
+ #readAttributesFromMarkup() {
4455
4548
  const rawValue = this.getAttribute("value");
4456
4549
  this.type = this.getAttribute("type") || "range";
4457
4550
  this.variant = this.getAttribute("variant") || "default";
@@ -4482,6 +4575,103 @@ class FigSlider extends HTMLElement {
4482
4575
  (rawValue === null ||
4483
4576
  (typeof rawValue === "string" && rawValue.trim() === ""));
4484
4577
  this.value = this.#normalizeSliderValue(rawValue);
4578
+ }
4579
+
4580
+ #canReuseRenderedMarkup() {
4581
+ const range = this.querySelector("[type=range]");
4582
+ if (!range) return false;
4583
+ const wantsText = this.getAttribute("text") !== "false";
4584
+ return wantsText === !!this.querySelector("fig-input-number");
4585
+ }
4586
+
4587
+ #updateRenderedMarkup() {
4588
+ this.#readAttributesFromMarkup();
4589
+ if (this.color) {
4590
+ this.style.setProperty("--color", this.color);
4591
+ } else {
4592
+ this.style.removeProperty("--color");
4593
+ }
4594
+
4595
+ this.input = this.querySelector("[type=range]");
4596
+ this.inputContainer = this.querySelector(".fig-slider-input-container");
4597
+ this.input.className = this.type;
4598
+ this.input.min = String(this.min);
4599
+ this.input.max = String(this.max);
4600
+ this.input.step = String(this.step);
4601
+ this.input.value = String(this.value);
4602
+ this.input.disabled = this.disabled;
4603
+ if (this.text) this.input.setAttribute("tabindex", "-1");
4604
+ else this.input.removeAttribute("tabindex");
4605
+ this.input.setAttribute("aria-valuemin", String(this.min));
4606
+ this.input.setAttribute("aria-valuemax", String(this.max));
4607
+ this.input.setAttribute("aria-valuenow", String(this.value));
4608
+
4609
+ this.figInputNumber = this.querySelector("fig-input-number");
4610
+ if (this.figInputNumber) {
4611
+ this.figInputNumber.setAttribute("placeholder", this.placeholder);
4612
+ this.figInputNumber.setAttribute("min", String(this.min));
4613
+ this.figInputNumber.setAttribute("max", String(this.max));
4614
+ this.figInputNumber.setAttribute("transform", String(this.transform));
4615
+ this.figInputNumber.setAttribute("step", String(this.step));
4616
+ this.figInputNumber.setAttribute(
4617
+ "value",
4618
+ this.#showEmptyTextValue ? "" : String(this.value),
4619
+ );
4620
+ if (this.units) this.figInputNumber.setAttribute("units", this.units);
4621
+ else this.figInputNumber.removeAttribute("units");
4622
+ if (this.precision !== null) {
4623
+ this.figInputNumber.setAttribute("precision", String(this.precision));
4624
+ } else {
4625
+ this.figInputNumber.removeAttribute("precision");
4626
+ }
4627
+ this.figInputNumber.disabled = this.disabled;
4628
+ this.figInputNumber.toggleAttribute("disabled", this.disabled);
4629
+ }
4630
+ }
4631
+
4632
+ #bindControlListeners() {
4633
+ this.#syncInputA11yAttributes();
4634
+ this.input.removeEventListener("input", this.#boundHandleInput);
4635
+ this.input.addEventListener("input", this.#boundHandleInput);
4636
+ this.input.removeEventListener("change", this.#boundHandleChange);
4637
+ this.input.addEventListener("change", this.#boundHandleChange);
4638
+ this.input.removeEventListener("keydown", this.#boundHandleKeyDown);
4639
+ this.input.addEventListener("keydown", this.#boundHandleKeyDown);
4640
+ this.input.removeEventListener("pointerdown", this.#boundRangePointerDown);
4641
+ this.input.addEventListener("pointerdown", this.#boundRangePointerDown);
4642
+ this.input.removeEventListener("pointerup", this.#boundRangePointerUp);
4643
+ this.input.addEventListener("pointerup", this.#boundRangePointerUp);
4644
+
4645
+ if (this.default) {
4646
+ this.style.setProperty(
4647
+ "--default",
4648
+ this.#calculateNormal(this.default),
4649
+ );
4650
+ }
4651
+
4652
+ if (this.figInputNumber) {
4653
+ this.#syncTextInputA11yAttributes();
4654
+ this.figInputNumber.removeEventListener(
4655
+ "input",
4656
+ this.#boundHandleTextInput,
4657
+ );
4658
+ this.figInputNumber.addEventListener(
4659
+ "input",
4660
+ this.#boundHandleTextInput,
4661
+ );
4662
+ this.figInputNumber.removeEventListener(
4663
+ "change",
4664
+ this.#boundHandleTextChange,
4665
+ );
4666
+ this.figInputNumber.addEventListener(
4667
+ "change",
4668
+ this.#boundHandleTextChange,
4669
+ );
4670
+ }
4671
+ }
4672
+
4673
+ #regenerateInnerHTML() {
4674
+ this.#readAttributesFromMarkup();
4485
4675
 
4486
4676
  if (this.color) {
4487
4677
  this.style.setProperty("--color", this.color);
@@ -4522,24 +4712,7 @@ class FigSlider extends HTMLElement {
4522
4712
 
4523
4713
  this.input = this.querySelector("[type=range]");
4524
4714
  this.inputContainer = this.querySelector(".fig-slider-input-container");
4525
- this.#syncInputA11yAttributes();
4526
- this.input.removeEventListener("input", this.#boundHandleInput);
4527
- this.input.addEventListener("input", this.#boundHandleInput);
4528
- this.input.removeEventListener("change", this.#boundHandleChange);
4529
- this.input.addEventListener("change", this.#boundHandleChange);
4530
- this.input.removeEventListener("keydown", this.#boundHandleKeyDown);
4531
- this.input.addEventListener("keydown", this.#boundHandleKeyDown);
4532
- this.input.removeEventListener("pointerdown", this.#boundRangePointerDown);
4533
- this.input.addEventListener("pointerdown", this.#boundRangePointerDown);
4534
- this.input.removeEventListener("pointerup", this.#boundRangePointerUp);
4535
- this.input.addEventListener("pointerup", this.#boundRangePointerUp);
4536
-
4537
- if (this.default) {
4538
- this.style.setProperty(
4539
- "--default",
4540
- this.#calculateNormal(this.default),
4541
- );
4542
- }
4715
+ this.#bindControlListeners();
4543
4716
 
4544
4717
  this.datalist = this.querySelector("datalist");
4545
4718
  this.figInputNumber = this.querySelector("fig-input-number");
@@ -4578,30 +4751,16 @@ class FigSlider extends HTMLElement {
4578
4751
  defaultOption.setAttribute("default", "true");
4579
4752
  }
4580
4753
  }
4581
- if (this.figInputNumber) {
4582
- this.#syncTextInputA11yAttributes();
4583
- this.figInputNumber.removeEventListener(
4584
- "input",
4585
- this.#boundHandleTextInput,
4586
- );
4587
- this.figInputNumber.addEventListener(
4588
- "input",
4589
- this.#boundHandleTextInput,
4590
- );
4591
- this.figInputNumber.removeEventListener(
4592
- "change",
4593
- this.#boundHandleTextChange,
4594
- );
4595
- this.figInputNumber.addEventListener(
4596
- "change",
4597
- this.#boundHandleTextChange,
4598
- );
4599
- }
4600
-
4601
4754
  this.#syncValue();
4602
4755
  }
4603
4756
 
4604
4757
  connectedCallback() {
4758
+ if (this.#canReuseRenderedMarkup()) {
4759
+ this.#updateRenderedMarkup();
4760
+ this.#bindControlListeners();
4761
+ this.#syncValue();
4762
+ return;
4763
+ }
4605
4764
  this.#regenerateInnerHTML();
4606
4765
  }
4607
4766
 
@@ -5039,35 +5198,7 @@ class FigInputText extends HTMLElement {
5039
5198
  }
5040
5199
  }
5041
5200
 
5042
- let html = `<input
5043
- type="${this.type}"
5044
- ${this.name ? `name="${this.name}"` : ""}
5045
- placeholder="${this.placeholder}"
5046
- value="${
5047
- this.type === "number" ? this.#transformNumber(this.value) : this.value
5048
- }" />`;
5049
- if (this.multiline) {
5050
- html = `<textarea
5051
- placeholder="${this.placeholder}">${this.value}</textarea>`;
5052
- }
5053
-
5054
- let append = this.querySelector("[slot=append]");
5055
- let prepend = this.querySelector("[slot=prepend]");
5056
-
5057
- this.innerHTML = html;
5058
-
5059
- if (prepend) {
5060
- prepend.removeEventListener("click", this.#boundFocusControl);
5061
- prepend.addEventListener("click", this.#boundFocusControl);
5062
- this.prepend(prepend);
5063
- }
5064
- if (append) {
5065
- append.removeEventListener("click", this.#boundFocusControl);
5066
- append.addEventListener("click", this.#boundFocusControl);
5067
- this.append(append);
5068
- }
5069
-
5070
- this.input = this.querySelector("input,textarea");
5201
+ this.input = this.#ensureInputControl();
5071
5202
  this.input.readOnly = this.readonly;
5072
5203
  this.#syncInputA11yAttributes();
5073
5204
  this.#syncSearchPrefix();
@@ -5107,6 +5238,46 @@ class FigInputText extends HTMLElement {
5107
5238
  focus() {
5108
5239
  this.input.focus();
5109
5240
  }
5241
+ #ensureInputControl() {
5242
+ const wantsTextarea = this.multiline;
5243
+ const existing = this.querySelector("input,textarea");
5244
+ if (existing) {
5245
+ const matches = wantsTextarea
5246
+ ? existing.tagName === "TEXTAREA"
5247
+ : existing.tagName === "INPUT";
5248
+ if (matches) return existing;
5249
+ }
5250
+
5251
+ let html = `<input
5252
+ type="${this.type}"
5253
+ ${this.name ? `name="${this.name}"` : ""}
5254
+ placeholder="${this.placeholder}"
5255
+ value="${
5256
+ this.type === "number" ? this.#transformNumber(this.value) : this.value
5257
+ }" />`;
5258
+ if (wantsTextarea) {
5259
+ html = `<textarea
5260
+ placeholder="${this.placeholder}">${this.value}</textarea>`;
5261
+ }
5262
+
5263
+ const append = this.querySelector("[slot=append]");
5264
+ const prepend = this.querySelector("[slot=prepend]");
5265
+
5266
+ this.innerHTML = html;
5267
+
5268
+ if (prepend) {
5269
+ prepend.removeEventListener("click", this.#boundFocusControl);
5270
+ prepend.addEventListener("click", this.#boundFocusControl);
5271
+ this.prepend(prepend);
5272
+ }
5273
+ if (append) {
5274
+ append.removeEventListener("click", this.#boundFocusControl);
5275
+ append.addEventListener("click", this.#boundFocusControl);
5276
+ this.append(append);
5277
+ }
5278
+
5279
+ return this.querySelector("input,textarea");
5280
+ }
5110
5281
  #syncInputA11yAttributes() {
5111
5282
  if (!this.input) return;
5112
5283
  this.#a11yAttributes.forEach((name) => {
@@ -5128,13 +5299,19 @@ class FigInputText extends HTMLElement {
5128
5299
  }
5129
5300
  const prepend = this.querySelector('[slot="prepend"]');
5130
5301
  if (prepend && prepend !== generated) return;
5131
- if (generated) return;
5302
+ if (generated) {
5303
+ const icon = generated.querySelector("fig-icon");
5304
+ if (icon && icon.getAttribute("name") !== "search") {
5305
+ icon.setAttribute("name", "search");
5306
+ }
5307
+ return;
5308
+ }
5132
5309
 
5133
5310
  const icon = createFigIcon("search");
5134
5311
  icon.setAttribute("slot", "prepend");
5135
5312
  icon.setAttribute("data-generated", "search-prefix");
5136
5313
  icon.setAttribute("color", "var(--figma-color-icon)");
5137
- icon.addEventListener("click", this.focus.bind(this));
5314
+ icon.addEventListener("click", this.#boundFocusControl);
5138
5315
  this.prepend(icon);
5139
5316
  }
5140
5317
  #syncSearchClear() {
@@ -5147,7 +5324,13 @@ class FigInputText extends HTMLElement {
5147
5324
  }
5148
5325
  const append = this.querySelector('[slot="append"]');
5149
5326
  if (append && append !== generated) return;
5150
- if (generated) return;
5327
+ if (generated) {
5328
+ const icon = generated.querySelector("fig-icon");
5329
+ if (icon && icon.getAttribute("name") !== "close") {
5330
+ icon.setAttribute("name", "close");
5331
+ }
5332
+ return;
5333
+ }
5151
5334
 
5152
5335
  const wrapper = document.createElement("span");
5153
5336
  wrapper.setAttribute("slot", "append");
@@ -5161,13 +5344,12 @@ class FigInputText extends HTMLElement {
5161
5344
  button.setAttribute("icon", "");
5162
5345
  button.setAttribute("aria-label", "Clear search");
5163
5346
 
5164
- const icon = createFigIcon("", { size: "small" });
5347
+ const icon = createFigIcon("close", { size: "small" });
5165
5348
  icon.setAttribute("color", "var(--figma-color-icon-secondary)");
5166
5349
  button.append(icon);
5167
5350
  tooltip.append(button);
5168
5351
  wrapper.append(tooltip);
5169
5352
  this.append(wrapper);
5170
- icon.style.setProperty("--icon", "var(--icon-16-close)");
5171
5353
 
5172
5354
  button.addEventListener("click", (e) => {
5173
5355
  e.preventDefault();
@@ -5633,30 +5815,7 @@ class FigInputNumber extends HTMLElement {
5633
5815
  this.hasAttribute("steppers") &&
5634
5816
  this.getAttribute("steppers") !== "false";
5635
5817
 
5636
- let html = `<input
5637
- type="text"
5638
- inputmode="decimal"
5639
- ${this.name ? `name="${this.name}"` : ""}
5640
- placeholder="${this.placeholder}"
5641
- value="${this.#formatWithUnit(this.value)}" />`;
5642
-
5643
- let append = this.querySelector("[slot=append]");
5644
- let prepend = this.querySelector("[slot=prepend]");
5645
-
5646
- this.innerHTML = html;
5647
-
5648
- if (prepend) {
5649
- prepend.removeEventListener("click", this.#boundFocusControl);
5650
- prepend.addEventListener("click", this.#boundFocusControl);
5651
- this.prepend(prepend);
5652
- }
5653
- if (append) {
5654
- append.removeEventListener("click", this.#boundFocusControl);
5655
- append.addEventListener("click", this.#boundFocusControl);
5656
- this.append(append);
5657
- }
5658
-
5659
- this.input = this.querySelector("input");
5818
+ this.input = this.#ensureInputControl();
5660
5819
  this.#syncInputA11yAttributes();
5661
5820
 
5662
5821
  if (this.getAttribute("min")) {
@@ -5710,6 +5869,36 @@ class FigInputNumber extends HTMLElement {
5710
5869
  this.input.focus();
5711
5870
  }
5712
5871
 
5872
+ #ensureInputControl() {
5873
+ const existing = this.querySelector("input");
5874
+ if (existing) return existing;
5875
+
5876
+ const html = `<input
5877
+ type="text"
5878
+ inputmode="decimal"
5879
+ ${this.name ? `name="${this.name}"` : ""}
5880
+ placeholder="${this.placeholder}"
5881
+ value="${this.#formatWithUnit(this.value)}" />`;
5882
+
5883
+ const append = this.querySelector("[slot=append]");
5884
+ const prepend = this.querySelector("[slot=prepend]");
5885
+
5886
+ this.innerHTML = html;
5887
+
5888
+ if (prepend) {
5889
+ prepend.removeEventListener("click", this.#boundFocusControl);
5890
+ prepend.addEventListener("click", this.#boundFocusControl);
5891
+ this.prepend(prepend);
5892
+ }
5893
+ if (append) {
5894
+ append.removeEventListener("click", this.#boundFocusControl);
5895
+ append.addEventListener("click", this.#boundFocusControl);
5896
+ this.append(append);
5897
+ }
5898
+
5899
+ return this.querySelector("input");
5900
+ }
5901
+
5713
5902
  #syncInputA11yAttributes() {
5714
5903
  if (!this.input) return;
5715
5904
  this.#a11yAttributes.forEach((name) => {
@@ -6293,6 +6482,14 @@ class FigInputColor extends HTMLElement {
6293
6482
  #suppressNativeColorClick = false;
6294
6483
  #pendingFillPickerPointerOpen = false;
6295
6484
  #nativeColorClickTimer = null;
6485
+ #boundSwatchPointerDown = this.#handleSwatchPointerDown.bind(this);
6486
+ #boundSwatchClick = this.#handleSwatchClick.bind(this);
6487
+ #boundSwatchKeyDown = this.#handleSwatchKeyDown.bind(this);
6488
+ #boundHandleInput = this.#handleInput.bind(this);
6489
+ #boundTextInput = this.#handleTextInput.bind(this);
6490
+ #boundChange = this.#handleChange.bind(this);
6491
+ #boundAlphaInput = this.#handleAlphaInput.bind(this);
6492
+ #boundFillPickerInput = this.#handleFillPickerInput.bind(this);
6296
6493
  constructor() {
6297
6494
  super();
6298
6495
  }
@@ -6329,9 +6526,105 @@ class FigInputColor extends HTMLElement {
6329
6526
  }
6330
6527
 
6331
6528
  connectedCallback() {
6529
+ if (this.#canReuseUI()) {
6530
+ this.#refreshUI();
6531
+ return;
6532
+ }
6332
6533
  this.#buildUI();
6333
6534
  }
6334
6535
 
6536
+ disconnectedCallback() {
6537
+ this.#teardownControlListeners();
6538
+ }
6539
+
6540
+ #canReuseUI() {
6541
+ const showText = this.getAttribute("text") !== "false";
6542
+ return showText
6543
+ ? !!this.querySelector(":scope > .input-combo")
6544
+ : !!this.querySelector(":scope > fig-chit");
6545
+ }
6546
+
6547
+ #refreshUI() {
6548
+ this.#setValues(this.getAttribute("value"));
6549
+ this.#swatch = this.querySelector("fig-chit");
6550
+ this.#fillPicker = this.querySelector("fig-fill-picker");
6551
+ this.#textInput = this.querySelector("fig-input-text:not([type=number])");
6552
+ this.#alphaInput = this.querySelector("fig-input-number");
6553
+ if (this.#textInput) {
6554
+ this.#textInput.setAttribute(
6555
+ "value",
6556
+ this.hexOpaque.slice(1).toUpperCase(),
6557
+ );
6558
+ }
6559
+ if (this.#alphaInput) {
6560
+ this.#alphaInput.setAttribute("value", String(this.#alphaPercent));
6561
+ }
6562
+ if (this.#swatch) {
6563
+ this.#swatch.setAttribute("background", this.hexOpaque);
6564
+ this.#swatch.setAttribute("alpha", String(this.rgba.a));
6565
+ }
6566
+ this.#syncA11yAttributes();
6567
+ this.#bindControlListeners();
6568
+ }
6569
+
6570
+ #teardownControlListeners() {
6571
+ if (this.#swatch) {
6572
+ this.#swatch.removeEventListener(
6573
+ "pointerdown",
6574
+ this.#boundSwatchPointerDown,
6575
+ { capture: true },
6576
+ );
6577
+ this.#swatch.removeEventListener("click", this.#boundSwatchClick, {
6578
+ capture: true,
6579
+ });
6580
+ const swatchInput = this.#swatch.querySelector('input[type="color"]');
6581
+ swatchInput?.removeEventListener("keydown", this.#boundSwatchKeyDown);
6582
+ this.#swatch.removeEventListener("input", this.#boundHandleInput);
6583
+ }
6584
+ this.#textInput?.removeEventListener("input", this.#boundTextInput);
6585
+ this.#textInput?.removeEventListener("change", this.#boundChange);
6586
+ this.#alphaInput?.removeEventListener("input", this.#boundAlphaInput);
6587
+ this.#alphaInput?.removeEventListener("change", this.#boundChange);
6588
+ this.#fillPicker?.removeEventListener("input", this.#boundFillPickerInput);
6589
+ this.#fillPicker?.removeEventListener("change", this.#boundChange);
6590
+ }
6591
+
6592
+ #bindControlListeners() {
6593
+ if (this.#swatch) {
6594
+ this.#swatch.disabled = this.hasAttribute("disabled");
6595
+ const swatchInput = this.#swatch.querySelector('input[type="color"]');
6596
+ if (this.#textInput || this.hasAttribute("swatch-disabled")) {
6597
+ swatchInput?.setAttribute("tabindex", "-1");
6598
+ }
6599
+ if (this.hasAttribute("swatch-disabled")) {
6600
+ swatchInput?.setAttribute("disabled", "");
6601
+ if (swatchInput) swatchInput.style.pointerEvents = "none";
6602
+ }
6603
+ this.#swatch.addEventListener(
6604
+ "pointerdown",
6605
+ this.#boundSwatchPointerDown,
6606
+ { capture: true },
6607
+ );
6608
+ this.#swatch.addEventListener("click", this.#boundSwatchClick, {
6609
+ capture: true,
6610
+ });
6611
+ swatchInput?.addEventListener("keydown", this.#boundSwatchKeyDown);
6612
+ this.#swatch.addEventListener("input", this.#boundHandleInput);
6613
+ }
6614
+ if (this.#textInput) {
6615
+ this.#textInput.addEventListener("input", this.#boundTextInput);
6616
+ this.#textInput.addEventListener("change", this.#boundChange);
6617
+ }
6618
+ if (this.#alphaInput) {
6619
+ this.#alphaInput.addEventListener("input", this.#boundAlphaInput);
6620
+ this.#alphaInput.addEventListener("change", this.#boundChange);
6621
+ }
6622
+ if (this.#fillPicker) {
6623
+ this.#fillPicker.addEventListener("input", this.#boundFillPickerInput);
6624
+ this.#fillPicker.addEventListener("change", this.#boundChange);
6625
+ }
6626
+ }
6627
+
6335
6628
  #buildUI() {
6336
6629
  this.#setValues(this.getAttribute("value"));
6337
6630
 
@@ -6377,54 +6670,15 @@ class FigInputColor extends HTMLElement {
6377
6670
  this.#alphaInput = this.querySelector("fig-input-number");
6378
6671
  this.#syncA11yAttributes();
6379
6672
 
6380
- // Setup swatch (native picker)
6381
- if (this.#swatch) {
6382
- this.#swatch.disabled = this.hasAttribute("disabled");
6383
- const swatchInput = this.#swatch.querySelector('input[type="color"]');
6384
- if (this.#textInput || this.hasAttribute("swatch-disabled")) {
6385
- swatchInput?.setAttribute("tabindex", "-1");
6386
- }
6387
- if (this.hasAttribute("swatch-disabled")) {
6388
- swatchInput?.setAttribute("disabled", "");
6389
- if (swatchInput) swatchInput.style.pointerEvents = "none";
6390
- }
6391
- this.#swatch.addEventListener("pointerdown", this.#handleSwatchPointerDown.bind(this), {
6392
- capture: true,
6393
- });
6394
- this.#swatch.addEventListener("click", this.#handleSwatchClick.bind(this), {
6395
- capture: true,
6396
- });
6397
- swatchInput?.addEventListener("keydown", this.#handleSwatchKeyDown.bind(this));
6398
- this.#swatch.addEventListener("input", this.#handleInput.bind(this));
6399
- }
6400
-
6401
6673
  if (this.#textInput) {
6402
6674
  const hex = this.rgbAlphaToHex(this.rgba, 1);
6403
- // Display without # prefix
6404
6675
  this.#textInput.value = hex.slice(1).toUpperCase();
6405
6676
  if (this.#swatch) {
6406
6677
  this.#swatch.background = hex;
6407
6678
  }
6408
- this.#textInput.addEventListener(
6409
- "input",
6410
- this.#handleTextInput.bind(this),
6411
- );
6412
- this.#textInput.addEventListener(
6413
- "change",
6414
- this.#handleChange.bind(this),
6415
- );
6416
6679
  }
6417
6680
 
6418
- if (this.#alphaInput) {
6419
- this.#alphaInput.addEventListener(
6420
- "input",
6421
- this.#handleAlphaInput.bind(this),
6422
- );
6423
- this.#alphaInput.addEventListener(
6424
- "change",
6425
- this.#handleChange.bind(this),
6426
- );
6427
- }
6681
+ this.#bindControlListeners();
6428
6682
  }
6429
6683
 
6430
6684
  #syncFillPicker() {
@@ -7976,6 +8230,7 @@ class FigInputPalette extends HTMLElement {
7976
8230
  if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
7977
8231
  this.#renderRAF = requestAnimationFrame(() => {
7978
8232
  this.#renderRAF = null;
8233
+ if (!this.isConnected) return;
7979
8234
  this.#parseValue();
7980
8235
  this.#render();
7981
8236
  });
@@ -9511,6 +9766,24 @@ class FigComboInput extends HTMLElement {
9511
9766
  this.setAttribute("value", val ?? "");
9512
9767
  }
9513
9768
 
9769
+ #canReuseMarkup() {
9770
+ return !!this.querySelector(":scope > .input-combo");
9771
+ }
9772
+
9773
+ #refreshMarkup() {
9774
+ this.#input = this.querySelector("fig-input-text");
9775
+ this.#button = this.querySelector("fig-button");
9776
+ this.#dropdown = this.querySelector("fig-dropdown");
9777
+ if (this.#input) {
9778
+ this.#input.setAttribute("value", this.value);
9779
+ this.#input.setAttribute(
9780
+ "placeholder",
9781
+ this.getAttribute("placeholder") || "",
9782
+ );
9783
+ }
9784
+ this.#syncA11yAttributes();
9785
+ }
9786
+
9514
9787
  connectedCallback() {
9515
9788
  this.#customDropdown =
9516
9789
  Array.from(this.children).find(
@@ -9521,8 +9794,13 @@ class FigComboInput extends HTMLElement {
9521
9794
  this.#customDropdown.remove();
9522
9795
  }
9523
9796
 
9524
- this.#render();
9525
- this.#setupListeners();
9797
+ if (this.#canReuseMarkup()) {
9798
+ this.#refreshMarkup();
9799
+ this.#setupListeners();
9800
+ } else {
9801
+ this.#render();
9802
+ this.#setupListeners();
9803
+ }
9526
9804
 
9527
9805
  if (this.hasAttribute("disabled")) {
9528
9806
  this.#applyDisabled(true);
@@ -9576,6 +9854,7 @@ class FigComboInput extends HTMLElement {
9576
9854
  }
9577
9855
 
9578
9856
  #setupListeners() {
9857
+ this.#teardownListeners();
9579
9858
  this.#dropdown?.addEventListener("input", this.#boundHandleDropdownInput);
9580
9859
  this.#input?.addEventListener("input", this.#boundHandleTextInput);
9581
9860
  this.#input?.addEventListener("change", this.#boundHandleTextChange);
@@ -12226,8 +12505,11 @@ class Fig3DRotate extends HTMLElement {
12226
12505
  #container = null;
12227
12506
  #boundKeyDown = null;
12228
12507
  #boundKeyUp = null;
12508
+ #boundContainerPointerDown = (e) => this.#startDrag(e);
12509
+ #eventAbort = null;
12229
12510
  #fields = [];
12230
12511
  #fieldInputs = {};
12512
+ #fieldInputHandlers = {};
12231
12513
 
12232
12514
  static get observedAttributes() {
12233
12515
  return [
@@ -12256,16 +12538,36 @@ class Fig3DRotate extends HTMLElement {
12256
12538
  this.#parseFields(this.getAttribute("fields"));
12257
12539
  const val = this.getAttribute("value");
12258
12540
  if (val) this.#parseValue(val);
12259
- this.#render();
12541
+ if (this.querySelector(".fig-3d-rotate-container")) {
12542
+ this.#reuseRenderedMarkup();
12543
+ } else {
12544
+ this.#render();
12545
+ }
12260
12546
  this.#syncSelected(this.getAttribute("selected"));
12261
12547
  this.#syncDragState();
12262
12548
  }
12263
12549
 
12264
12550
  disconnectedCallback() {
12265
12551
  this.#isDragging = false;
12552
+ this.#teardownEvents();
12553
+ }
12554
+
12555
+ #reuseRenderedMarkup() {
12556
+ this.#container = this.querySelector(".fig-3d-rotate-container");
12557
+ this.#cube = this.querySelector(".fig-3d-rotate-cube");
12558
+ this.#wireFieldInputs();
12559
+ this.#updateCube();
12560
+ this.#setupEvents();
12561
+ }
12562
+
12563
+ #teardownEvents() {
12564
+ this.#eventAbort?.abort();
12565
+ this.#eventAbort = null;
12266
12566
  if (this.#boundKeyDown) {
12267
12567
  window.removeEventListener("keydown", this.#boundKeyDown);
12268
12568
  window.removeEventListener("keyup", this.#boundKeyUp);
12569
+ this.#boundKeyDown = null;
12570
+ this.#boundKeyUp = null;
12269
12571
  }
12270
12572
  }
12271
12573
 
@@ -12419,12 +12721,19 @@ class Fig3DRotate extends HTMLElement {
12419
12721
  </div>${fieldsHTML}`;
12420
12722
  this.#container = this.querySelector(".fig-3d-rotate-container");
12421
12723
  this.#cube = this.querySelector(".fig-3d-rotate-cube");
12724
+ this.#wireFieldInputs();
12725
+ this.#updateCube();
12726
+ this.#setupEvents();
12727
+ }
12728
+
12729
+ #wireFieldInputs() {
12422
12730
  this.#fieldInputs = {};
12423
12731
  for (const axis of this.#fields) {
12424
12732
  const input = this.querySelector(`fig-input-number[name="${axis}"]`);
12425
- if (input) {
12426
- this.#fieldInputs[axis] = input;
12427
- const handleFieldValue = (e) => {
12733
+ if (!input) continue;
12734
+ this.#fieldInputs[axis] = input;
12735
+ if (!this.#fieldInputHandlers[axis]) {
12736
+ this.#fieldInputHandlers[axis] = (e) => {
12428
12737
  e.stopPropagation();
12429
12738
  const val = parseFloat(e.target.value);
12430
12739
  if (isNaN(val)) return;
@@ -12434,12 +12743,13 @@ class Fig3DRotate extends HTMLElement {
12434
12743
  this.#updateCube();
12435
12744
  this.#emit(e.type);
12436
12745
  };
12437
- input.addEventListener("input", handleFieldValue);
12438
- input.addEventListener("change", handleFieldValue);
12439
12746
  }
12747
+ const handler = this.#fieldInputHandlers[axis];
12748
+ input.removeEventListener("input", handler);
12749
+ input.removeEventListener("change", handler);
12750
+ input.addEventListener("input", handler);
12751
+ input.addEventListener("change", handler);
12440
12752
  }
12441
- this.#updateCube();
12442
- this.#setupEvents();
12443
12753
  }
12444
12754
 
12445
12755
  #syncFieldInputs() {
@@ -12480,7 +12790,12 @@ class Fig3DRotate extends HTMLElement {
12480
12790
  }
12481
12791
 
12482
12792
  #setupEvents() {
12483
- this.#container.addEventListener("pointerdown", (e) => this.#startDrag(e));
12793
+ this.#teardownEvents();
12794
+ if (!this.#container) return;
12795
+ this.#eventAbort = new AbortController();
12796
+ this.#container.addEventListener("pointerdown", this.#boundContainerPointerDown, {
12797
+ signal: this.#eventAbort.signal,
12798
+ });
12484
12799
  this.#boundKeyDown = (e) => {
12485
12800
  if (e.key === "Shift") this.#isShiftHeld = true;
12486
12801
  };
@@ -12572,12 +12887,28 @@ class FigOriginGrid extends HTMLElement {
12572
12887
  return ["value", "precision", "aspect-ratio", "drag", "fields"];
12573
12888
  }
12574
12889
 
12890
+ #reuseRenderedMarkup() {
12891
+ this.#grid = this.querySelector(".origin-grid");
12892
+ this.#cells = Array.from(this.querySelectorAll(".origin-grid-cell"));
12893
+ this.#handle = this.querySelector("fig-handle");
12894
+ this.#xInput = this.querySelector('fig-input-number[name="x"]');
12895
+ this.#yInput = this.querySelector('fig-input-number[name="y"]');
12896
+ this.#syncHandlePosition();
12897
+ this.#syncOverflowState();
12898
+ this.#syncValueInputs();
12899
+ this.#setupEvents();
12900
+ }
12901
+
12575
12902
  connectedCallback() {
12576
12903
  this.#precision = parseInt(this.getAttribute("precision") || "0");
12577
12904
  figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
12578
12905
  this.#applyIncomingValue(this.getAttribute("value"));
12579
12906
 
12580
- this.#render();
12907
+ if (this.querySelector(".fig-origin-grid-surface")) {
12908
+ this.#reuseRenderedMarkup();
12909
+ } else {
12910
+ this.#render();
12911
+ }
12581
12912
  this.#syncDragState();
12582
12913
  this.#syncValueAttribute();
12583
12914
  }
@@ -12585,6 +12916,7 @@ class FigOriginGrid extends HTMLElement {
12585
12916
  disconnectedCallback() {
12586
12917
  this.#isDragging = false;
12587
12918
  this.#detachHandleDragListeners();
12919
+ this.#teardownEvents();
12588
12920
  }
12589
12921
 
12590
12922
  get value() {
@@ -12989,38 +13321,65 @@ class FigOriginGrid extends HTMLElement {
12989
13321
  );
12990
13322
  }
12991
13323
 
13324
+ #eventAbort = null;
13325
+
13326
+ #teardownEvents() {
13327
+ this.#eventAbort?.abort();
13328
+ this.#eventAbort = null;
13329
+ }
13330
+
12992
13331
  #setupEvents() {
13332
+ this.#teardownEvents();
12993
13333
  if (!this.#grid || !this.#handle) return;
12994
13334
 
12995
- this.#grid.addEventListener("pointerdown", (e) => {
12996
- const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
12997
- this.#setHoveredCell(hovered);
13335
+ this.#eventAbort = new AbortController();
13336
+ const { signal } = this.#eventAbort;
12998
13337
 
12999
- if (this.#dragEnabled) {
13000
- this.#startGridDrag(e);
13001
- return;
13002
- }
13338
+ this.#grid.addEventListener(
13339
+ "pointerdown",
13340
+ (e) => {
13341
+ const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
13342
+ this.#setHoveredCell(hovered);
13003
13343
 
13004
- const center = this.#cellCenterFromClient(e.clientX, e.clientY);
13005
- this.#setFromPercent(center.x, center.y, "input");
13006
- this.#emit("change");
13007
- });
13344
+ if (this.#dragEnabled) {
13345
+ this.#startGridDrag(e);
13346
+ return;
13347
+ }
13008
13348
 
13009
- this.#grid.addEventListener("pointermove", (e) => {
13010
- if (this.#isDragging) return;
13011
- const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
13012
- this.#setHoveredCell(hovered);
13013
- });
13349
+ const center = this.#cellCenterFromClient(e.clientX, e.clientY);
13350
+ this.#setFromPercent(center.x, center.y, "input");
13351
+ this.#emit("change");
13352
+ },
13353
+ { signal },
13354
+ );
13014
13355
 
13015
- this.#grid.addEventListener("pointerleave", () => {
13016
- this.#clearHoveredCells();
13017
- });
13356
+ this.#grid.addEventListener(
13357
+ "pointermove",
13358
+ (e) => {
13359
+ if (this.#isDragging) return;
13360
+ const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
13361
+ this.#setHoveredCell(hovered);
13362
+ },
13363
+ { signal },
13364
+ );
13018
13365
 
13019
- this.#handle.addEventListener("keydown", (e) => {
13020
- if (!this.#moveHandleByKeyboard(e)) return;
13021
- e.preventDefault();
13022
- e.stopPropagation();
13023
- });
13366
+ this.#grid.addEventListener(
13367
+ "pointerleave",
13368
+ () => {
13369
+ this.#clearHoveredCells();
13370
+ },
13371
+ { signal },
13372
+ );
13373
+
13374
+ this.#handle.addEventListener(
13375
+ "keydown",
13376
+ (e) => {
13377
+ if (!this.#moveHandleByKeyboard(e)) return;
13378
+ e.preventDefault();
13379
+ e.stopPropagation();
13380
+ },
13381
+ { signal },
13382
+ );
13024
13383
 
13025
13384
  const bindValueInput = (inputEl, axis) => {
13026
13385
  if (!inputEl) return;
@@ -13033,11 +13392,15 @@ class FigOriginGrid extends HTMLElement {
13033
13392
  this.#setFromPercent(this.#x, next, "input");
13034
13393
  }
13035
13394
  };
13036
- inputEl.addEventListener("input", handle);
13037
- inputEl.addEventListener("change", handle);
13038
- inputEl.addEventListener("focusout", () => {
13039
- this.#emit("change");
13040
- });
13395
+ inputEl.addEventListener("input", handle, { signal });
13396
+ inputEl.addEventListener("change", handle, { signal });
13397
+ inputEl.addEventListener(
13398
+ "focusout",
13399
+ () => {
13400
+ this.#emit("change");
13401
+ },
13402
+ { signal },
13403
+ );
13041
13404
  };
13042
13405
 
13043
13406
  bindValueInput(this.#xInput, "x");
@@ -13091,9 +13454,16 @@ class FigInputJoystick extends HTMLElement {
13091
13454
  this.#boundYFocusOut = () => this.#handleFieldFocusOut();
13092
13455
  }
13093
13456
 
13457
+ #reuseRenderedMarkup() {
13458
+ this.#setupListeners();
13459
+ this.#syncHandlePosition();
13460
+ this.#syncValueAttribute();
13461
+ this.#syncResetButton();
13462
+ this.#initialized = true;
13463
+ }
13464
+
13094
13465
  connectedCallback() {
13095
- // Initialize position
13096
- requestAnimationFrame(() => {
13466
+ figNextFrame(this, () => {
13097
13467
  this.precision = this.getAttribute("precision") || 3;
13098
13468
  this.precision = parseInt(this.precision);
13099
13469
  this.transform = this.getAttribute("transform") || 1;
@@ -13104,6 +13474,11 @@ class FigInputJoystick extends HTMLElement {
13104
13474
  this.setAttribute("value", "50% 50%");
13105
13475
  }
13106
13476
 
13477
+ if (this.querySelector(".fig-input-joystick-plane")) {
13478
+ this.#reuseRenderedMarkup();
13479
+ return;
13480
+ }
13481
+
13107
13482
  this.#render();
13108
13483
  this.#setupListeners();
13109
13484
  this.#syncHandlePosition();
@@ -14392,7 +14767,7 @@ class FigChooser extends HTMLElement {
14392
14767
  this.#startObserver();
14393
14768
  this.#startResizeObserver();
14394
14769
 
14395
- requestAnimationFrame(() => {
14770
+ figNextFrame(this, () => {
14396
14771
  this.#syncSelection();
14397
14772
  this.#syncOverflow();
14398
14773
  this.#scheduleInitialScrollSettle();