@rogieking/figui3 6.6.6 → 6.7.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
@@ -747,9 +747,10 @@ customElements.define("fig-dropdown", FigDropdown);
747
747
  */
748
748
  class FigTooltip extends HTMLElement {
749
749
  static #lastShownAt = 0;
750
- static #lastShownInstance = null;
751
- static #warmupWindow = 500;
750
+ static #lastHiddenAt = 0;
751
+ static #warmupWindow = 1000;
752
752
  static #hoverOpen = null;
753
+ static #documentExitListenersReady = false;
753
754
 
754
755
  #boundHideOnChromeOpen;
755
756
  #boundHidePopupOutsideClick;
@@ -788,6 +789,7 @@ class FigTooltip extends HTMLElement {
788
789
  };
789
790
  }
790
791
  connectedCallback() {
792
+ FigTooltip.#ensureDocumentExitListeners();
791
793
  this.setup();
792
794
  this.#bindTriggerListeners();
793
795
  this.setupEventListeners();
@@ -989,17 +991,29 @@ class FigTooltip extends HTMLElement {
989
991
  return this.hasAttribute("show") && this.getAttribute("show") !== "false";
990
992
  }
991
993
 
994
+ #isWarmSession() {
995
+ const now = Date.now();
996
+ const windowMs = FigTooltip.#warmupWindow;
997
+ if (this.action === "hover" && FigTooltip.#hoverOpen) return true;
998
+ if (FigTooltip.#lastShownAt && now - FigTooltip.#lastShownAt < windowMs)
999
+ return true;
1000
+ if (FigTooltip.#lastHiddenAt && now - FigTooltip.#lastHiddenAt < windowMs)
1001
+ return true;
1002
+ return false;
1003
+ }
1004
+
992
1005
  showDelayedPopup() {
993
1006
  if (this.#showPersisted) return;
994
1007
  clearTimeout(this.timeout);
995
- const warm =
996
- FigTooltip.#lastShownInstance === this &&
997
- Date.now() - FigTooltip.#lastShownAt < FigTooltip.#warmupWindow;
998
- const effectiveDelay = warm ? 0 : this.delay;
1008
+ if (this.#isWarmSession()) {
1009
+ this.render();
1010
+ this.showPopup();
1011
+ return;
1012
+ }
999
1013
  this.timeout = setTimeout(() => {
1000
1014
  this.render();
1001
1015
  this.showPopup();
1002
- }, effectiveDelay);
1016
+ }, this.delay);
1003
1017
  }
1004
1018
 
1005
1019
  showPopup() {
@@ -1021,19 +1035,20 @@ class FigTooltip extends HTMLElement {
1021
1035
  this.isOpen = true;
1022
1036
  if (this.action === "hover") FigTooltip.#hoverOpen = this;
1023
1037
  FigTooltip.#lastShownAt = Date.now();
1024
- FigTooltip.#lastShownInstance = this;
1025
1038
  }
1026
1039
 
1027
1040
  hidePopup() {
1028
1041
  if (this.#showPersisted) return;
1029
1042
  clearTimeout(this.timeout);
1030
1043
  clearTimeout(this.#touchTimeout);
1044
+ const wasShowing = this.isOpen;
1031
1045
  if (this.popup) {
1032
1046
  this.destroy();
1033
1047
  }
1034
1048
 
1035
1049
  this.isOpen = false;
1036
1050
  if (FigTooltip.#hoverOpen === this) FigTooltip.#hoverOpen = null;
1051
+ if (wasShowing) FigTooltip.#lastHiddenAt = Date.now();
1037
1052
  }
1038
1053
 
1039
1054
  hidePopupOutsideClick(event) {
@@ -1184,6 +1199,48 @@ class FigTooltip extends HTMLElement {
1184
1199
  }
1185
1200
  }
1186
1201
 
1202
+ static #ensureDocumentExitListeners() {
1203
+ if (FigTooltip.#documentExitListenersReady) return;
1204
+ FigTooltip.#documentExitListenersReady = true;
1205
+
1206
+ const handlePointerLeftDocument = () => {
1207
+ FigTooltip.#dismissHoverTooltipsOnDocumentExit();
1208
+ };
1209
+
1210
+ document.documentElement.addEventListener(
1211
+ "mouseleave",
1212
+ handlePointerLeftDocument,
1213
+ );
1214
+ document.addEventListener("mouseout", (event) => {
1215
+ if (event.relatedTarget) return;
1216
+ handlePointerLeftDocument();
1217
+ });
1218
+
1219
+ // Same-origin embed: leaving the iframe element (e.g. into a parent dialog).
1220
+ try {
1221
+ const frame = window.frameElement;
1222
+ if (frame) {
1223
+ frame.addEventListener("mouseleave", handlePointerLeftDocument);
1224
+ }
1225
+ } catch {}
1226
+
1227
+ window.addEventListener("message", (event) => {
1228
+ if (event?.data?.type !== "figui:dismiss-tooltips") return;
1229
+ if (window.parent !== window && event.source !== window.parent) return;
1230
+ handlePointerLeftDocument();
1231
+ });
1232
+ }
1233
+
1234
+ static #dismissHoverTooltipsOnDocumentExit() {
1235
+ for (const node of document.querySelectorAll("fig-tooltip")) {
1236
+ if (!(node instanceof FigTooltip)) continue;
1237
+ if (node.action !== "hover") continue;
1238
+ if (node.hasAttribute("show") && node.getAttribute("show") !== "false")
1239
+ continue;
1240
+ if (node.isOpen || node.timeout) node.hidePopup();
1241
+ }
1242
+ }
1243
+
1187
1244
  static #programmatic = new WeakMap();
1188
1245
 
1189
1246
  static show(anchor, text, options = {}) {
@@ -1363,6 +1420,8 @@ class FigDialog extends HTMLDialogElement {
1363
1420
  this._boundContentMutation = this._scheduleAutoResize.bind(this);
1364
1421
  this._boundContentResize = this._scheduleAutoResize.bind(this);
1365
1422
  this._boundRestoreFocus = this._restoreFocus.bind(this);
1423
+ this._boundIframeMouseLeave = this._handleIframeMouseLeave.bind(this);
1424
+ this._iframeDismissMutationObserver = null;
1366
1425
  this._previousFocus = null;
1367
1426
  }
1368
1427
 
@@ -1389,6 +1448,7 @@ class FigDialog extends HTMLDialogElement {
1389
1448
  this._setupDragListeners();
1390
1449
  this._applyPosition();
1391
1450
  this._syncAutoResize();
1451
+ this._setupIframeDismissListeners();
1392
1452
  this._syncA11y();
1393
1453
  });
1394
1454
 
@@ -1403,6 +1463,7 @@ class FigDialog extends HTMLDialogElement {
1403
1463
  });
1404
1464
  window.removeEventListener("message", this._boundIframeMessage);
1405
1465
  this._teardownAutoResize();
1466
+ this._teardownIframeDismissListeners();
1406
1467
  this.removeEventListener("close", this._boundRestoreFocus);
1407
1468
  }
1408
1469
 
@@ -1435,6 +1496,44 @@ class FigDialog extends HTMLDialogElement {
1435
1496
  return super.showModal();
1436
1497
  }
1437
1498
 
1499
+ _handleIframeMouseLeave(event) {
1500
+ const iframe = event?.currentTarget;
1501
+ if (!(iframe instanceof HTMLIFrameElement)) return;
1502
+ try {
1503
+ iframe.contentWindow?.postMessage({ type: "figui:dismiss-tooltips" }, "*");
1504
+ } catch {}
1505
+ }
1506
+
1507
+ _syncIframeDismissListeners() {
1508
+ for (const iframe of this.querySelectorAll(":scope > iframe")) {
1509
+ if (!(iframe instanceof HTMLIFrameElement)) continue;
1510
+ if (iframe.dataset.figuiDismissBound === "true") continue;
1511
+ iframe.dataset.figuiDismissBound = "true";
1512
+ iframe.addEventListener("mouseleave", this._boundIframeMouseLeave);
1513
+ }
1514
+ }
1515
+
1516
+ _setupIframeDismissListeners() {
1517
+ this._syncIframeDismissListeners();
1518
+ if (this._iframeDismissMutationObserver) return;
1519
+ this._iframeDismissMutationObserver = new MutationObserver(() => {
1520
+ this._syncIframeDismissListeners();
1521
+ });
1522
+ this._iframeDismissMutationObserver.observe(this, {
1523
+ childList: true,
1524
+ subtree: false,
1525
+ });
1526
+ }
1527
+
1528
+ _teardownIframeDismissListeners() {
1529
+ this._iframeDismissMutationObserver?.disconnect();
1530
+ this._iframeDismissMutationObserver = null;
1531
+ for (const iframe of this.querySelectorAll(":scope > iframe")) {
1532
+ iframe.removeEventListener("mouseleave", this._boundIframeMouseLeave);
1533
+ delete iframe.dataset.figuiDismissBound;
1534
+ }
1535
+ }
1536
+
1438
1537
  _handleIframeMessage(event) {
1439
1538
  if (!this.autoresize) return;
1440
1539
  const data = event?.data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "6.6.6",
3
+ "version": "6.7.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",