refactor(ui): MetricTooltip delegates to InfoPopover (circled ? glyph)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,114 +0,0 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
/* A real button: 24px tap target (WCAG 2.5.8) drawn as the small glyph. */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
margin: -6px 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-muted, #8a7a72);
|
||||
cursor: help;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.wrapper:hover .icon,
|
||||
.icon[aria-expanded="true"] {
|
||||
color: var(--accent-coral-dark, #b04a2e);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
width: 220px;
|
||||
background: var(--bg-primary, #faf7f2);
|
||||
border: 1px solid var(--border-color, #e8ddd4);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(44, 36, 32, 0.15);
|
||||
padding: 0.6rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||
}
|
||||
|
||||
/* Reveal on hover (desktop), keyboard focus, or explicit tap/click toggle. */
|
||||
.wrapper:hover .tooltip,
|
||||
.wrapper:focus-within .tooltip,
|
||||
.tooltipOpen {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltipOpen {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Small arrow pointing down */
|
||||
.tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--border-color, #e8ddd4);
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary, #2c2420);
|
||||
}
|
||||
|
||||
.tooltipPlain {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #5a4a44);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tooltipDetail {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted, #8a7a72);
|
||||
line-height: 1.4;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.tooltip {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Anchor the bubble to open rightward on phones — icons follow their labels,
|
||||
which start at the left edge, so centring pushed the bubble off-screen. */
|
||||
@media (max-width: 640px) {
|
||||
.tooltip {
|
||||
left: -12px;
|
||||
right: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
left: 16px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { METRIC_EXPLANATIONS } from '@/lib/metrics';
|
||||
import styles from './MetricTooltip.module.css';
|
||||
import { InfoPopover } from './InfoPopover';
|
||||
|
||||
interface MetricTooltipProps {
|
||||
metricKey?: string;
|
||||
@@ -13,51 +12,13 @@ interface MetricTooltipProps {
|
||||
|
||||
export function MetricTooltip({ metricKey, label, plain, detail }: MetricTooltipProps) {
|
||||
const explanation = metricKey ? METRIC_EXPLANATIONS[metricKey] : undefined;
|
||||
const tooltipLabel = label ?? explanation?.label;
|
||||
const tooltipPlain = plain ?? explanation?.plain;
|
||||
const tooltipDetail = detail ?? explanation?.detail;
|
||||
|
||||
// Tap/click/keyboard toggle so the definition is reachable on touch devices
|
||||
// and by keyboard, not just mouse hover (hover still works on desktop).
|
||||
const [open, setOpen] = useState(false);
|
||||
const wrapperRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const dismiss = (e: Event) => {
|
||||
if (wrapperRef.current && e.target instanceof Node && !wrapperRef.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('click', dismiss);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => {
|
||||
document.removeEventListener('click', dismiss);
|
||||
document.removeEventListener('keydown', onKey);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
if (!tooltipPlain) return null;
|
||||
|
||||
const resolvedLabel = label ?? explanation?.label;
|
||||
return (
|
||||
<span className={styles.wrapper} ref={wrapperRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.icon}
|
||||
aria-expanded={open}
|
||||
aria-label={`What does ${tooltipLabel ?? 'this metric'} mean?`}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
ⓘ
|
||||
</button>
|
||||
<span className={`${styles.tooltip}${open ? ` ${styles.tooltipOpen}` : ''}`} role="tooltip">
|
||||
{tooltipLabel && <span className={styles.tooltipLabel}>{tooltipLabel}</span>}
|
||||
<span className={styles.tooltipPlain}>{tooltipPlain}</span>
|
||||
{tooltipDetail && <span className={styles.tooltipDetail}>{tooltipDetail}</span>}
|
||||
</span>
|
||||
</span>
|
||||
<InfoPopover
|
||||
label={resolvedLabel}
|
||||
plain={plain ?? explanation?.plain}
|
||||
detail={detail ?? explanation?.detail}
|
||||
ariaLabel={resolvedLabel ? `What does ${resolvedLabel} mean?` : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user