@rogieking/figui3 6.4.8 → 6.5.1

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,72 @@ function createFigIcon(name, options = {}) {
27
27
  return icon;
28
28
  }
29
29
 
30
+ function createFigOverflowButtons({
31
+ owner,
32
+ onStart,
33
+ onEnd,
34
+ startClass = "",
35
+ endClass = "",
36
+ chevronClass = "",
37
+ } = {}) {
38
+ const makeButton = (direction, onPointerDown) => {
39
+ const button = document.createElement("button");
40
+ button.className = [
41
+ "fig-overflow",
42
+ `fig-overflow-${direction}`,
43
+ direction === "start" ? startClass : endClass,
44
+ ]
45
+ .filter(Boolean)
46
+ .join(" ");
47
+ button.dataset.figOverflow = direction;
48
+ if (owner) button.setAttribute(`data-fig-${owner}-nav`, direction);
49
+ button.setAttribute("tabindex", "-1");
50
+ button.setAttribute("aria-label", direction === "start" ? "Scroll back" : "Scroll forward");
51
+ button.appendChild(
52
+ createFigIcon("chevron", {
53
+ size: "small",
54
+ className: ["fig-overflow-chevron", chevronClass].filter(Boolean).join(" "),
55
+ }),
56
+ );
57
+ button.addEventListener("pointerdown", (event) => {
58
+ event.preventDefault();
59
+ event.stopPropagation();
60
+ onPointerDown?.(event);
61
+ });
62
+ return button;
63
+ };
64
+
65
+ return {
66
+ start: makeButton("start", onStart),
67
+ end: makeButton("end", onEnd),
68
+ };
69
+ }
70
+
71
+ function figSyncOverflowState(host, scrollEl, axis = "x", threshold = 2) {
72
+ if (!host || !scrollEl) return false;
73
+ const isHorizontal = axis === "x";
74
+ const scrollSize = isHorizontal ? scrollEl.scrollWidth : scrollEl.scrollHeight;
75
+ const clientSize = isHorizontal ? scrollEl.clientWidth : scrollEl.clientHeight;
76
+ const scrollPosition = isHorizontal ? scrollEl.scrollLeft : scrollEl.scrollTop;
77
+ const scrollable = scrollSize - clientSize > threshold;
78
+ const atStart = !scrollable || scrollPosition <= threshold;
79
+ const atEnd = !scrollable || scrollPosition + clientSize >= scrollSize - threshold;
80
+ host.classList.toggle("overflow-start", !atStart);
81
+ host.classList.toggle("overflow-end", !atEnd);
82
+ return scrollable;
83
+ }
84
+
85
+ function figScrollOverflowPage(scrollEl, axis = "x", direction = 1) {
86
+ if (!scrollEl) return;
87
+ const isHorizontal = axis === "x";
88
+ const pageSize = isHorizontal ? scrollEl.clientWidth : scrollEl.clientHeight;
89
+ const scrollAmount = pageSize * 0.8 * direction;
90
+ scrollEl.scrollBy({
91
+ [isHorizontal ? "left" : "top"]: scrollAmount,
92
+ behavior: "smooth",
93
+ });
94
+ }
95
+
30
96
  function hasFigFillPicker() {
31
97
  return typeof customElements !== "undefined" && !!customElements.get("fig-fill-picker");
32
98
  }
@@ -3117,6 +3183,14 @@ customElements.define("fig-tab", FigTab);
3117
3183
  class FigTabs extends HTMLElement {
3118
3184
  #boundHandleClick;
3119
3185
  #boundHandleKeyDown;
3186
+ #boundSyncOverflow = this.#syncOverflow.bind(this);
3187
+ #mutationObserver = null;
3188
+ #resizeObserver = null;
3189
+ #scroller = null;
3190
+ #navStart = null;
3191
+ #navEnd = null;
3192
+ #isStructuring = false;
3193
+
3120
3194
  constructor() {
3121
3195
  super();
3122
3196
  this.#boundHandleClick = this.handleClick.bind(this);
@@ -3130,8 +3204,13 @@ class FigTabs extends HTMLElement {
3130
3204
  connectedCallback() {
3131
3205
  this.name = this.getAttribute("name") || "tabs";
3132
3206
  this.setAttribute("role", "tablist");
3207
+ this.#ensureScroller();
3133
3208
  this.addEventListener("click", this.#boundHandleClick);
3134
3209
  this.addEventListener("keydown", this.#boundHandleKeyDown);
3210
+ this.#scrollElement.addEventListener("scroll", this.#boundSyncOverflow);
3211
+ this.#createNavButtons();
3212
+ this.#startObserver();
3213
+ this.#startResizeObserver();
3135
3214
  requestAnimationFrame(() => {
3136
3215
  const value = this.getAttribute("value");
3137
3216
  if (value) {
@@ -3140,9 +3219,36 @@ class FigTabs extends HTMLElement {
3140
3219
  if (this.hasAttribute("disabled")) {
3141
3220
  this.#applyDisabled(true);
3142
3221
  }
3222
+ this.#syncTabIndexes();
3223
+ this.#syncOverflow();
3224
+ this.#scrollSelectedTabIntoView(undefined, "auto");
3143
3225
  });
3144
3226
  }
3145
3227
 
3228
+ get #scrollElement() {
3229
+ return this.#scroller || this;
3230
+ }
3231
+
3232
+ get scrollLeft() {
3233
+ return this.#scrollElement.scrollLeft;
3234
+ }
3235
+
3236
+ set scrollLeft(value) {
3237
+ this.#scrollElement.scrollLeft = value;
3238
+ }
3239
+
3240
+ get scrollWidth() {
3241
+ return this.#scrollElement.scrollWidth;
3242
+ }
3243
+
3244
+ scrollTo(...args) {
3245
+ this.#scrollElement.scrollTo(...args);
3246
+ }
3247
+
3248
+ scrollBy(...args) {
3249
+ this.#scrollElement.scrollBy(...args);
3250
+ }
3251
+
3146
3252
  #applyDisabled(disabled) {
3147
3253
  const tabs = this.querySelectorAll("fig-tab");
3148
3254
  tabs.forEach((tab) => {
@@ -3173,9 +3279,136 @@ class FigTabs extends HTMLElement {
3173
3279
  });
3174
3280
  }
3175
3281
 
3282
+ #scrollSelectedTabIntoView(tab = this.selectedTab, behavior = "smooth") {
3283
+ const target =
3284
+ tab ||
3285
+ this.querySelector('fig-tab[selected]:not([selected="false"])') ||
3286
+ this.#availableTabs()[0];
3287
+ if (!target) return;
3288
+
3289
+ requestAnimationFrame(() => {
3290
+ if (!this.isConnected || !target.isConnected) return;
3291
+ const scrollEl = this.#scrollElement;
3292
+ const overflowX = scrollEl.scrollWidth > scrollEl.clientWidth;
3293
+ if (!overflowX) return;
3294
+
3295
+ const containerRect = scrollEl.getBoundingClientRect();
3296
+ const tabRect = target.getBoundingClientRect();
3297
+ const tabCenter =
3298
+ tabRect.left - containerRect.left + scrollEl.scrollLeft + tabRect.width / 2;
3299
+
3300
+ scrollEl.scrollTo({
3301
+ left: tabCenter - scrollEl.clientWidth / 2,
3302
+ behavior,
3303
+ });
3304
+ this.#syncOverflow();
3305
+ });
3306
+ }
3307
+
3176
3308
  disconnectedCallback() {
3177
3309
  this.removeEventListener("click", this.#boundHandleClick);
3178
3310
  this.removeEventListener("keydown", this.#boundHandleKeyDown);
3311
+ this.#scrollElement.removeEventListener("scroll", this.#boundSyncOverflow);
3312
+ this.#mutationObserver?.disconnect();
3313
+ this.#mutationObserver = null;
3314
+ this.#resizeObserver?.disconnect();
3315
+ this.#resizeObserver = null;
3316
+ this.#removeNavButtons();
3317
+ }
3318
+
3319
+ #ensureScroller() {
3320
+ if (this.#isStructuring) return;
3321
+ const existing = this.querySelector(":scope > [data-fig-tabs-scroll]");
3322
+ if (existing && existing === this.#scroller) return;
3323
+
3324
+ this.#isStructuring = true;
3325
+ try {
3326
+ const previousScroller = this.#scroller;
3327
+ if (previousScroller && previousScroller !== existing) {
3328
+ previousScroller.removeEventListener("scroll", this.#boundSyncOverflow);
3329
+ }
3330
+
3331
+ const scroller = existing || document.createElement("div");
3332
+ scroller.className = "fig-tabs-scroll";
3333
+ scroller.dataset.figTabsScroll = "";
3334
+
3335
+ const nodes = Array.from(this.childNodes).filter((node) => {
3336
+ if (node === scroller) return false;
3337
+ if (node.nodeType !== Node.ELEMENT_NODE) return true;
3338
+ return !node.hasAttribute("data-fig-tabs-nav");
3339
+ });
3340
+
3341
+ for (const node of nodes) {
3342
+ scroller.appendChild(node);
3343
+ }
3344
+
3345
+ if (!existing) this.prepend(scroller);
3346
+
3347
+ this.#scroller = scroller;
3348
+ this.#scroller.removeEventListener("scroll", this.#boundSyncOverflow);
3349
+ this.#scroller.addEventListener("scroll", this.#boundSyncOverflow);
3350
+ if (this.#resizeObserver) this.#resizeObserver.observe(this.#scroller);
3351
+ } finally {
3352
+ this.#isStructuring = false;
3353
+ }
3354
+ }
3355
+
3356
+ #syncOverflow() {
3357
+ figSyncOverflowState(this, this.#scrollElement, "x");
3358
+ }
3359
+
3360
+ #startResizeObserver() {
3361
+ this.#resizeObserver?.disconnect();
3362
+ this.#resizeObserver = new ResizeObserver(() => this.#syncOverflow());
3363
+ this.#resizeObserver.observe(this);
3364
+ if (this.#scroller) this.#resizeObserver.observe(this.#scroller);
3365
+ }
3366
+
3367
+ #startObserver() {
3368
+ this.#mutationObserver?.disconnect();
3369
+ this.#mutationObserver = new MutationObserver(() => {
3370
+ if (this.#isStructuring) return;
3371
+ this.#ensureScroller();
3372
+ this.#createNavButtons();
3373
+ this.#syncTabIndexes();
3374
+ requestAnimationFrame(() => {
3375
+ this.#syncOverflow();
3376
+ this.#scrollSelectedTabIntoView();
3377
+ });
3378
+ });
3379
+ this.#mutationObserver.observe(this, { childList: true, subtree: true });
3380
+ }
3381
+
3382
+ #removeNavButtons() {
3383
+ this.#navStart?.remove();
3384
+ this.#navEnd?.remove();
3385
+ this.#navStart = null;
3386
+ this.#navEnd = null;
3387
+ this.classList.remove("overflow-start", "overflow-end");
3388
+ }
3389
+
3390
+ #createNavButtons() {
3391
+ if (
3392
+ this.#navStart &&
3393
+ this.#navEnd &&
3394
+ this.contains(this.#navStart) &&
3395
+ this.contains(this.#navEnd)
3396
+ ) {
3397
+ return;
3398
+ }
3399
+
3400
+ this.#navStart?.remove();
3401
+ this.#navEnd?.remove();
3402
+
3403
+ const buttons = createFigOverflowButtons({
3404
+ owner: "tabs",
3405
+ onStart: () => figScrollOverflowPage(this.#scrollElement, "x", -1),
3406
+ onEnd: () => figScrollOverflowPage(this.#scrollElement, "x", 1),
3407
+ });
3408
+ this.#navStart = buttons.start;
3409
+ this.#navEnd = buttons.end;
3410
+ this.prepend(this.#navStart);
3411
+ this.append(this.#navEnd);
3179
3412
  }
3180
3413
 
3181
3414
  #handleKeyDown(event) {
@@ -3215,6 +3448,7 @@ class FigTabs extends HTMLElement {
3215
3448
  if (val) this.setAttribute("value", val);
3216
3449
  tabs[newIndex].focus();
3217
3450
  this.#syncTabIndexes();
3451
+ this.#scrollSelectedTabIntoView(tabs[newIndex]);
3218
3452
  }
3219
3453
  }
3220
3454
 
@@ -3237,6 +3471,7 @@ class FigTabs extends HTMLElement {
3237
3471
  }
3238
3472
  }
3239
3473
  this.#syncTabIndexes();
3474
+ this.#scrollSelectedTabIntoView(this.selectedTab);
3240
3475
  }
3241
3476
 
3242
3477
  attributeChangedCallback(name, oldValue, newValue) {
@@ -3268,6 +3503,7 @@ class FigTabs extends HTMLElement {
3268
3503
  const val = target.getAttribute("value");
3269
3504
  if (val) this.setAttribute("value", val);
3270
3505
  this.#syncTabIndexes();
3506
+ this.#scrollSelectedTabIntoView(target);
3271
3507
  }
3272
3508
  }
3273
3509
  customElements.define("fig-tabs", FigTabs);
@@ -14011,9 +14247,11 @@ class FigChooser extends HTMLElement {
14011
14247
  #boundSyncOverflow = this.#syncOverflow.bind(this);
14012
14248
  #mutationObserver = null;
14013
14249
  #resizeObserver = null;
14250
+ #scroller = null;
14014
14251
  #navStart = null;
14015
14252
  #navEnd = null;
14016
14253
  #dragState = null;
14254
+ #isStructuring = false;
14017
14255
 
14018
14256
  constructor() {
14019
14257
  super();
@@ -14046,7 +14284,43 @@ class FigChooser extends HTMLElement {
14046
14284
  }
14047
14285
 
14048
14286
  get choices() {
14049
- return Array.from(this.querySelectorAll(this.#choiceSelector));
14287
+ return Array.from((this.#scroller || this).querySelectorAll(this.#choiceSelector));
14288
+ }
14289
+
14290
+ get #scrollElement() {
14291
+ return this.#scroller || this;
14292
+ }
14293
+
14294
+ get scrollTop() {
14295
+ return this.#scrollElement.scrollTop;
14296
+ }
14297
+
14298
+ set scrollTop(value) {
14299
+ this.#scrollElement.scrollTop = value;
14300
+ }
14301
+
14302
+ get scrollLeft() {
14303
+ return this.#scrollElement.scrollLeft;
14304
+ }
14305
+
14306
+ set scrollLeft(value) {
14307
+ this.#scrollElement.scrollLeft = value;
14308
+ }
14309
+
14310
+ get scrollWidth() {
14311
+ return this.#scrollElement.scrollWidth;
14312
+ }
14313
+
14314
+ get scrollHeight() {
14315
+ return this.#scrollElement.scrollHeight;
14316
+ }
14317
+
14318
+ scrollTo(...args) {
14319
+ this.#scrollElement.scrollTo(...args);
14320
+ }
14321
+
14322
+ scrollBy(...args) {
14323
+ this.#scrollElement.scrollBy(...args);
14050
14324
  }
14051
14325
 
14052
14326
  get selectedChoice() {
@@ -14086,9 +14360,10 @@ class FigChooser extends HTMLElement {
14086
14360
 
14087
14361
  connectedCallback() {
14088
14362
  this.setAttribute("role", "listbox");
14363
+ this.#ensureScroller();
14089
14364
  this.addEventListener("click", this.#boundHandleClick);
14090
14365
  this.addEventListener("keydown", this.#boundHandleKeyDown);
14091
- this.addEventListener("scroll", this.#boundSyncOverflow);
14366
+ this.#scrollElement.addEventListener("scroll", this.#boundSyncOverflow);
14092
14367
  this.#applyOverflowMode();
14093
14368
  this.#setupDrag();
14094
14369
  this.#startObserver();
@@ -14132,7 +14407,7 @@ class FigChooser extends HTMLElement {
14132
14407
  disconnectedCallback() {
14133
14408
  this.removeEventListener("click", this.#boundHandleClick);
14134
14409
  this.removeEventListener("keydown", this.#boundHandleKeyDown);
14135
- this.removeEventListener("scroll", this.#boundSyncOverflow);
14410
+ this.#scrollElement.removeEventListener("scroll", this.#boundSyncOverflow);
14136
14411
  this.#teardownDrag();
14137
14412
  this.#mutationObserver?.disconnect();
14138
14413
  this.#mutationObserver = null;
@@ -14308,25 +14583,7 @@ class FigChooser extends HTMLElement {
14308
14583
  #syncOverflow() {
14309
14584
  if (this.#overflowMode === "scrollbar") return;
14310
14585
  const isHorizontal = this.getAttribute("layout") === "horizontal";
14311
- const threshold = 2;
14312
-
14313
- if (isHorizontal) {
14314
- const scrollable = this.scrollWidth - this.clientWidth > threshold;
14315
- const atStart = !scrollable || this.scrollLeft <= threshold;
14316
- const atEnd =
14317
- !scrollable ||
14318
- this.scrollLeft + this.clientWidth >= this.scrollWidth - threshold;
14319
- this.classList.toggle("overflow-start", !atStart);
14320
- this.classList.toggle("overflow-end", !atEnd);
14321
- } else {
14322
- const scrollable = this.scrollHeight - this.clientHeight > threshold;
14323
- const atStart = !scrollable || this.scrollTop <= threshold;
14324
- const atEnd =
14325
- !scrollable ||
14326
- this.scrollTop + this.clientHeight >= this.scrollHeight - threshold;
14327
- this.classList.toggle("overflow-start", !atStart);
14328
- this.classList.toggle("overflow-end", !atEnd);
14329
- }
14586
+ figSyncOverflowState(this, this.#scrollElement, isHorizontal ? "x" : "y");
14330
14587
  }
14331
14588
 
14332
14589
  #startResizeObserver() {
@@ -14335,28 +14592,30 @@ class FigChooser extends HTMLElement {
14335
14592
  this.#syncOverflow();
14336
14593
  });
14337
14594
  this.#resizeObserver.observe(this);
14595
+ if (this.#scroller) this.#resizeObserver.observe(this.#scroller);
14338
14596
  }
14339
14597
 
14340
14598
  #setupDrag() {
14341
14599
  if (this.#dragState?.bound) return;
14342
14600
  if (!this.#dragEnabled) return;
14601
+ const scrollEl = this.#scrollElement;
14343
14602
 
14344
14603
  const onPointerDown = (e) => {
14345
14604
  if (e.button !== 0) return;
14346
14605
  const isHorizontal = this.getAttribute("layout") === "horizontal";
14347
14606
  const hasOverflow = isHorizontal
14348
- ? this.scrollWidth > this.clientWidth
14349
- : this.scrollHeight > this.clientHeight;
14607
+ ? scrollEl.scrollWidth > scrollEl.clientWidth
14608
+ : scrollEl.scrollHeight > scrollEl.clientHeight;
14350
14609
  if (!hasOverflow) return;
14351
14610
 
14352
14611
  this.#dragState.active = true;
14353
14612
  this.#dragState.didDrag = false;
14354
14613
  this.#dragState.startX = e.clientX;
14355
14614
  this.#dragState.startY = e.clientY;
14356
- this.#dragState.scrollLeft = this.scrollLeft;
14357
- this.#dragState.scrollTop = this.scrollTop;
14358
- this.style.cursor = "grab";
14359
- this.style.userSelect = "none";
14615
+ this.#dragState.scrollLeft = scrollEl.scrollLeft;
14616
+ this.#dragState.scrollTop = scrollEl.scrollTop;
14617
+ scrollEl.style.cursor = "grab";
14618
+ scrollEl.style.userSelect = "none";
14360
14619
  };
14361
14620
 
14362
14621
  const onPointerMove = (e) => {
@@ -14367,16 +14626,16 @@ class FigChooser extends HTMLElement {
14367
14626
 
14368
14627
  if (!this.#dragState.didDrag && Math.abs(isHorizontal ? dx : dy) > 3) {
14369
14628
  this.#dragState.didDrag = true;
14370
- this.style.cursor = "grabbing";
14371
- this.setPointerCapture(e.pointerId);
14629
+ scrollEl.style.cursor = "grabbing";
14630
+ scrollEl.setPointerCapture(e.pointerId);
14372
14631
  }
14373
14632
 
14374
14633
  if (!this.#dragState.didDrag) return;
14375
14634
 
14376
14635
  if (isHorizontal) {
14377
- this.scrollLeft = this.#dragState.scrollLeft - dx;
14636
+ scrollEl.scrollLeft = this.#dragState.scrollLeft - dx;
14378
14637
  } else {
14379
- this.scrollTop = this.#dragState.scrollTop - dy;
14638
+ scrollEl.scrollTop = this.#dragState.scrollTop - dy;
14380
14639
  }
14381
14640
  };
14382
14641
 
@@ -14385,11 +14644,11 @@ class FigChooser extends HTMLElement {
14385
14644
  const wasDrag = this.#dragState.didDrag;
14386
14645
  this.#dragState.active = false;
14387
14646
  this.#dragState.didDrag = false;
14388
- this.style.cursor = "";
14389
- this.style.userSelect = "";
14647
+ scrollEl.style.cursor = "";
14648
+ scrollEl.style.userSelect = "";
14390
14649
  if (e.pointerId !== undefined) {
14391
14650
  try {
14392
- this.releasePointerCapture(e.pointerId);
14651
+ scrollEl.releasePointerCapture(e.pointerId);
14393
14652
  } catch {}
14394
14653
  }
14395
14654
  if (wasDrag) {
@@ -14429,9 +14688,10 @@ class FigChooser extends HTMLElement {
14429
14688
  onPointerUp,
14430
14689
  onClick,
14431
14690
  onPointerUpCapture,
14691
+ scrollEl,
14432
14692
  };
14433
14693
 
14434
- this.addEventListener("pointerdown", onPointerDown);
14694
+ scrollEl.addEventListener("pointerdown", onPointerDown);
14435
14695
  window.addEventListener("pointermove", onPointerMove);
14436
14696
  window.addEventListener("pointerup", onPointerUp);
14437
14697
  this.addEventListener("pointerup", onPointerUpCapture, true);
@@ -14440,7 +14700,7 @@ class FigChooser extends HTMLElement {
14440
14700
 
14441
14701
  #teardownDrag() {
14442
14702
  if (!this.#dragState?.bound) return;
14443
- this.removeEventListener("pointerdown", this.#dragState.onPointerDown);
14703
+ this.#dragState.scrollEl.removeEventListener("pointerdown", this.#dragState.onPointerDown);
14444
14704
  window.removeEventListener("pointermove", this.#dragState.onPointerMove);
14445
14705
  window.removeEventListener("pointerup", this.#dragState.onPointerUp);
14446
14706
  this.removeEventListener(
@@ -14449,12 +14709,52 @@ class FigChooser extends HTMLElement {
14449
14709
  true,
14450
14710
  );
14451
14711
  this.removeEventListener("click", this.#dragState.onClick, true);
14452
- this.style.cursor = "";
14453
- this.style.userSelect = "";
14712
+ this.#dragState.scrollEl.style.cursor = "";
14713
+ this.#dragState.scrollEl.style.userSelect = "";
14454
14714
  this.#dragState = null;
14455
14715
  }
14456
14716
 
14717
+ #ensureScroller() {
14718
+ if (this.#isStructuring) return;
14719
+ const existing = this.querySelector(":scope > [data-fig-chooser-scroll]");
14720
+ if (existing && existing === this.#scroller) return;
14721
+
14722
+ this.#isStructuring = true;
14723
+ try {
14724
+ const previousScroller = this.#scroller;
14725
+ if (previousScroller && previousScroller !== existing) {
14726
+ previousScroller.removeEventListener("scroll", this.#boundSyncOverflow);
14727
+ }
14728
+
14729
+ const scroller = existing || document.createElement("div");
14730
+ scroller.className = "fig-chooser-scroll";
14731
+ scroller.dataset.figChooserScroll = "";
14732
+
14733
+ const nodes = Array.from(this.childNodes).filter((node) => {
14734
+ if (node === scroller) return false;
14735
+ if (node.nodeType !== Node.ELEMENT_NODE) return true;
14736
+ return !node.hasAttribute("data-fig-chooser-nav");
14737
+ });
14738
+
14739
+ for (const node of nodes) {
14740
+ scroller.appendChild(node);
14741
+ }
14742
+
14743
+ if (!existing) {
14744
+ this.prepend(scroller);
14745
+ }
14746
+
14747
+ this.#scroller = scroller;
14748
+ this.#scroller.removeEventListener("scroll", this.#boundSyncOverflow);
14749
+ this.#scroller.addEventListener("scroll", this.#boundSyncOverflow);
14750
+ if (this.#resizeObserver) this.#resizeObserver.observe(this.#scroller);
14751
+ } finally {
14752
+ this.#isStructuring = false;
14753
+ }
14754
+ }
14755
+
14457
14756
  #applyOverflowMode() {
14757
+ this.#ensureScroller();
14458
14758
  if (this.#overflowMode === "scrollbar") {
14459
14759
  this.#removeNavButtons();
14460
14760
  } else {
@@ -14471,36 +14771,30 @@ class FigChooser extends HTMLElement {
14471
14771
  }
14472
14772
 
14473
14773
  #createNavButtons() {
14474
- if (this.#navStart) return;
14475
-
14476
- const makeChevron = () => {
14477
- return createFigIcon("chevron", {
14478
- size: "small",
14479
- className: "fig-chooser-nav-chevron",
14480
- });
14481
- };
14482
-
14483
- this.#navStart = document.createElement("button");
14484
- this.#navStart.className = "fig-chooser-nav-start";
14485
- this.#navStart.setAttribute("tabindex", "-1");
14486
- this.#navStart.setAttribute("aria-label", "Scroll back");
14487
- this.#navStart.appendChild(makeChevron());
14488
-
14489
- this.#navEnd = document.createElement("button");
14490
- this.#navEnd.className = "fig-chooser-nav-end";
14491
- this.#navEnd.setAttribute("tabindex", "-1");
14492
- this.#navEnd.setAttribute("aria-label", "Scroll forward");
14493
- this.#navEnd.appendChild(makeChevron());
14774
+ if (
14775
+ this.#navStart &&
14776
+ this.#navEnd &&
14777
+ this.contains(this.#navStart) &&
14778
+ this.contains(this.#navEnd)
14779
+ ) {
14780
+ return;
14781
+ }
14494
14782
 
14495
- this.#navStart.addEventListener("pointerdown", (e) => {
14496
- e.stopPropagation();
14497
- this.#scrollByPage(-1);
14498
- });
14783
+ this.#navStart?.remove();
14784
+ this.#navEnd?.remove();
14785
+ this.#navStart = null;
14786
+ this.#navEnd = null;
14499
14787
 
14500
- this.#navEnd.addEventListener("pointerdown", (e) => {
14501
- e.stopPropagation();
14502
- this.#scrollByPage(1);
14788
+ const buttons = createFigOverflowButtons({
14789
+ owner: "chooser",
14790
+ startClass: "fig-chooser-nav-start",
14791
+ endClass: "fig-chooser-nav-end",
14792
+ chevronClass: "fig-chooser-nav-chevron",
14793
+ onStart: () => this.#scrollByPage(-1),
14794
+ onEnd: () => this.#scrollByPage(1),
14503
14795
  });
14796
+ this.#navStart = buttons.start;
14797
+ this.#navEnd = buttons.end;
14504
14798
 
14505
14799
  this.prepend(this.#navStart);
14506
14800
  this.append(this.#navEnd);
@@ -14508,49 +14802,48 @@ class FigChooser extends HTMLElement {
14508
14802
 
14509
14803
  #scrollByPage(direction) {
14510
14804
  const isHorizontal = this.getAttribute("layout") === "horizontal";
14511
- const pageSize = isHorizontal ? this.clientWidth : this.clientHeight;
14512
- const scrollAmount = pageSize * 0.8 * direction;
14513
-
14514
- this.scrollBy({
14515
- [isHorizontal ? "left" : "top"]: scrollAmount,
14516
- behavior: "smooth",
14517
- });
14805
+ const scrollEl = this.#scrollElement;
14806
+ figScrollOverflowPage(scrollEl, isHorizontal ? "x" : "y", direction);
14518
14807
  }
14519
14808
 
14520
14809
  #scrollToChoice(el, behavior = "smooth") {
14521
14810
  if (!el) return;
14522
14811
  requestAnimationFrame(() => {
14523
14812
  if (!el.isConnected) return;
14524
- const overflowY = this.scrollHeight > this.clientHeight;
14525
- const overflowX = this.scrollWidth > this.clientWidth;
14813
+ const scrollEl = this.#scrollElement;
14814
+ const overflowY = scrollEl.scrollHeight > scrollEl.clientHeight;
14815
+ const overflowX = scrollEl.scrollWidth > scrollEl.clientWidth;
14526
14816
  if (!overflowX && !overflowY) return;
14527
14817
 
14528
14818
  const choiceRect = el.getBoundingClientRect();
14529
- const hostRect = this.getBoundingClientRect();
14819
+ const hostRect = scrollEl.getBoundingClientRect();
14530
14820
  const options = { behavior };
14531
14821
 
14532
14822
  if (overflowY) {
14533
14823
  const choiceCenter =
14534
- choiceRect.top - hostRect.top + this.scrollTop + choiceRect.height / 2;
14535
- options.top = choiceCenter - this.clientHeight / 2;
14824
+ choiceRect.top - hostRect.top + scrollEl.scrollTop + choiceRect.height / 2;
14825
+ options.top = choiceCenter - scrollEl.clientHeight / 2;
14536
14826
  }
14537
14827
 
14538
14828
  if (overflowX) {
14539
14829
  const choiceCenter =
14540
14830
  choiceRect.left -
14541
14831
  hostRect.left +
14542
- this.scrollLeft +
14832
+ scrollEl.scrollLeft +
14543
14833
  choiceRect.width / 2;
14544
- options.left = choiceCenter - this.clientWidth / 2;
14834
+ options.left = choiceCenter - scrollEl.clientWidth / 2;
14545
14835
  }
14546
14836
 
14547
- this.scrollTo(options);
14837
+ scrollEl.scrollTo(options);
14548
14838
  });
14549
14839
  }
14550
14840
 
14551
14841
  #startObserver() {
14552
14842
  this.#mutationObserver?.disconnect();
14553
14843
  this.#mutationObserver = new MutationObserver(() => {
14844
+ if (this.#isStructuring) return;
14845
+ this.#ensureScroller();
14846
+ this.#applyOverflowMode();
14554
14847
  const choices = this.choices;
14555
14848
  if (this.#selectedChoice && !choices.includes(this.#selectedChoice)) {
14556
14849
  this.#selectedChoice = null;
@@ -14558,6 +14851,7 @@ class FigChooser extends HTMLElement {
14558
14851
  } else if (!this.#selectedChoice && choices.length) {
14559
14852
  this.#syncSelection();
14560
14853
  }
14854
+ requestAnimationFrame(() => this.#syncOverflow());
14561
14855
  });
14562
14856
  this.#mutationObserver.observe(this, { childList: true, subtree: true });
14563
14857
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.4.8",
3
+ "version": "6.5.1",
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",