Skip to content

feat: better alerts around theme awareness #279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 70 additions & 24 deletions iframe-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@
display: flex;
align-items: center;
}
.theme-status {
margin-left: 15px;
padding: 5px 10px;
border-radius: 4px;
font-size: 14px;
background-color: #f0f0f0;
}
.status-waiting {
color: #856404;
background-color: #fff3cd;
}
.status-ready {
color: #155724;
background-color: #d4edda;
}
</style>
</head>
<body>
Expand Down Expand Up @@ -103,6 +118,7 @@ <h1>Iframe Test Page</h1>
<option value="dark">Dark</option>
</select>
<button id="apply-theme">Apply Theme</button>
<span id="theme-status" class="theme-status status-waiting">Waiting for theme sync...</span>
</div>
</div>

Expand All @@ -123,13 +139,41 @@ <h1>Iframe Test Page</h1>
const iframeContainer = document.querySelector('.iframe-container');
const themeMode = document.getElementById('theme-mode');
const applyThemeButton = document.getElementById('apply-theme');
const themeStatus = document.getElementById('theme-status');

// Theme sync status
let isThemeSyncReady = false;

// Update theme status display
function updateThemeStatus(ready) {
isThemeSyncReady = ready;
if (ready) {
themeStatus.textContent = 'Theme sync ready';
themeStatus.classList.remove('status-waiting');
themeStatus.classList.add('status-ready');
applyThemeButton.disabled = false;
} else {
themeStatus.textContent = 'Waiting for theme sync...';
themeStatus.classList.add('status-waiting');
themeStatus.classList.remove('status-ready');
applyThemeButton.disabled = true;
}
}

// Initialize theme status
updateThemeStatus(false);

// Function to send theme to iframe
function sendThemeToIframe(theme) {
iframe.contentWindow.postMessage(
{ type: 'theme-update', theme: theme },
'*'
);
if (isThemeSyncReady) {
iframe.contentWindow.postMessage(
{ type: 'theme-update', theme: theme },
'*'
);
console.log('Sent theme to iframe:', theme);
} else {
console.log('Theme sync not ready yet, cannot send theme');
}
}

// Apply theme when button is clicked
Expand All @@ -140,28 +184,21 @@ <h1>Iframe Test Page</h1>

// Auto-apply theme when iframe loads
iframe.addEventListener('load', function() {
// Wait a moment for Docusaurus to initialize
setTimeout(() => {
sendThemeToIframe(themeMode.value);
}, 500);
});

// Load iframe with selected URL
loadButton.addEventListener('click', function() {
let url = customUrl.value.trim();

if (!url && presetUrls.value) {
url = presetUrls.value;
}
console.log('Iframe loaded, waiting for theme-ready message...');
// Reset the theme sync status when iframe loads
updateThemeStatus(false);

if (url) {
// If URL doesn't start with http or /, add / at the beginning
if (!url.startsWith('http') && !url.startsWith('/')) {
url = '/' + url;
// Set a timeout for theme sync readiness
const syncTimeout = setTimeout(() => {
if (!isThemeSyncReady) {
console.warn('Theme sync ready message not received within timeout period');
// Enable theme controls anyway after timeout
updateThemeStatus(true);
}

iframe.src = url;
}
}, 5000);

// Store the timeout in a property so we can clear it if needed
iframe.syncTimeoutId = syncTimeout;
});

// Listen for messages from the iframe
Expand All @@ -171,6 +208,15 @@ <h1>Iframe Test Page</h1>
// Handle theme-ready message
if (event.data && event.data.type === 'theme-ready') {
console.log('Docusaurus is ready for theme sync');

// Clear any existing timeout
if (iframe.syncTimeoutId) {
clearTimeout(iframe.syncTimeoutId);
}

// Update theme status
updateThemeStatus(true);

// Send current theme
sendThemeToIframe(themeMode.value);
}
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useIframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useState, useEffect } from 'react';

/**
* Determines if the current window is running inside an iframe
*/
function isInIframe(): boolean {
return typeof window !== 'undefined' && window.self !== window.top;
}

/**
* React hook that detects if the current page is displayed inside an iframe
* @returns boolean indicating if the current page is in an iframe
*/
export function useIframe(): boolean {
const [isInIframeState, setIsInIframeState] = useState(false);

useEffect(() => {
setIsInIframeState(isInIframe());
}, []);

return isInIframeState;
}
10 changes: 3 additions & 7 deletions src/theme/Layout/IframeNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import React, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useLocation } from "@docusaurus/router";
import { isInIframe } from "./utils";
import { useIframe } from "../../hooks/useIframe";

/**
* Component that handles navigation events between iframe and parent window
*/
export function IframeNavigation(): JSX.Element | null {
const location = useLocation();
const [isInIframeState, setIsInIframeState] = useState(false);

useEffect(() => {
setIsInIframeState(isInIframe());
}, []);
const isInIframeState = useIframe();

// Handle navigation events
useEffect(() => {
Expand Down
28 changes: 19 additions & 9 deletions src/theme/Layout/ThemeSync.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
import React, { useEffect, useState } from "react";
import { useColorMode } from "@docusaurus/theme-common";
import { isInIframe } from "./utils";
import { useIframe } from "../../hooks/useIframe";

/**
* Component that handles theme synchronization between iframe and parent window
*/
export function ThemeSync(): JSX.Element | null {
const [isInIframeState, setIsInIframeState] = useState(false);
const isInIframeState = useIframe();
const [lastSentTheme, setLastSentTheme] = useState<string | null>(null);
const { colorMode, setColorMode } = useColorMode();

// Ensure theme system is ready and notify parent
useEffect(() => {
setIsInIframeState(isInIframe());
}, []);
if (isInIframeState) {
// Wait a short time to ensure Docusaurus theme system is fully initialized
const readyTimer = setTimeout(() => {
// Send ready message to parent
window.parent.postMessage({ type: 'theme-ready' }, '*');
}, 20);

return () => clearTimeout(readyTimer);
}
}, [isInIframeState]);

// Handle theme message events from parent
useEffect(() => {
Expand All @@ -34,9 +44,6 @@ export function ThemeSync(): JSX.Element | null {
// Add event listener
window.addEventListener('message', handleMessage);

// Send ready message to parent
window.parent.postMessage({ type: 'theme-ready' }, '*');

// Clean up
return () => {
window.removeEventListener('message', handleMessage);
Expand All @@ -46,14 +53,17 @@ export function ThemeSync(): JSX.Element | null {

// Notify parent when the theme changes
useEffect(() => {
if (isInIframeState) {
if (isInIframeState && colorMode !== lastSentTheme) {
// Send theme change notification to parent
window.parent.postMessage({
type: 'theme-changed',
theme: colorMode
}, '*');

// Update the last sent theme
setLastSentTheme(colorMode);
}
}, [colorMode, isInIframeState]);
}, [colorMode, isInIframeState, lastSentTheme]);

// This component doesn't render anything
return null;
Expand Down
11 changes: 3 additions & 8 deletions src/theme/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import React, { useEffect, useState } from "react";
import React from "react";
import Layout from "@theme-original/Layout";
import { ThemeSync } from "./ThemeSync";
import { IframeNavigation } from "./IframeNavigation";
import { isInIframe } from "./utils";
import { useIframe } from "../../hooks/useIframe";

export default function LayoutWrapper(props) {
const [isInIframeState, setIsInIframeState] = useState(false);

useEffect(() => {
setIsInIframeState(isInIframe());
}, []);

const isInIframeState = useIframe();
const dataAttributes = isInIframeState ? { 'data-iframe': 'true' } : {};

return (
Expand Down
6 changes: 0 additions & 6 deletions src/theme/Layout/utils.ts

This file was deleted.