Skip to content

Commit 2a323a1

Browse files
yuluo-yxrootfs
authored andcommitted
docs: add mermaid modal (#288)
* docs: add mermaid modal Signed-off-by: yuluo-yx <[email protected]> * fix Signed-off-by: yuluo-yx <[email protected]> * fix Signed-off-by: yuluo-yx <[email protected]> * fix: fix lit Signed-off-by: yuluo-yx <[email protected]> * fix Signed-off-by: yuluo-yx <[email protected]> * fix Signed-off-by: yuluo-yx <[email protected]> * Fix the issue where the top scroll bar is not visible when the chart is enlarged. Signed-off-by: yuluo-yx <[email protected]> * fix lint Signed-off-by: yuluo-yx <[email protected]> --------- Signed-off-by: yuluo-yx <[email protected]> Co-authored-by: Huamin Chen <[email protected]> Signed-off-by: liuhy <[email protected]>
1 parent 1b7b097 commit 2a323a1

File tree

1 file changed

+235
-0
lines changed
  • website/src/components/ZoomableMermaid

1 file changed

+235
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import React, { useState, useRef, useEffect, useCallback } from 'react'
2+
import { createPortal } from 'react-dom'
3+
import Mermaid from '@theme/Mermaid'
4+
import styles from './styles.module.css'
5+
6+
const ZoomableMermaid = ({ children, title, defaultZoom = 1.2 }) => {
7+
const [isModalOpen, setIsModalOpen] = useState(false)
8+
const [isHovered, setIsHovered] = useState(false)
9+
const [zoomLevel, setZoomLevel] = useState(defaultZoom) // Use defaultZoom prop
10+
const modalRef = useRef(null)
11+
const containerRef = useRef(null)
12+
13+
const openModal = useCallback(() => {
14+
setIsModalOpen(true)
15+
setZoomLevel(defaultZoom) // Reset to default zoom when opening
16+
document.body.style.overflow = 'hidden'
17+
}, [defaultZoom])
18+
19+
const closeModal = useCallback(() => {
20+
setIsModalOpen(false)
21+
document.body.style.overflow = 'unset'
22+
// Return focus to the original container
23+
if (containerRef.current) {
24+
containerRef.current.focus()
25+
}
26+
}, [])
27+
28+
const zoomIn = useCallback(() => {
29+
setZoomLevel(prev => Math.min(prev + 0.2, 5.0)) // Max 500%
30+
}, [])
31+
32+
const zoomOut = useCallback(() => {
33+
setZoomLevel(prev => Math.max(prev - 0.2, 0.5)) // Min 50%
34+
}, [])
35+
36+
const resetZoom = useCallback(() => {
37+
setZoomLevel(defaultZoom) // Reset to custom default instead of hardcoded 1.2
38+
}, [defaultZoom])
39+
40+
useEffect(() => {
41+
const handleEscape = (e) => {
42+
if (e.key === 'Escape' && isModalOpen) {
43+
closeModal()
44+
}
45+
}
46+
47+
const handleClickOutside = (e) => {
48+
if (modalRef.current && !modalRef.current.contains(e.target)) {
49+
closeModal()
50+
}
51+
}
52+
53+
const handleKeydown = (e) => {
54+
if (!isModalOpen) return
55+
56+
if (e.key === '=' || e.key === '+') {
57+
e.preventDefault()
58+
zoomIn()
59+
}
60+
else if (e.key === '-') {
61+
e.preventDefault()
62+
zoomOut()
63+
}
64+
else if (e.key === '0') {
65+
e.preventDefault()
66+
resetZoom()
67+
}
68+
}
69+
70+
if (isModalOpen) {
71+
document.addEventListener('keydown', handleEscape)
72+
document.addEventListener('mousedown', handleClickOutside)
73+
document.addEventListener('keydown', handleKeydown)
74+
75+
// Focus the modal content when opened
76+
setTimeout(() => {
77+
if (modalRef.current) {
78+
modalRef.current.focus()
79+
}
80+
}, 100)
81+
}
82+
83+
return () => {
84+
document.removeEventListener('keydown', handleEscape)
85+
document.removeEventListener('mousedown', handleClickOutside)
86+
document.removeEventListener('keydown', handleKeydown)
87+
}
88+
}, [isModalOpen, closeModal, zoomIn, zoomOut, resetZoom])
89+
90+
// Cleanup on unmount
91+
useEffect(() => {
92+
return () => {
93+
document.body.style.overflow = 'unset'
94+
}
95+
}, [])
96+
97+
const handleKeyDown = (e) => {
98+
if (e.key === 'Enter' || e.key === ' ') {
99+
e.preventDefault()
100+
openModal()
101+
}
102+
}
103+
104+
const modalContent = (
105+
<div
106+
className={styles.modal}
107+
role="dialog"
108+
aria-modal="true"
109+
aria-labelledby={title ? 'modal-title' : undefined}
110+
aria-describedby="modal-description"
111+
>
112+
<div
113+
className={styles.modalContent}
114+
ref={modalRef}
115+
tabIndex={-1}
116+
>
117+
<div className={styles.modalHeader}>
118+
{title && (
119+
<h3 id="modal-title" className={styles.modalTitle}>
120+
{title}
121+
</h3>
122+
)}
123+
<div className={styles.modalControls}>
124+
<span className={styles.zoomIndicator}>
125+
{Math.round(zoomLevel * 100)}
126+
%
127+
</span>
128+
<button
129+
className={styles.zoomButton}
130+
onClick={zoomOut}
131+
disabled={zoomLevel <= 0.5}
132+
aria-label="Reduce the size of the chart"
133+
type="button"
134+
title="Reduce (Shortcut key: -)"
135+
>
136+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
137+
<circle cx="11" cy="11" r="8" />
138+
<path d="M8 11h6" />
139+
<path d="m21 21-4.35-4.35" />
140+
</svg>
141+
</button>
142+
<button
143+
className={styles.resetButton}
144+
onClick={resetZoom}
145+
aria-label={`Reset to default zoom level ${Math.round(defaultZoom * 100)}%`}
146+
type="button"
147+
title={`Reset to default zoom level ${Math.round(defaultZoom * 100)}% (Shortcut key: 0)`}
148+
>
149+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
150+
<path d="M3 3l18 18" />
151+
<path d="m19 4-7 7-7-7" />
152+
<path d="m5 20 7-7 7 7" />
153+
</svg>
154+
</button>
155+
<button
156+
className={styles.zoomButton}
157+
onClick={zoomIn}
158+
disabled={zoomLevel >= 5.0}
159+
aria-label="Enlarge the chart"
160+
type="button"
161+
title="Enlarge (Shortcut key: +)"
162+
>
163+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
164+
<circle cx="11" cy="11" r="8" />
165+
<path d="M8 11h6" />
166+
<path d="M11 8v6" />
167+
<path d="m21 21-4.35-4.35" />
168+
</svg>
169+
</button>
170+
<button
171+
className={styles.closeButton}
172+
onClick={closeModal}
173+
aria-label="Close the zoomed view"
174+
type="button"
175+
>
176+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
177+
<line x1="18" y1="6" x2="6" y2="18" />
178+
<line x1="6" y1="6" x2="18" y2="18" />
179+
</svg>
180+
</button>
181+
</div>
182+
</div>
183+
<div
184+
className={styles.modalBody}
185+
id="modal-description"
186+
aria-label="Enlarged Mermaid diagram"
187+
>
188+
<div
189+
className={styles.diagramContainer}
190+
style={{
191+
transform: `scale(${zoomLevel})`,
192+
// Ensure scaling is from the center of the diagram.
193+
// Fix the issue where the top scroll bar is not visible when the chart is enlarged.
194+
transformOrigin: 'center top',
195+
}}
196+
>
197+
<Mermaid value={children} />
198+
</div>
199+
</div>
200+
</div>
201+
</div>
202+
)
203+
204+
return (
205+
<>
206+
<div
207+
ref={containerRef}
208+
className={`${styles.mermaidContainer} ${isHovered ? styles.hovered : ''}`}
209+
onClick={openModal}
210+
onMouseEnter={() => setIsHovered(true)}
211+
onMouseLeave={() => setIsHovered(false)}
212+
role="button"
213+
tabIndex={0}
214+
onKeyDown={handleKeyDown}
215+
aria-label={`Click to enlarge ${title || 'Mermaid diagram'}`}
216+
aria-expanded={isModalOpen}
217+
>
218+
<div className={styles.zoomHint} aria-hidden="true">
219+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
220+
<circle cx="11" cy="11" r="8" />
221+
<path d="m21 21-4.35-4.35" />
222+
<path d="M11 8v6" />
223+
<path d="M8 11h6" />
224+
</svg>
225+
<span>Click to enlarge</span>
226+
</div>
227+
<Mermaid value={children} />
228+
</div>
229+
230+
{isModalOpen && createPortal(modalContent, document.body)}
231+
</>
232+
)
233+
}
234+
235+
export default ZoomableMermaid

0 commit comments

Comments
 (0)