mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-04-09 09:03:40 +00:00
feat: enhance mermaid lightbox interaction with improved accessibility and hover hints
This commit is contained in:
parent
dba96e38e0
commit
b7cf4442bd
2 changed files with 129 additions and 48 deletions
|
|
@ -229,43 +229,81 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
|
|
||||||
/* ── Mermaid diagram zoom / lightbox ──────────────────────────── */
|
/* ── Mermaid diagram zoom / lightbox ──────────────────────────── */
|
||||||
|
|
||||||
/* Wrapper that holds the diagram + the expand button */
|
/* Wrapper — entire diagram is the click target */
|
||||||
.mermaid-zoom-wrapper {
|
.mermaid-zoom-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
cursor: zoom-in;
|
||||||
|
border-radius: var(--ifm-card-border-radius, 0.5rem);
|
||||||
|
/* Clip the hover overlay to the same rounded corners */
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Expand button — hidden until the diagram is hovered */
|
/* Semi-transparent tint that fades in on hover */
|
||||||
.mermaid-zoom-btn {
|
.mermaid-zoom-hint {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
inset: 0;
|
||||||
right: 0.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 1.75rem;
|
pointer-events: none;
|
||||||
height: 1.75rem;
|
|
||||||
padding: 0;
|
|
||||||
background: var(--ifm-background-surface-color);
|
|
||||||
border: 1px solid var(--ifm-color-emphasis-300);
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
color: var(--ifm-color-emphasis-600);
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease,
|
background: rgba(0, 0, 0, 0);
|
||||||
border-color 0.15s ease;
|
transition: opacity 0.2s ease, background 0.2s ease;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-zoom-wrapper:hover .mermaid-zoom-btn,
|
.mermaid-zoom-wrapper:hover .mermaid-zoom-hint,
|
||||||
.mermaid-zoom-btn:focus-visible {
|
.mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mermaid-zoom-btn:hover {
|
[data-theme='dark'] .mermaid-zoom-wrapper:hover .mermaid-zoom-hint,
|
||||||
background: var(--ifm-color-primary);
|
[data-theme='dark'] .mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint {
|
||||||
border-color: var(--ifm-color-primary);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
color: #fff;
|
}
|
||||||
|
|
||||||
|
/* Centered pill badge inside the tint */
|
||||||
|
.mermaid-zoom-hint-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.35rem 0.8rem;
|
||||||
|
background: var(--ifm-background-surface-color);
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
border-radius: 2rem;
|
||||||
|
color: var(--ifm-color-emphasis-800);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
|
||||||
|
transform: translateY(4px);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-zoom-wrapper:hover .mermaid-zoom-hint-badge,
|
||||||
|
.mermaid-zoom-wrapper:focus-visible .mermaid-zoom-hint-badge {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus ring for keyboard users */
|
||||||
|
.mermaid-zoom-wrapper:focus-visible {
|
||||||
|
outline: 2px solid var(--ifm-color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch devices: no hover state — show badge permanently so users know it's tappable */
|
||||||
|
@media (hover: none) {
|
||||||
|
.mermaid-zoom-hint {
|
||||||
|
opacity: 1;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-zoom-hint-badge {
|
||||||
|
transform: translateY(0);
|
||||||
|
/* Slightly more opaque so it reads well without hover context */
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full-screen backdrop */
|
/* Full-screen backdrop */
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ type Props = WrapperProps<typeof MermaidType>;
|
||||||
|
|
||||||
export default function MermaidWrapper(props: Props): React.JSX.Element {
|
export default function MermaidWrapper(props: Props): React.JSX.Element {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lightboxRef = useRef<HTMLDivElement>(null);
|
||||||
const [overlayOpen, setOverlayOpen] = useState(false);
|
const [overlayOpen, setOverlayOpen] = useState(false);
|
||||||
const [svgMarkup, setSvgMarkup] = useState('');
|
const [svgMarkup, setSvgMarkup] = useState('');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
@ -33,14 +34,43 @@ export default function MermaidWrapper(props: Props): React.JSX.Element {
|
||||||
setOverlayOpen(false);
|
setOverlayOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Keyboard + body-scroll lock while overlay is open
|
// Keyboard + body-scroll lock + focus trap while overlay is open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!overlayOpen) return;
|
if (!overlayOpen) return;
|
||||||
|
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') handleClose();
|
if (e.key === 'Escape') {
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Focus trap: keep Tab/Shift+Tab inside the lightbox
|
||||||
|
if (e.key === 'Tab' && lightboxRef.current) {
|
||||||
|
const focusable = lightboxRef.current.querySelectorAll<HTMLElement>(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
last?.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
first?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', onKey);
|
document.addEventListener('keydown', onKey);
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// Move focus into the lightbox on open
|
||||||
|
const closeBtn = lightboxRef.current?.querySelector<HTMLElement>('button');
|
||||||
|
closeBtn?.focus();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onKey);
|
document.removeEventListener('keydown', onKey);
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
|
|
@ -48,19 +78,28 @@ export default function MermaidWrapper(props: Props): React.JSX.Element {
|
||||||
}, [overlayOpen, handleClose]);
|
}, [overlayOpen, handleClose]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="mermaid-zoom-wrapper">
|
<div
|
||||||
<OriginalMermaid {...props} />
|
ref={containerRef}
|
||||||
<button
|
className="mermaid-zoom-wrapper"
|
||||||
className="mermaid-zoom-btn"
|
|
||||||
onClick={handleOpen}
|
onClick={handleOpen}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleOpen();
|
||||||
|
}
|
||||||
|
}}
|
||||||
aria-label="View diagram enlarged"
|
aria-label="View diagram enlarged"
|
||||||
title="View enlarged"
|
|
||||||
>
|
>
|
||||||
{/* Expand / fullscreen icon */}
|
<OriginalMermaid {...props} />
|
||||||
|
{/* Hover hint — pointer-events:none so it never swallows clicks */}
|
||||||
|
<div className="mermaid-zoom-hint" aria-hidden="true">
|
||||||
|
<div className="mermaid-zoom-hint-badge">
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
width="15"
|
width="14"
|
||||||
height="15"
|
height="14"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2.5"
|
strokeWidth="2.5"
|
||||||
|
|
@ -68,17 +107,20 @@ export default function MermaidWrapper(props: Props): React.JSX.Element {
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<polyline points="15 3 21 3 21 9" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
<polyline points="9 21 3 21 3 15" />
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
<line x1="21" y1="3" x2="14" y2="10" />
|
<line x1="11" y1="8" x2="11" y2="14" />
|
||||||
<line x1="3" y1="21" x2="10" y2="14" />
|
<line x1="8" y1="11" x2="14" y2="11" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
Enlarge
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{mounted &&
|
{mounted &&
|
||||||
overlayOpen &&
|
overlayOpen &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
|
ref={lightboxRef}
|
||||||
className="mermaid-lightbox"
|
className="mermaid-lightbox"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
|
@ -95,6 +137,7 @@ export default function MermaidWrapper(props: Props): React.JSX.Element {
|
||||||
className="mermaid-lightbox-close"
|
className="mermaid-lightbox-close"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
aria-label="Close enlarged view"
|
aria-label="Close enlarged view"
|
||||||
|
title="Close (Esc)"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue