@rogieking/figui3 6.22.0 → 6.23.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
@@ -105,9 +105,12 @@ function createFigOverflowButtons({
105
105
  startClass = "",
106
106
  endClass = "",
107
107
  chevronClass = "",
108
+ startLabel = "Scroll back",
109
+ endLabel = "Scroll forward",
108
110
  } = {}) {
109
111
  const makeButton = (direction, onPointerDown) => {
110
112
  const button = document.createElement("button");
113
+ button.type = "button";
111
114
  button.className = [
112
115
  "fig-overflow",
113
116
  `fig-overflow-${direction}`,
@@ -118,7 +121,10 @@ function createFigOverflowButtons({
118
121
  button.dataset.figOverflow = direction;
119
122
  if (owner) button.setAttribute(`data-fig-${owner}-nav`, direction);
120
123
  button.setAttribute("tabindex", "-1");
121
- button.setAttribute("aria-label", direction === "start" ? "Scroll back" : "Scroll forward");
124
+ button.setAttribute(
125
+ "aria-label",
126
+ direction === "start" ? startLabel : endLabel,
127
+ );
122
128
  button.appendChild(
123
129
  createFigIcon("chevron", {
124
130
  size: "small",
@@ -164,6 +170,48 @@ function figScrollOverflowPage(scrollEl, axis = "x", direction = 1) {
164
170
  });
165
171
  }
166
172
 
173
+ function figScrollElementToCenter(
174
+ scrollEl,
175
+ element,
176
+ axis = "y",
177
+ behavior = "auto",
178
+ ) {
179
+ if (!scrollEl || !element || !scrollEl.contains(element)) return;
180
+ requestAnimationFrame(() => {
181
+ if (!scrollEl.isConnected || !element.isConnected) return;
182
+ const isHorizontal = axis === "x";
183
+ const scrollSize = isHorizontal
184
+ ? scrollEl.scrollWidth
185
+ : scrollEl.scrollHeight;
186
+ const clientSize = isHorizontal
187
+ ? scrollEl.clientWidth
188
+ : scrollEl.clientHeight;
189
+ if (scrollSize <= clientSize + 1) {
190
+ figSyncOverflowState(scrollEl, scrollEl, axis);
191
+ return;
192
+ }
193
+ const elementRect = element.getBoundingClientRect();
194
+ const hostRect = scrollEl.getBoundingClientRect();
195
+ const currentScroll = isHorizontal
196
+ ? scrollEl.scrollLeft
197
+ : scrollEl.scrollTop;
198
+ const elementStart =
199
+ (isHorizontal ? elementRect.left - hostRect.left : elementRect.top - hostRect.top) +
200
+ currentScroll;
201
+ const elementSize = isHorizontal ? elementRect.width : elementRect.height;
202
+ const maxScroll = scrollSize - clientSize;
203
+ const nextScroll = Math.max(
204
+ 0,
205
+ Math.min(elementStart + elementSize / 2 - clientSize / 2, maxScroll),
206
+ );
207
+ scrollEl.scrollTo({
208
+ [isHorizontal ? "left" : "top"]: nextScroll,
209
+ behavior,
210
+ });
211
+ figSyncOverflowState(scrollEl, scrollEl, axis);
212
+ });
213
+ }
214
+
167
215
  function hasFigFillPicker() {
168
216
  return typeof customElements !== "undefined" && !!customElements.get("fig-fill-picker");
169
217
  }
@@ -234,7 +282,7 @@ function figUniqueId() {
234
282
  }
235
283
 
236
284
  /** Zero-size portal for fixed overlays so they never affect body layout metrics. */
237
- function figGetOverlayRoot(source) {
285
+ function figGetOverlayRoot() {
238
286
  if (!document.body) return null;
239
287
  const attr = "data-figui-overlay-root";
240
288
  let root = document.body.querySelector(`:scope > [${attr}]`);
@@ -243,40 +291,9 @@ function figGetOverlayRoot(source) {
243
291
  root.setAttribute(attr, "");
244
292
  document.body.append(root);
245
293
  }
246
- figSyncOverlayThemeFromSource(root, source);
247
294
  return root;
248
295
  }
249
296
 
250
- /** Copy PropsKit / FigUI theme onto the overlay portal so portaled popups stay themed. */
251
- function figSyncOverlayThemeFromSource(overlayRoot, source) {
252
- if (!overlayRoot) return;
253
- const panel =
254
- (source instanceof Element
255
- ? source.closest(".figui-root")
256
- : null) || document.querySelector(".figui-root");
257
- if (!panel) return;
258
-
259
- const themeAttr = panel.getAttribute("theme");
260
- const theme =
261
- themeAttr === "light" || themeAttr === "dark" || themeAttr === "system"
262
- ? themeAttr
263
- : panel.classList.contains("figma-dark")
264
- ? "dark"
265
- : panel.classList.contains("figma-light")
266
- ? "light"
267
- : "system";
268
-
269
- overlayRoot.classList.toggle("figma-light", theme === "light");
270
- overlayRoot.classList.toggle("figma-dark", theme === "dark");
271
- if (theme === "system") {
272
- overlayRoot.style.setProperty("color-scheme", "light dark");
273
- } else {
274
- overlayRoot.style.setProperty("color-scheme", theme);
275
- }
276
- if (themeAttr) overlayRoot.setAttribute("theme", theme);
277
- else overlayRoot.removeAttribute("theme");
278
- }
279
-
280
297
  let _figZCounter = 10000;
281
298
  function figGetHighestZIndex() {
282
299
  return _figZCounter++;
@@ -1065,13 +1082,13 @@ class FigTooltip extends HTMLElement {
1065
1082
  // - Without popover support, fall back to today's behavior: nearest open
1066
1083
  // <dialog> ancestor if present, else document.body.
1067
1084
  if (supportsPopover) {
1068
- (figGetOverlayRoot(this) ?? document.body).append(this.popup);
1085
+ (figGetOverlayRoot() ?? document.body).append(this.popup);
1069
1086
  } else {
1070
1087
  const parentDialog = this.closest("dialog");
1071
1088
  if (parentDialog && parentDialog.open) {
1072
1089
  parentDialog.append(this.popup);
1073
1090
  } else {
1074
- (figGetOverlayRoot(this) ?? document.body).append(this.popup);
1091
+ (figGetOverlayRoot() ?? document.body).append(this.popup);
1075
1092
  }
1076
1093
  }
1077
1094
 
@@ -1463,13 +1480,13 @@ class FigTooltip extends HTMLElement {
1463
1480
  popup.append(content);
1464
1481
 
1465
1482
  if (supportsPopover) {
1466
- (figGetOverlayRoot(anchor) ?? document.body).append(popup);
1483
+ (figGetOverlayRoot() ?? document.body).append(popup);
1467
1484
  } else {
1468
1485
  const parentDialog = anchor.closest?.("dialog");
1469
1486
  if (parentDialog && parentDialog.open) {
1470
1487
  parentDialog.append(popup);
1471
1488
  } else {
1472
- (figGetOverlayRoot(anchor) ?? document.body).append(popup);
1489
+ (figGetOverlayRoot() ?? document.body).append(popup);
1473
1490
  }
1474
1491
  }
1475
1492
 
@@ -5220,57 +5237,6 @@ class FigSelectOption extends HTMLElement {
5220
5237
  }
5221
5238
  figDefineElement("fig-select-option", FigSelectOption);
5222
5239
 
5223
- function figSelectSyncOverflowState(host, scrollEl, threshold = 2) {
5224
- if (!host || !scrollEl) return false;
5225
- const scrollable = scrollEl.scrollHeight - scrollEl.clientHeight > threshold;
5226
- const atStart = !scrollable || scrollEl.scrollTop <= threshold;
5227
- const atEnd =
5228
- !scrollable ||
5229
- scrollEl.scrollTop + scrollEl.clientHeight >=
5230
- scrollEl.scrollHeight - threshold;
5231
- host.classList.toggle("overflow-start", !atStart);
5232
- host.classList.toggle("overflow-end", !atEnd);
5233
- return scrollable;
5234
- }
5235
-
5236
- function figSelectScrollOverflowPage(scrollEl, direction = 1) {
5237
- if (!scrollEl) return;
5238
- scrollEl.scrollBy({
5239
- top: scrollEl.clientHeight * 0.8 * direction,
5240
- behavior: "smooth",
5241
- });
5242
- }
5243
-
5244
- function figSelectCreateOverflowButtons({ onStart, onEnd } = {}) {
5245
- const makeButton = (direction, onClick) => {
5246
- const button = document.createElement("button");
5247
- button.type = "button";
5248
- button.className = `fig-overflow fig-overflow-${direction}`;
5249
- button.dataset.figOverflow = direction;
5250
- button.setAttribute("data-fig-select-nav", direction);
5251
- button.setAttribute("tabindex", "-1");
5252
- button.setAttribute(
5253
- "aria-label",
5254
- direction === "start" ? "Scroll up" : "Scroll down",
5255
- );
5256
- const icon = document.createElement("fig-icon");
5257
- icon.setAttribute("name", "chevron");
5258
- icon.setAttribute("size", "small");
5259
- icon.className = "fig-overflow-chevron";
5260
- button.appendChild(icon);
5261
- button.addEventListener("click", (event) => {
5262
- event.preventDefault();
5263
- event.stopPropagation();
5264
- onClick?.(event);
5265
- });
5266
- return button;
5267
- };
5268
- return {
5269
- start: makeButton("start", onStart),
5270
- end: makeButton("end", onEnd),
5271
- };
5272
- }
5273
-
5274
5240
  /** Light-DOM panel wrapper projected into fig-select's popup; owns overflow buttons. */
5275
5241
  class FigSelectOptions extends HTMLElement {
5276
5242
  #navStart = null;
@@ -5297,31 +5263,11 @@ class FigSelectOptions extends HTMLElement {
5297
5263
  }
5298
5264
 
5299
5265
  syncOverflow() {
5300
- return figSelectSyncOverflowState(this, this);
5266
+ return figSyncOverflowState(this, this, "y");
5301
5267
  }
5302
5268
 
5303
5269
  scrollToOption(option, behavior = "auto") {
5304
- if (!option || !this.contains(option)) return;
5305
- requestAnimationFrame(() => {
5306
- if (!option.isConnected) return;
5307
- if (this.scrollHeight <= this.clientHeight + 1) {
5308
- this.syncOverflow();
5309
- return;
5310
- }
5311
- const optionRect = option.getBoundingClientRect();
5312
- const hostRect = this.getBoundingClientRect();
5313
- const optionTop = optionRect.top - hostRect.top + this.scrollTop;
5314
- const maxScroll = this.scrollHeight - this.clientHeight;
5315
- const top = Math.max(
5316
- 0,
5317
- Math.min(
5318
- optionTop + optionRect.height / 2 - this.clientHeight / 2,
5319
- maxScroll,
5320
- ),
5321
- );
5322
- this.scrollTo({ top, behavior });
5323
- this.syncOverflow();
5324
- });
5270
+ figScrollElementToCenter(this, option, "y", behavior);
5325
5271
  }
5326
5272
 
5327
5273
  #unwrapLegacyChooser() {
@@ -5343,9 +5289,12 @@ class FigSelectOptions extends HTMLElement {
5343
5289
  return;
5344
5290
  }
5345
5291
  this.#removeNavButtons();
5346
- const buttons = figSelectCreateOverflowButtons({
5347
- onStart: () => figSelectScrollOverflowPage(this, -1),
5348
- onEnd: () => figSelectScrollOverflowPage(this, 1),
5292
+ const buttons = createFigOverflowButtons({
5293
+ owner: "select",
5294
+ startLabel: "Scroll up",
5295
+ endLabel: "Scroll down",
5296
+ onStart: () => figScrollOverflowPage(this, "y", -1),
5297
+ onEnd: () => figScrollOverflowPage(this, "y", 1),
5349
5298
  });
5350
5299
  this.#navStart = buttons.start;
5351
5300
  this.#navEnd = buttons.end;
@@ -19366,14 +19315,19 @@ figDefineElement("fig-menu-separator", FigMenuSeparator);
19366
19315
 
19367
19316
  class FigMenu extends HTMLElement {
19368
19317
  #popup = null;
19318
+ #panel = null;
19369
19319
  #trigger = null;
19370
19320
  #virtualAnchor = null;
19371
19321
  #observer = null;
19322
+ #resizeObserver = null;
19323
+ #navStart = null;
19324
+ #navEnd = null;
19372
19325
  #boundTriggerClick;
19373
19326
  #boundTriggerContextMenu;
19374
19327
  #boundPopupClick;
19375
19328
  #boundMenuKeydown;
19376
19329
  #boundPopupClose;
19330
+ #boundSyncOverflow = this.#syncOverflow.bind(this);
19377
19331
  #focusedIndex = -1;
19378
19332
 
19379
19333
  static get observedAttributes() {
@@ -19415,6 +19369,7 @@ class FigMenu extends HTMLElement {
19415
19369
  this.#createPopup();
19416
19370
  this.#moveItemsToPopup();
19417
19371
  this.#setupListeners();
19372
+ this.#setupOverflow();
19418
19373
  this.#setupObserver();
19419
19374
  this.#syncDisabled();
19420
19375
 
@@ -19425,6 +19380,7 @@ class FigMenu extends HTMLElement {
19425
19380
 
19426
19381
  disconnectedCallback() {
19427
19382
  this.#teardownListeners();
19383
+ this.#teardownOverflow();
19428
19384
  document.removeEventListener("keydown", this.#boundMenuKeydown, true);
19429
19385
  if (this.#observer) {
19430
19386
  this.#observer.disconnect();
@@ -19433,13 +19389,14 @@ class FigMenu extends HTMLElement {
19433
19389
  if (this.#popup) {
19434
19390
  this.#popup.removeEventListener("close", this.#boundPopupClose);
19435
19391
  const items = Array.from(
19436
- this.#popup.querySelectorAll(
19392
+ this.#panel?.querySelectorAll(
19437
19393
  ":scope > fig-menu-item, :scope > fig-menu-separator",
19438
- ),
19394
+ ) ?? [],
19439
19395
  );
19440
19396
  for (const item of items) this.insertBefore(item, this.#popup);
19441
19397
  this.#popup.remove();
19442
19398
  this.#popup = null;
19399
+ this.#panel = null;
19443
19400
  }
19444
19401
  }
19445
19402
 
@@ -19487,6 +19444,7 @@ class FigMenu extends HTMLElement {
19487
19444
  #createPopup() {
19488
19445
  this.#popup = document.createElement("dialog", { is: "fig-popup" });
19489
19446
  this.#popup.setAttribute("is", "fig-popup");
19447
+ this.#popup.classList.add("fig-menu-popup");
19490
19448
  this.#popup.setAttribute("theme", "menu");
19491
19449
  this.#popup.setAttribute("role", "menu");
19492
19450
  this.#popup.setAttribute("id", this.#popup.getAttribute("id") || figUniqueId());
@@ -19504,16 +19462,21 @@ class FigMenu extends HTMLElement {
19504
19462
  this.#popup.anchor = this.#trigger;
19505
19463
  }
19506
19464
 
19465
+ this.#panel = document.createElement("div");
19466
+ this.#panel.className = "fig-menu-options";
19467
+ this.#panel.setAttribute("role", "presentation");
19468
+ this.#popup.appendChild(this.#panel);
19507
19469
  this.#popup.addEventListener("close", this.#boundPopupClose);
19508
19470
  this.appendChild(this.#popup);
19509
19471
  }
19510
19472
 
19511
19473
  #moveItemsToPopup() {
19474
+ if (!this.#panel) return;
19512
19475
  const items = Array.from(this.querySelectorAll(
19513
19476
  ":scope > fig-menu-item, :scope > fig-menu-separator"
19514
19477
  ));
19515
19478
  for (const item of items) {
19516
- this.#popup.appendChild(item);
19479
+ this.#panel.appendChild(item);
19517
19480
  }
19518
19481
  }
19519
19482
 
@@ -19553,7 +19516,7 @@ class FigMenu extends HTMLElement {
19553
19516
  (node.tagName === "FIG-MENU-ITEM" || node.tagName === "FIG-MENU-SEPARATOR") &&
19554
19517
  node.parentElement === this
19555
19518
  ) {
19556
- this.#popup.appendChild(node);
19519
+ this.#panel?.appendChild(node);
19557
19520
  } else if (!this.#trigger && node.parentElement === this) {
19558
19521
  this.#detectTrigger();
19559
19522
  if (this.#trigger) {
@@ -19568,13 +19531,68 @@ class FigMenu extends HTMLElement {
19568
19531
  }
19569
19532
  }
19570
19533
  }
19534
+ this.#syncOverflow();
19571
19535
  });
19572
19536
  this.#observer.observe(this, { childList: true });
19573
19537
  }
19574
19538
 
19539
+ #setupOverflow() {
19540
+ if (!this.#panel) return;
19541
+ this.#ensureNavButtons();
19542
+ this.#panel.addEventListener("scroll", this.#boundSyncOverflow, {
19543
+ passive: true,
19544
+ });
19545
+ this.#resizeObserver?.disconnect();
19546
+ this.#resizeObserver = new ResizeObserver(() => this.#syncOverflow());
19547
+ this.#resizeObserver.observe(this.#panel);
19548
+ requestAnimationFrame(() => this.#syncOverflow());
19549
+ }
19550
+
19551
+ #teardownOverflow() {
19552
+ this.#panel?.removeEventListener("scroll", this.#boundSyncOverflow);
19553
+ this.#resizeObserver?.disconnect();
19554
+ this.#resizeObserver = null;
19555
+ this.#removeNavButtons();
19556
+ }
19557
+
19558
+ #syncOverflow() {
19559
+ return figSyncOverflowState(this.#panel, this.#panel, "y");
19560
+ }
19561
+
19562
+ #ensureNavButtons() {
19563
+ if (
19564
+ this.#navStart &&
19565
+ this.#navEnd &&
19566
+ this.#panel?.contains(this.#navStart) &&
19567
+ this.#panel?.contains(this.#navEnd)
19568
+ ) {
19569
+ return;
19570
+ }
19571
+ this.#removeNavButtons();
19572
+ const buttons = createFigOverflowButtons({
19573
+ owner: "menu",
19574
+ startLabel: "Scroll up",
19575
+ endLabel: "Scroll down",
19576
+ onStart: () => figScrollOverflowPage(this.#panel, "y", -1),
19577
+ onEnd: () => figScrollOverflowPage(this.#panel, "y", 1),
19578
+ });
19579
+ this.#navStart = buttons.start;
19580
+ this.#navEnd = buttons.end;
19581
+ this.#panel.prepend(this.#navStart);
19582
+ this.#panel.append(this.#navEnd);
19583
+ }
19584
+
19585
+ #removeNavButtons() {
19586
+ this.#navStart?.remove();
19587
+ this.#navEnd?.remove();
19588
+ this.#navStart = null;
19589
+ this.#navEnd = null;
19590
+ this.#panel?.classList.remove("overflow-start", "overflow-end");
19591
+ }
19592
+
19575
19593
  #getItems() {
19576
- if (!this.#popup) return [];
19577
- return Array.from(this.#popup.querySelectorAll("fig-menu-item")).filter(
19594
+ if (!this.#panel) return [];
19595
+ return Array.from(this.#panel.querySelectorAll("fig-menu-item")).filter(
19578
19596
  (item) =>
19579
19597
  !item.hasAttribute("disabled") || item.getAttribute("disabled") === "false",
19580
19598
  );
@@ -19598,7 +19616,9 @@ class FigMenu extends HTMLElement {
19598
19616
  if (!items.length) return;
19599
19617
  const wrapped = (index + items.length) % items.length;
19600
19618
  this.#focusedIndex = wrapped;
19601
- items[wrapped].focus();
19619
+ const item = items[wrapped];
19620
+ item.focus();
19621
+ figScrollElementToCenter(this.#panel, item, "y");
19602
19622
  }
19603
19623
 
19604
19624
  #syncDisabled() {
@@ -19809,6 +19829,7 @@ class FigMenu extends HTMLElement {
19809
19829
  }
19810
19830
  this.#focusedIndex = -1;
19811
19831
  requestAnimationFrame(() => {
19832
+ this.#syncOverflow();
19812
19833
  if (!this.#trigger?.matches?.(":focus-visible")) return;
19813
19834
  this.#focusItemAt(0);
19814
19835
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.22.0",
3
+ "version": "6.23.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",
@@ -19,15 +19,6 @@
19
19
  "./fig-lab.css": "./dist/fig-lab.css",
20
20
  "./base.css": "./dist/base.css",
21
21
  "./components.css": "./dist/components.css",
22
- "./propskit": {
23
- "types": "./propskit.d.ts",
24
- "default": "./dist/propskit.js"
25
- },
26
- "./propskit.js": {
27
- "types": "./propskit.d.ts",
28
- "default": "./dist/propskit.js"
29
- },
30
- "./propskit.css": "./dist/propskit.css",
31
22
  "./src/fig.js": "./fig.js",
32
23
  "./src/fig-layer.js": "./fig-layer.js",
33
24
  "./src/fig-editor.js": "./fig-editor.js",
@@ -44,9 +35,6 @@
44
35
  "fig-layer.js",
45
36
  "fig-editor.js",
46
37
  "fig-lab.js",
47
- "propskit.js",
48
- "propskit-core.js",
49
- "propskit.d.ts",
50
38
  "fig.css",
51
39
  "fig-layer.css",
52
40
  "fig-editor.css",
@@ -61,17 +49,14 @@
61
49
  ],
62
50
  "sideEffects": [
63
51
  "*.css",
64
- "./propskit.js",
65
- "./dist/propskit.js",
66
52
  "./fig.js",
67
53
  "./fig-lab.js",
68
54
  "./fig-editor.js"
69
55
  ],
70
56
  "scripts": {
71
57
  "dev": "bun --hot server.ts",
72
- "build": "bun build fig.js --minify --outdir dist && bun build fig-layer.js --minify --outdir dist && bun build fig-editor.js --minify --outdir dist --external ./fig.js --external ./fig-lab.js && bun build fig-lab.js --minify --outdir dist --external ./fig.js --external ./fig-editor.js && bun build propskit.js --minify --outdir dist --external ./fig.js --external ./fig-lab.js --external ./fig-editor.js && npm run build:css && npm run build:propskit-css",
58
+ "build": "bun build fig.js --minify --outdir dist && bun build fig-layer.js --minify --outdir dist && bun build fig-editor.js --minify --outdir dist --external ./fig.js --external ./fig-lab.js && bun build fig-lab.js --minify --outdir dist --external ./fig.js --external ./fig-editor.js && npm run build:css",
73
59
  "build:css": "node scripts/build-css.mjs",
74
- "build:propskit-css": "node scripts/build-propskit-css.mjs",
75
60
  "dev:playground": "node playground/dev.mjs",
76
61
  "build:playground": "cd playground && npm run build",
77
62
  "test": "npm run test:components",
@@ -125,17 +110,6 @@
125
110
  "playwright": "^1.58.2"
126
111
  },
127
112
  "peerDependencies": {
128
- "typescript": "^5.0.0",
129
- "react": ">=18",
130
- "vue": ">=3.3",
131
- "svelte": ">=5",
132
- "solid-js": ">=1.8"
133
- },
134
- "peerDependenciesMeta": {
135
- "react": { "optional": true },
136
- "vue": { "optional": true },
137
- "svelte": { "optional": true },
138
- "solid-js": { "optional": true },
139
- "typescript": { "optional": true }
113
+ "typescript": "^5.0.0"
140
114
  }
141
115
  }