@rogieking/figui3 6.6.1 → 6.6.3

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
@@ -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);
@@ -3662,7 +3691,7 @@ class FigSegmentedControl extends HTMLElement {
3662
3691
  this.#startResizeObserver();
3663
3692
 
3664
3693
  // Defer initial selection so child segments are available.
3665
- requestAnimationFrame(() => {
3694
+ figNextFrame(this, () => {
3666
3695
  this.#syncSelectionFromAttributes({ enforceFallback: true });
3667
3696
  this.#refreshResizeObserverTargets();
3668
3697
  this.#queueIndicatorSync({ forceInstant: true });
@@ -4118,13 +4147,52 @@ class FigOptions extends HTMLElement {
4118
4147
  return String(fallbackValue);
4119
4148
  }
4120
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
+
4121
4185
  connectedCallback() {
4122
4186
  this.#parseOptions();
4187
+ if (this.#canReuseControl()) {
4188
+ this.#reuseControl();
4189
+ this.#startResizeObserver();
4190
+ figNextFrame(this, () => figNextFrame(this, () => this.#checkOverflow()));
4191
+ return;
4192
+ }
4123
4193
  this.#renderSegments();
4124
4194
  this.#startResizeObserver();
4125
- requestAnimationFrame(() => {
4126
- requestAnimationFrame(() => this.#checkOverflow());
4127
- });
4195
+ figNextFrame(this, () => figNextFrame(this, () => this.#checkOverflow()));
4128
4196
  }
4129
4197
 
4130
4198
  disconnectedCallback() {
@@ -4476,7 +4544,7 @@ class FigSlider extends HTMLElement {
4476
4544
  };
4477
4545
  }
4478
4546
 
4479
- #regenerateInnerHTML() {
4547
+ #readAttributesFromMarkup() {
4480
4548
  const rawValue = this.getAttribute("value");
4481
4549
  this.type = this.getAttribute("type") || "range";
4482
4550
  this.variant = this.getAttribute("variant") || "default";
@@ -4507,6 +4575,103 @@ class FigSlider extends HTMLElement {
4507
4575
  (rawValue === null ||
4508
4576
  (typeof rawValue === "string" && rawValue.trim() === ""));
4509
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();
4510
4675
 
4511
4676
  if (this.color) {
4512
4677
  this.style.setProperty("--color", this.color);
@@ -4547,24 +4712,7 @@ class FigSlider extends HTMLElement {
4547
4712
 
4548
4713
  this.input = this.querySelector("[type=range]");
4549
4714
  this.inputContainer = this.querySelector(".fig-slider-input-container");
4550
- this.#syncInputA11yAttributes();
4551
- this.input.removeEventListener("input", this.#boundHandleInput);
4552
- this.input.addEventListener("input", this.#boundHandleInput);
4553
- this.input.removeEventListener("change", this.#boundHandleChange);
4554
- this.input.addEventListener("change", this.#boundHandleChange);
4555
- this.input.removeEventListener("keydown", this.#boundHandleKeyDown);
4556
- this.input.addEventListener("keydown", this.#boundHandleKeyDown);
4557
- this.input.removeEventListener("pointerdown", this.#boundRangePointerDown);
4558
- this.input.addEventListener("pointerdown", this.#boundRangePointerDown);
4559
- this.input.removeEventListener("pointerup", this.#boundRangePointerUp);
4560
- this.input.addEventListener("pointerup", this.#boundRangePointerUp);
4561
-
4562
- if (this.default) {
4563
- this.style.setProperty(
4564
- "--default",
4565
- this.#calculateNormal(this.default),
4566
- );
4567
- }
4715
+ this.#bindControlListeners();
4568
4716
 
4569
4717
  this.datalist = this.querySelector("datalist");
4570
4718
  this.figInputNumber = this.querySelector("fig-input-number");
@@ -4603,30 +4751,16 @@ class FigSlider extends HTMLElement {
4603
4751
  defaultOption.setAttribute("default", "true");
4604
4752
  }
4605
4753
  }
4606
- if (this.figInputNumber) {
4607
- this.#syncTextInputA11yAttributes();
4608
- this.figInputNumber.removeEventListener(
4609
- "input",
4610
- this.#boundHandleTextInput,
4611
- );
4612
- this.figInputNumber.addEventListener(
4613
- "input",
4614
- this.#boundHandleTextInput,
4615
- );
4616
- this.figInputNumber.removeEventListener(
4617
- "change",
4618
- this.#boundHandleTextChange,
4619
- );
4620
- this.figInputNumber.addEventListener(
4621
- "change",
4622
- this.#boundHandleTextChange,
4623
- );
4624
- }
4625
-
4626
4754
  this.#syncValue();
4627
4755
  }
4628
4756
 
4629
4757
  connectedCallback() {
4758
+ if (this.#canReuseRenderedMarkup()) {
4759
+ this.#updateRenderedMarkup();
4760
+ this.#bindControlListeners();
4761
+ this.#syncValue();
4762
+ return;
4763
+ }
4630
4764
  this.#regenerateInnerHTML();
4631
4765
  }
4632
4766
 
@@ -5064,35 +5198,7 @@ class FigInputText extends HTMLElement {
5064
5198
  }
5065
5199
  }
5066
5200
 
5067
- let html = `<input
5068
- type="${this.type}"
5069
- ${this.name ? `name="${this.name}"` : ""}
5070
- placeholder="${this.placeholder}"
5071
- value="${
5072
- this.type === "number" ? this.#transformNumber(this.value) : this.value
5073
- }" />`;
5074
- if (this.multiline) {
5075
- html = `<textarea
5076
- placeholder="${this.placeholder}">${this.value}</textarea>`;
5077
- }
5078
-
5079
- let append = this.querySelector("[slot=append]");
5080
- let prepend = this.querySelector("[slot=prepend]");
5081
-
5082
- this.innerHTML = html;
5083
-
5084
- if (prepend) {
5085
- prepend.removeEventListener("click", this.#boundFocusControl);
5086
- prepend.addEventListener("click", this.#boundFocusControl);
5087
- this.prepend(prepend);
5088
- }
5089
- if (append) {
5090
- append.removeEventListener("click", this.#boundFocusControl);
5091
- append.addEventListener("click", this.#boundFocusControl);
5092
- this.append(append);
5093
- }
5094
-
5095
- this.input = this.querySelector("input,textarea");
5201
+ this.input = this.#ensureInputControl();
5096
5202
  this.input.readOnly = this.readonly;
5097
5203
  this.#syncInputA11yAttributes();
5098
5204
  this.#syncSearchPrefix();
@@ -5132,6 +5238,46 @@ class FigInputText extends HTMLElement {
5132
5238
  focus() {
5133
5239
  this.input.focus();
5134
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
+ }
5135
5281
  #syncInputA11yAttributes() {
5136
5282
  if (!this.input) return;
5137
5283
  this.#a11yAttributes.forEach((name) => {
@@ -5153,13 +5299,19 @@ class FigInputText extends HTMLElement {
5153
5299
  }
5154
5300
  const prepend = this.querySelector('[slot="prepend"]');
5155
5301
  if (prepend && prepend !== generated) return;
5156
- 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
+ }
5157
5309
 
5158
5310
  const icon = createFigIcon("search");
5159
5311
  icon.setAttribute("slot", "prepend");
5160
5312
  icon.setAttribute("data-generated", "search-prefix");
5161
5313
  icon.setAttribute("color", "var(--figma-color-icon)");
5162
- icon.addEventListener("click", this.focus.bind(this));
5314
+ icon.addEventListener("click", this.#boundFocusControl);
5163
5315
  this.prepend(icon);
5164
5316
  }
5165
5317
  #syncSearchClear() {
@@ -5172,7 +5324,13 @@ class FigInputText extends HTMLElement {
5172
5324
  }
5173
5325
  const append = this.querySelector('[slot="append"]');
5174
5326
  if (append && append !== generated) return;
5175
- 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
+ }
5176
5334
 
5177
5335
  const wrapper = document.createElement("span");
5178
5336
  wrapper.setAttribute("slot", "append");
@@ -5186,13 +5344,12 @@ class FigInputText extends HTMLElement {
5186
5344
  button.setAttribute("icon", "");
5187
5345
  button.setAttribute("aria-label", "Clear search");
5188
5346
 
5189
- const icon = createFigIcon("", { size: "small" });
5347
+ const icon = createFigIcon("close", { size: "small" });
5190
5348
  icon.setAttribute("color", "var(--figma-color-icon-secondary)");
5191
5349
  button.append(icon);
5192
5350
  tooltip.append(button);
5193
5351
  wrapper.append(tooltip);
5194
5352
  this.append(wrapper);
5195
- icon.style.setProperty("--icon", "var(--icon-16-close)");
5196
5353
 
5197
5354
  button.addEventListener("click", (e) => {
5198
5355
  e.preventDefault();
@@ -5658,30 +5815,7 @@ class FigInputNumber extends HTMLElement {
5658
5815
  this.hasAttribute("steppers") &&
5659
5816
  this.getAttribute("steppers") !== "false";
5660
5817
 
5661
- let html = `<input
5662
- type="text"
5663
- inputmode="decimal"
5664
- ${this.name ? `name="${this.name}"` : ""}
5665
- placeholder="${this.placeholder}"
5666
- value="${this.#formatWithUnit(this.value)}" />`;
5667
-
5668
- let append = this.querySelector("[slot=append]");
5669
- let prepend = this.querySelector("[slot=prepend]");
5670
-
5671
- this.innerHTML = html;
5672
-
5673
- if (prepend) {
5674
- prepend.removeEventListener("click", this.#boundFocusControl);
5675
- prepend.addEventListener("click", this.#boundFocusControl);
5676
- this.prepend(prepend);
5677
- }
5678
- if (append) {
5679
- append.removeEventListener("click", this.#boundFocusControl);
5680
- append.addEventListener("click", this.#boundFocusControl);
5681
- this.append(append);
5682
- }
5683
-
5684
- this.input = this.querySelector("input");
5818
+ this.input = this.#ensureInputControl();
5685
5819
  this.#syncInputA11yAttributes();
5686
5820
 
5687
5821
  if (this.getAttribute("min")) {
@@ -5735,6 +5869,36 @@ class FigInputNumber extends HTMLElement {
5735
5869
  this.input.focus();
5736
5870
  }
5737
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
+
5738
5902
  #syncInputA11yAttributes() {
5739
5903
  if (!this.input) return;
5740
5904
  this.#a11yAttributes.forEach((name) => {
@@ -6318,6 +6482,14 @@ class FigInputColor extends HTMLElement {
6318
6482
  #suppressNativeColorClick = false;
6319
6483
  #pendingFillPickerPointerOpen = false;
6320
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);
6321
6493
  constructor() {
6322
6494
  super();
6323
6495
  }
@@ -6354,9 +6526,105 @@ class FigInputColor extends HTMLElement {
6354
6526
  }
6355
6527
 
6356
6528
  connectedCallback() {
6529
+ if (this.#canReuseUI()) {
6530
+ this.#refreshUI();
6531
+ return;
6532
+ }
6357
6533
  this.#buildUI();
6358
6534
  }
6359
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
+
6360
6628
  #buildUI() {
6361
6629
  this.#setValues(this.getAttribute("value"));
6362
6630
 
@@ -6402,54 +6670,15 @@ class FigInputColor extends HTMLElement {
6402
6670
  this.#alphaInput = this.querySelector("fig-input-number");
6403
6671
  this.#syncA11yAttributes();
6404
6672
 
6405
- // Setup swatch (native picker)
6406
- if (this.#swatch) {
6407
- this.#swatch.disabled = this.hasAttribute("disabled");
6408
- const swatchInput = this.#swatch.querySelector('input[type="color"]');
6409
- if (this.#textInput || this.hasAttribute("swatch-disabled")) {
6410
- swatchInput?.setAttribute("tabindex", "-1");
6411
- }
6412
- if (this.hasAttribute("swatch-disabled")) {
6413
- swatchInput?.setAttribute("disabled", "");
6414
- if (swatchInput) swatchInput.style.pointerEvents = "none";
6415
- }
6416
- this.#swatch.addEventListener("pointerdown", this.#handleSwatchPointerDown.bind(this), {
6417
- capture: true,
6418
- });
6419
- this.#swatch.addEventListener("click", this.#handleSwatchClick.bind(this), {
6420
- capture: true,
6421
- });
6422
- swatchInput?.addEventListener("keydown", this.#handleSwatchKeyDown.bind(this));
6423
- this.#swatch.addEventListener("input", this.#handleInput.bind(this));
6424
- }
6425
-
6426
6673
  if (this.#textInput) {
6427
6674
  const hex = this.rgbAlphaToHex(this.rgba, 1);
6428
- // Display without # prefix
6429
6675
  this.#textInput.value = hex.slice(1).toUpperCase();
6430
6676
  if (this.#swatch) {
6431
6677
  this.#swatch.background = hex;
6432
6678
  }
6433
- this.#textInput.addEventListener(
6434
- "input",
6435
- this.#handleTextInput.bind(this),
6436
- );
6437
- this.#textInput.addEventListener(
6438
- "change",
6439
- this.#handleChange.bind(this),
6440
- );
6441
6679
  }
6442
6680
 
6443
- if (this.#alphaInput) {
6444
- this.#alphaInput.addEventListener(
6445
- "input",
6446
- this.#handleAlphaInput.bind(this),
6447
- );
6448
- this.#alphaInput.addEventListener(
6449
- "change",
6450
- this.#handleChange.bind(this),
6451
- );
6452
- }
6681
+ this.#bindControlListeners();
6453
6682
  }
6454
6683
 
6455
6684
  #syncFillPicker() {
@@ -8001,6 +8230,7 @@ class FigInputPalette extends HTMLElement {
8001
8230
  if (this.#renderRAF) cancelAnimationFrame(this.#renderRAF);
8002
8231
  this.#renderRAF = requestAnimationFrame(() => {
8003
8232
  this.#renderRAF = null;
8233
+ if (!this.isConnected) return;
8004
8234
  this.#parseValue();
8005
8235
  this.#render();
8006
8236
  });
@@ -9536,6 +9766,24 @@ class FigComboInput extends HTMLElement {
9536
9766
  this.setAttribute("value", val ?? "");
9537
9767
  }
9538
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
+
9539
9787
  connectedCallback() {
9540
9788
  this.#customDropdown =
9541
9789
  Array.from(this.children).find(
@@ -9546,8 +9794,13 @@ class FigComboInput extends HTMLElement {
9546
9794
  this.#customDropdown.remove();
9547
9795
  }
9548
9796
 
9549
- this.#render();
9550
- this.#setupListeners();
9797
+ if (this.#canReuseMarkup()) {
9798
+ this.#refreshMarkup();
9799
+ this.#setupListeners();
9800
+ } else {
9801
+ this.#render();
9802
+ this.#setupListeners();
9803
+ }
9551
9804
 
9552
9805
  if (this.hasAttribute("disabled")) {
9553
9806
  this.#applyDisabled(true);
@@ -9601,6 +9854,7 @@ class FigComboInput extends HTMLElement {
9601
9854
  }
9602
9855
 
9603
9856
  #setupListeners() {
9857
+ this.#teardownListeners();
9604
9858
  this.#dropdown?.addEventListener("input", this.#boundHandleDropdownInput);
9605
9859
  this.#input?.addEventListener("input", this.#boundHandleTextInput);
9606
9860
  this.#input?.addEventListener("change", this.#boundHandleTextChange);
@@ -12251,8 +12505,11 @@ class Fig3DRotate extends HTMLElement {
12251
12505
  #container = null;
12252
12506
  #boundKeyDown = null;
12253
12507
  #boundKeyUp = null;
12508
+ #boundContainerPointerDown = (e) => this.#startDrag(e);
12509
+ #eventAbort = null;
12254
12510
  #fields = [];
12255
12511
  #fieldInputs = {};
12512
+ #fieldInputHandlers = {};
12256
12513
 
12257
12514
  static get observedAttributes() {
12258
12515
  return [
@@ -12281,16 +12538,36 @@ class Fig3DRotate extends HTMLElement {
12281
12538
  this.#parseFields(this.getAttribute("fields"));
12282
12539
  const val = this.getAttribute("value");
12283
12540
  if (val) this.#parseValue(val);
12284
- this.#render();
12541
+ if (this.querySelector(".fig-3d-rotate-container")) {
12542
+ this.#reuseRenderedMarkup();
12543
+ } else {
12544
+ this.#render();
12545
+ }
12285
12546
  this.#syncSelected(this.getAttribute("selected"));
12286
12547
  this.#syncDragState();
12287
12548
  }
12288
12549
 
12289
12550
  disconnectedCallback() {
12290
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;
12291
12566
  if (this.#boundKeyDown) {
12292
12567
  window.removeEventListener("keydown", this.#boundKeyDown);
12293
12568
  window.removeEventListener("keyup", this.#boundKeyUp);
12569
+ this.#boundKeyDown = null;
12570
+ this.#boundKeyUp = null;
12294
12571
  }
12295
12572
  }
12296
12573
 
@@ -12444,12 +12721,19 @@ class Fig3DRotate extends HTMLElement {
12444
12721
  </div>${fieldsHTML}`;
12445
12722
  this.#container = this.querySelector(".fig-3d-rotate-container");
12446
12723
  this.#cube = this.querySelector(".fig-3d-rotate-cube");
12724
+ this.#wireFieldInputs();
12725
+ this.#updateCube();
12726
+ this.#setupEvents();
12727
+ }
12728
+
12729
+ #wireFieldInputs() {
12447
12730
  this.#fieldInputs = {};
12448
12731
  for (const axis of this.#fields) {
12449
12732
  const input = this.querySelector(`fig-input-number[name="${axis}"]`);
12450
- if (input) {
12451
- this.#fieldInputs[axis] = input;
12452
- const handleFieldValue = (e) => {
12733
+ if (!input) continue;
12734
+ this.#fieldInputs[axis] = input;
12735
+ if (!this.#fieldInputHandlers[axis]) {
12736
+ this.#fieldInputHandlers[axis] = (e) => {
12453
12737
  e.stopPropagation();
12454
12738
  const val = parseFloat(e.target.value);
12455
12739
  if (isNaN(val)) return;
@@ -12459,12 +12743,13 @@ class Fig3DRotate extends HTMLElement {
12459
12743
  this.#updateCube();
12460
12744
  this.#emit(e.type);
12461
12745
  };
12462
- input.addEventListener("input", handleFieldValue);
12463
- input.addEventListener("change", handleFieldValue);
12464
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);
12465
12752
  }
12466
- this.#updateCube();
12467
- this.#setupEvents();
12468
12753
  }
12469
12754
 
12470
12755
  #syncFieldInputs() {
@@ -12505,7 +12790,12 @@ class Fig3DRotate extends HTMLElement {
12505
12790
  }
12506
12791
 
12507
12792
  #setupEvents() {
12508
- 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
+ });
12509
12799
  this.#boundKeyDown = (e) => {
12510
12800
  if (e.key === "Shift") this.#isShiftHeld = true;
12511
12801
  };
@@ -12597,12 +12887,28 @@ class FigOriginGrid extends HTMLElement {
12597
12887
  return ["value", "precision", "aspect-ratio", "drag", "fields"];
12598
12888
  }
12599
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
+
12600
12902
  connectedCallback() {
12601
12903
  this.#precision = parseInt(this.getAttribute("precision") || "0");
12602
12904
  figSyncCssVar(this, "--aspect-ratio", this.getAttribute("aspect-ratio"));
12603
12905
  this.#applyIncomingValue(this.getAttribute("value"));
12604
12906
 
12605
- this.#render();
12907
+ if (this.querySelector(".fig-origin-grid-surface")) {
12908
+ this.#reuseRenderedMarkup();
12909
+ } else {
12910
+ this.#render();
12911
+ }
12606
12912
  this.#syncDragState();
12607
12913
  this.#syncValueAttribute();
12608
12914
  }
@@ -12610,6 +12916,7 @@ class FigOriginGrid extends HTMLElement {
12610
12916
  disconnectedCallback() {
12611
12917
  this.#isDragging = false;
12612
12918
  this.#detachHandleDragListeners();
12919
+ this.#teardownEvents();
12613
12920
  }
12614
12921
 
12615
12922
  get value() {
@@ -13014,38 +13321,65 @@ class FigOriginGrid extends HTMLElement {
13014
13321
  );
13015
13322
  }
13016
13323
 
13324
+ #eventAbort = null;
13325
+
13326
+ #teardownEvents() {
13327
+ this.#eventAbort?.abort();
13328
+ this.#eventAbort = null;
13329
+ }
13330
+
13017
13331
  #setupEvents() {
13332
+ this.#teardownEvents();
13018
13333
  if (!this.#grid || !this.#handle) return;
13019
13334
 
13020
- this.#grid.addEventListener("pointerdown", (e) => {
13021
- const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
13022
- this.#setHoveredCell(hovered);
13335
+ this.#eventAbort = new AbortController();
13336
+ const { signal } = this.#eventAbort;
13023
13337
 
13024
- if (this.#dragEnabled) {
13025
- this.#startGridDrag(e);
13026
- return;
13027
- }
13338
+ this.#grid.addEventListener(
13339
+ "pointerdown",
13340
+ (e) => {
13341
+ const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
13342
+ this.#setHoveredCell(hovered);
13028
13343
 
13029
- const center = this.#cellCenterFromClient(e.clientX, e.clientY);
13030
- this.#setFromPercent(center.x, center.y, "input");
13031
- this.#emit("change");
13032
- });
13344
+ if (this.#dragEnabled) {
13345
+ this.#startGridDrag(e);
13346
+ return;
13347
+ }
13033
13348
 
13034
- this.#grid.addEventListener("pointermove", (e) => {
13035
- if (this.#isDragging) return;
13036
- const hovered = this.#gridCellFromClient(e.clientX, e.clientY);
13037
- this.#setHoveredCell(hovered);
13038
- });
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
+ );
13039
13355
 
13040
- this.#grid.addEventListener("pointerleave", () => {
13041
- this.#clearHoveredCells();
13042
- });
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
+ );
13043
13365
 
13044
- this.#handle.addEventListener("keydown", (e) => {
13045
- if (!this.#moveHandleByKeyboard(e)) return;
13046
- e.preventDefault();
13047
- e.stopPropagation();
13048
- });
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
+ );
13049
13383
 
13050
13384
  const bindValueInput = (inputEl, axis) => {
13051
13385
  if (!inputEl) return;
@@ -13058,11 +13392,15 @@ class FigOriginGrid extends HTMLElement {
13058
13392
  this.#setFromPercent(this.#x, next, "input");
13059
13393
  }
13060
13394
  };
13061
- inputEl.addEventListener("input", handle);
13062
- inputEl.addEventListener("change", handle);
13063
- inputEl.addEventListener("focusout", () => {
13064
- this.#emit("change");
13065
- });
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
+ );
13066
13404
  };
13067
13405
 
13068
13406
  bindValueInput(this.#xInput, "x");
@@ -13116,9 +13454,16 @@ class FigInputJoystick extends HTMLElement {
13116
13454
  this.#boundYFocusOut = () => this.#handleFieldFocusOut();
13117
13455
  }
13118
13456
 
13457
+ #reuseRenderedMarkup() {
13458
+ this.#setupListeners();
13459
+ this.#syncHandlePosition();
13460
+ this.#syncValueAttribute();
13461
+ this.#syncResetButton();
13462
+ this.#initialized = true;
13463
+ }
13464
+
13119
13465
  connectedCallback() {
13120
- // Initialize position
13121
- requestAnimationFrame(() => {
13466
+ figNextFrame(this, () => {
13122
13467
  this.precision = this.getAttribute("precision") || 3;
13123
13468
  this.precision = parseInt(this.precision);
13124
13469
  this.transform = this.getAttribute("transform") || 1;
@@ -13129,6 +13474,11 @@ class FigInputJoystick extends HTMLElement {
13129
13474
  this.setAttribute("value", "50% 50%");
13130
13475
  }
13131
13476
 
13477
+ if (this.querySelector(".fig-input-joystick-plane")) {
13478
+ this.#reuseRenderedMarkup();
13479
+ return;
13480
+ }
13481
+
13132
13482
  this.#render();
13133
13483
  this.#setupListeners();
13134
13484
  this.#syncHandlePosition();
@@ -14417,7 +14767,7 @@ class FigChooser extends HTMLElement {
14417
14767
  this.#startObserver();
14418
14768
  this.#startResizeObserver();
14419
14769
 
14420
- requestAnimationFrame(() => {
14770
+ figNextFrame(this, () => {
14421
14771
  this.#syncSelection();
14422
14772
  this.#syncOverflow();
14423
14773
  this.#scheduleInitialScrollSettle();