From b3ff3f6f8fa1ab8c3d524451456017a4ade2a159 Mon Sep 17 00:00:00 2001 From: yasnazariel <82168644+yasnazariel@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:45:04 +0330 Subject: [PATCH] fix(theme): prevent duplicate iframe listeners and improve dark mode sync his update improves the dark mode synchronization logic for Chromatic iframes. Changes include: - Preventing duplicate event listeners using a dataset flag - Adding safety checks before calling postMessage on iframe contentWindow - Removing unnecessary polling via setInterval in favor of MutationObserver - Optimizing mutation handling loop for better performance - Using setAttribute for better DOM compatibilit --- docs/iframe-theme.js | 114 +++++++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/docs/iframe-theme.js b/docs/iframe-theme.js index 35287274c..fb2393c9d 100644 --- a/docs/iframe-theme.js +++ b/docs/iframe-theme.js @@ -1,67 +1,85 @@ (function () { - function updateIframesForDarkMode() { - const isDark = document.documentElement.classList.contains("dark"); + const IFRAME_SELECTOR = 'iframe[src*="chromatic.com/iframe.html"]'; + const MESSAGE_TYPE = "THEME_CHANGE"; - document.querySelectorAll('iframe[src*="chromatic.com/iframe.html"]').forEach((iframe) => { - iframe.style.background = "transparent"; - iframe.allowTransparency = true; + function getIsDark() { + return document.documentElement.classList.contains("dark"); + } + + function sendThemeMessage(iframe, isDark) { + try { + if (!iframe.contentWindow) return; + + iframe.contentWindow.postMessage( + { + type: MESSAGE_TYPE, + isDark, + styles: { + background: isDark ? "#0b0d0f" : "transparent", + }, + }, + "*" // consider restricting origin if possible + ); + } catch (e) { + console.warn("Could not send message to iframe:", e); + } + } + + function setupIframe(iframe) { + const isDark = getIsDark(); + + iframe.style.background = "transparent"; + iframe.setAttribute("allowTransparency", "true"); + + // prevent duplicate listeners + if (iframe.dataset.themeListenerAttached === "true") return; + iframe.dataset.themeListenerAttached = "true"; - // Use postMessage to communicate with the iframe since we can't access contentDocument due to CORS - const sendThemeMessage = () => { - try { - const message = { - type: 'THEME_CHANGE', - isDark: isDark, - styles: { - background: isDark ? '#0b0d0f' : 'transparent' // transparent for light mode - } - }; - iframe.contentWindow.postMessage(message, '*'); - } catch (e) { - console.warn("Could not send message to iframe:", e); - } - }; + const onLoad = () => { + setTimeout(() => sendThemeMessage(iframe, getIsDark()), 100); + }; - // Send message immediately if iframe might be loaded - sendThemeMessage(); + iframe.addEventListener("load", onLoad); - // Also send message when iframe loads - iframe.addEventListener("load", () => { - // Add a small delay to ensure iframe is ready - setTimeout(sendThemeMessage, 100); - }); - }); + // initial attempt + sendThemeMessage(iframe, isDark); } - // Listen for theme changes on the parent document + function updateIframes() { + const iframes = document.querySelectorAll(IFRAME_SELECTOR); + iframes.forEach(setupIframe); + } + + // Observe class changes on const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - mutation.attributeName === 'class' && - mutation.target === document.documentElement) { - updateIframesForDarkMode(); + for (const mutation of mutations) { + if ( + mutation.type === "attributes" && + mutation.attributeName === "class" + ) { + updateIframes(); + break; } - }); + } }); observer.observe(document.documentElement, { attributes: true, - attributeFilter: ['class'] + attributeFilter: ["class"], }); + function init() { + updateIframes(); + } + if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", updateIframesForDarkMode); + document.addEventListener("DOMContentLoaded", init, { once: true }); } else { - setTimeout(updateIframesForDarkMode, 100); - // TODO: add Storybook with Darkmode enabled - let themeChangeCount = 0; - const themeChangeInterval = setInterval(() => { - if (themeChangeCount < 2) { - updateIframesForDarkMode(); - themeChangeCount++; - } else { - clearInterval(themeChangeInterval); - } - }, 1000); + init(); } + + // optional: cleanup on page unload (good practice) + window.addEventListener("beforeunload", () => { + observer.disconnect(); + }); })();