Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 48s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Rankings mobile: the metric value — the point of the page — sat behind a sideways swipe at 390px. The Area column now folds into a subline under the school name, leaving Rank | School | Value to fit the viewport with no horizontal scroll. Rankings default metric becomes 'expected standard' (rwm_expected_pct); 'higher standard' stays available but no longer frames every school's headline number in the terms parents least understand. MetricTooltip was hover-only and display:none on phones — the mobile- primary audience had zero access to the Attainment 8 / Progress 8 / EBacc definitions. It is now a real button: tap/click/keyboard toggleable with outside-click and Escape dismissal, 24px target, shown at all viewports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { METRIC_EXPLANATIONS } from '@/lib/metrics';
|
|
import styles from './MetricTooltip.module.css';
|
|
|
|
interface MetricTooltipProps {
|
|
metricKey?: string;
|
|
label?: string;
|
|
plain?: string;
|
|
detail?: string;
|
|
}
|
|
|
|
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;
|
|
|
|
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>
|
|
);
|
|
}
|