Skip to content

Commit 7e7ab7e

Browse files
authored
Fix MermaidWithFallback diagram display (#2609)
was not properly loading diagrams when switching tabs
1 parent ec07daa commit 7e7ab7e

File tree

1 file changed

+162
-41
lines changed

1 file changed

+162
-41
lines changed

docusaurus/src/components/MermaidWithFallback.js

Lines changed: 162 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// src/components/DocusaurusMermaidFileFallback.js
2-
import React, { useEffect, useState, useRef } from 'react';
1+
import React, { useEffect, useState, useRef, useCallback } from 'react';
32
import { useColorMode } from '@docusaurus/theme-common';
43
import Icon from './Icon'
54

@@ -13,9 +12,11 @@ export default function DocusaurusMermaidFileFallback({
1312
const [chartContent, setChartContent] = useState('');
1413
const [isLoading, setIsLoading] = useState(true);
1514
const [renderFailed, setRenderFailed] = useState(false);
15+
const [shouldRender, setShouldRender] = useState(false);
1616
const { colorMode } = useColorMode();
1717
const imgRef = useRef(null);
1818
const mermaidContainerRef = useRef(null);
19+
const hasRendered = useRef(false);
1920

2021
// Determine which image to display based on the current theme
2122
const imageToShow = colorMode === 'dark' && fallbackImageDark
@@ -37,10 +38,31 @@ export default function DocusaurusMermaidFileFallback({
3738
);
3839
};
3940

41+
// Clean and prepare mermaid content
42+
const cleanMermaidContent = useCallback(() => {
43+
if (mermaidContainerRef.current) {
44+
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
45+
if (mermaidDiv) {
46+
// Remove any existing SVG and reset
47+
mermaidDiv.innerHTML = chartContent;
48+
mermaidDiv.removeAttribute('data-processed');
49+
// Remove any mermaid-generated attributes
50+
const attributes = [...mermaidDiv.attributes];
51+
attributes.forEach(attr => {
52+
if (attr.name.startsWith('data-mermaid')) {
53+
mermaidDiv.removeAttribute(attr.name);
54+
}
55+
});
56+
}
57+
}
58+
}, [chartContent]);
59+
4060
// Load chart content from file
4161
useEffect(() => {
4262
if (chartFile) {
4363
setIsLoading(true);
64+
setRenderFailed(false);
65+
hasRendered.current = false;
4466

4567
fetch(chartFile)
4668
.then(response => {
@@ -53,6 +75,8 @@ export default function DocusaurusMermaidFileFallback({
5375
console.log('Mermaid file loaded successfully:', chartFile);
5476
setChartContent(content.trim());
5577
setIsLoading(false);
78+
// Trigger initial render attempt
79+
setShouldRender(true);
5680
})
5781
.catch(error => {
5882
console.error('Error loading Mermaid chart file:', error);
@@ -61,58 +85,155 @@ export default function DocusaurusMermaidFileFallback({
6185
});
6286
}
6387
}, [chartFile]);
64-
65-
// Force Mermaid to use the current theme when it renders
66-
useEffect(() => {
67-
// When content is loaded or theme changes, we'll need to re-render Mermaid
68-
if (!isLoading && chartContent && mermaidContainerRef.current) {
69-
const renderMermaidWithTheme = async () => {
70-
try {
71-
// Dynamically import mermaid to ensure it's loaded in the browser
72-
const mermaid = (await import('mermaid')).default;
73-
74-
// Initialize mermaid with the current theme
75-
mermaid.initialize({
76-
startOnLoad: false,
77-
theme: colorMode === 'dark' ? 'dark' : 'default',
78-
securityLevel: 'loose',
79-
});
80-
81-
// Clear previous content
82-
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
83-
if (mermaidDiv) {
84-
mermaidDiv.innerHTML = chartContent;
85-
86-
// Run mermaid to render the diagram
87-
await mermaid.run({
88-
nodes: [mermaidDiv],
89-
});
90-
}
91-
} catch (error) {
92-
console.error('Mermaid rendering failed:', error);
93-
setRenderFailed(true);
94-
}
95-
};
88+
89+
// Check if component is in active tab
90+
const checkIfVisible = useCallback(() => {
91+
if (!mermaidContainerRef.current) return false;
92+
93+
// Check if the container is actually visible (not in hidden tab)
94+
const rect = mermaidContainerRef.current.getBoundingClientRect();
95+
const isInViewport = rect.width > 0 && rect.height > 0;
96+
97+
// Also check if parent tab is active
98+
const tabPanel = mermaidContainerRef.current.closest('[role="tabpanel"]');
99+
const isTabActive = tabPanel ? !tabPanel.hasAttribute('hidden') : true;
100+
101+
return isInViewport && isTabActive;
102+
}, []);
103+
104+
// Render Mermaid diagram
105+
const renderMermaidDiagram = useCallback(async () => {
106+
if (!chartContent || !mermaidContainerRef.current) return;
107+
108+
// Don't render if not visible or already rendered
109+
if (!checkIfVisible()) {
110+
return;
111+
}
112+
113+
try {
114+
console.log('Attempting to render Mermaid diagram...');
96115

97-
renderMermaidWithTheme();
116+
// Clean previous content
117+
cleanMermaidContent();
118+
119+
// Dynamically import mermaid
120+
const mermaid = (await import('mermaid')).default;
98121

99-
// Also set a timeout to check if rendering was successful
122+
// Initialize mermaid with current theme
123+
mermaid.initialize({
124+
startOnLoad: false,
125+
theme: colorMode === 'dark' ? 'dark' : 'default',
126+
securityLevel: 'loose',
127+
});
128+
129+
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
130+
if (mermaidDiv && chartContent) {
131+
// Set content
132+
mermaidDiv.innerHTML = chartContent;
133+
134+
// Generate unique ID
135+
const uniqueId = `mermaid-${chartFile.replace(/[^a-zA-Z0-9]/g, '')}-${Date.now()}`;
136+
mermaidDiv.id = uniqueId;
137+
138+
// Render the diagram
139+
await mermaid.run({
140+
nodes: [mermaidDiv],
141+
});
142+
143+
hasRendered.current = true;
144+
console.log('Mermaid diagram rendered successfully');
145+
}
146+
} catch (error) {
147+
console.error('Mermaid rendering failed:', error);
148+
setRenderFailed(true);
149+
}
150+
}, [chartContent, colorMode, cleanMermaidContent, checkIfVisible, chartFile]);
151+
152+
// Listen for tab changes and visibility
153+
useEffect(() => {
154+
if (!shouldRender || isLoading || renderFailed) return;
155+
156+
// Initial render attempt
157+
const initialRender = () => {
158+
if (checkIfVisible()) {
159+
renderMermaidDiagram();
160+
}
161+
};
162+
163+
// Small delay for initial render
164+
const initialTimer = setTimeout(initialRender, 100);
165+
166+
// Listen for tab changes
167+
const handleTabChange = () => {
168+
setTimeout(() => {
169+
if (checkIfVisible() && !hasRendered.current) {
170+
hasRendered.current = false; // Reset for re-render
171+
renderMermaidDiagram();
172+
}
173+
}, 50);
174+
};
175+
176+
// Listen for clicks on tabs (Docusaurus tab behavior)
177+
const tabButtons = document.querySelectorAll('[role="tab"]');
178+
tabButtons.forEach(button => {
179+
button.addEventListener('click', handleTabChange);
180+
});
181+
182+
// Also listen for visibility changes
183+
const observer = new MutationObserver((mutations) => {
184+
mutations.forEach((mutation) => {
185+
if (mutation.type === 'attributes' && mutation.attributeName === 'hidden') {
186+
handleTabChange();
187+
}
188+
});
189+
});
190+
191+
const tabPanel = mermaidContainerRef.current?.closest('[role="tabpanel"]');
192+
if (tabPanel) {
193+
observer.observe(tabPanel, { attributes: true });
194+
}
195+
196+
return () => {
197+
clearTimeout(initialTimer);
198+
tabButtons.forEach(button => {
199+
button.removeEventListener('click', handleTabChange);
200+
});
201+
observer.disconnect();
202+
};
203+
}, [shouldRender, isLoading, renderFailed, renderMermaidDiagram, checkIfVisible]);
204+
205+
// Re-render when theme changes
206+
useEffect(() => {
207+
if (!isLoading && chartContent && shouldRender && !renderFailed) {
208+
hasRendered.current = false;
209+
setTimeout(() => {
210+
if (checkIfVisible()) {
211+
renderMermaidDiagram();
212+
}
213+
}, 100);
214+
}
215+
}, [colorMode, isLoading, chartContent, shouldRender, renderFailed, checkIfVisible, renderMermaidDiagram]);
216+
217+
// Check for rendering success/failure
218+
useEffect(() => {
219+
if (!isLoading && chartContent && shouldRender && !renderFailed) {
100220
const timer = setTimeout(() => {
101-
if (mermaidContainerRef.current) {
221+
if (mermaidContainerRef.current && checkIfVisible()) {
102222
const mermaidDiv = mermaidContainerRef.current.querySelector('.mermaid');
103-
if (
223+
if (mermaidDiv && (
104224
!mermaidDiv.querySelector('svg') ||
105225
mermaidDiv.textContent.includes('Syntax error') ||
106226
mermaidDiv.querySelector('.error-icon')
107-
) {
227+
)) {
228+
console.warn('Mermaid rendering appears to have failed, switching to fallback');
108229
setRenderFailed(true);
109230
}
110231
}
111-
}, 2000);
232+
}, 3000);
112233

113234
return () => clearTimeout(timer);
114235
}
115-
}, [chartContent, isLoading, colorMode]);
236+
}, [isLoading, chartContent, shouldRender, renderFailed, checkIfVisible]);
116237

117238
// Initialize zoom on the fallback image
118239
useEffect(() => {

0 commit comments

Comments
 (0)