feat(rankings,metrics): score visible on phones; definitions usable everywhere (P2.2, P2.3)
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
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>
This commit is contained in:
@@ -6,7 +6,17 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 0.85em;
|
||||
/* 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;
|
||||
@@ -14,8 +24,9 @@
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.wrapper:hover .icon {
|
||||
color: var(--accent-coral, #e07256);
|
||||
.wrapper:hover .icon,
|
||||
.icon[aria-expanded="true"] {
|
||||
color: var(--accent-coral-dark, #b04a2e);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@@ -39,12 +50,18 @@
|
||||
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||
}
|
||||
|
||||
/* Keep tooltip visible when hovering over it */
|
||||
.wrapper:hover .tooltip {
|
||||
/* 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: '';
|
||||
@@ -75,19 +92,23 @@
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
/* Flip tooltip below when near top of screen */
|
||||
@media (max-width: 480px) {
|
||||
.tooltip {
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
/* On phones the icon was rendering at ~9px and the tooltip relied on
|
||||
:hover, which doesn't fire on touch. Rather than build a tap-to-show
|
||||
layer with backdrop dismissal, hide the helper entirely — the metric
|
||||
labels themselves carry the meaning. */
|
||||
/* 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) {
|
||||
.wrapper {
|
||||
display: none;
|
||||
.tooltip {
|
||||
left: -12px;
|
||||
right: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
left: 16px;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { METRIC_EXPLANATIONS } from '@/lib/metrics';
|
||||
import styles from './MetricTooltip.module.css';
|
||||
|
||||
@@ -16,12 +17,43 @@ export function MetricTooltip({ metricKey, label, plain, detail }: MetricTooltip
|
||||
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}>
|
||||
<span className={styles.icon} aria-label={tooltipLabel ?? 'More information'} role="img">ⓘ</span>
|
||||
<span className={styles.tooltip} role="tooltip">
|
||||
<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>}
|
||||
|
||||
@@ -55,13 +55,13 @@
|
||||
}
|
||||
|
||||
.phaseTabActive {
|
||||
background: var(--accent-coral, #e07256);
|
||||
background: var(--accent-coral-dark, #b04a2e);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.phaseTabActive:hover {
|
||||
background: var(--accent-coral, #e07256);
|
||||
background: var(--accent-coral-darker, #9c3f26);
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
@@ -290,7 +290,7 @@
|
||||
}
|
||||
|
||||
.schoolLink:hover {
|
||||
color: var(--accent-coral, #e07256);
|
||||
color: var(--accent-coral-dark, #b04a2e);
|
||||
}
|
||||
|
||||
.areaCell,
|
||||
@@ -298,6 +298,14 @@
|
||||
color: var(--text-secondary, #5c564d);
|
||||
}
|
||||
|
||||
/* LA subline under the school name — mobile only (Area column hidden there). */
|
||||
.schoolCellArea {
|
||||
display: none;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted, #8a847a);
|
||||
}
|
||||
|
||||
.valueCell {
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
@@ -374,20 +382,26 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Hide less-critical columns on mobile so the metric value stays visible */
|
||||
/* Hide less-critical columns on mobile so the metric value stays visible.
|
||||
Area moves to a subline under the school name (.schoolCellArea) — with a
|
||||
four-column layout the value still overflowed a 390px viewport and the
|
||||
whole point of the page (the score) needed a sideways swipe to see. */
|
||||
.typeHeader,
|
||||
.typeCell,
|
||||
.actionHeader,
|
||||
.actionCell {
|
||||
.actionCell,
|
||||
.areaHeader,
|
||||
.areaCell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.schoolHeader {
|
||||
min-width: 140px;
|
||||
.schoolCellArea {
|
||||
display: block;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.areaHeader {
|
||||
min-width: 80px;
|
||||
.schoolHeader {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.valueHeader,
|
||||
|
||||
@@ -75,7 +75,7 @@ export function RankingsView({
|
||||
};
|
||||
|
||||
const handlePhaseChange = (phase: string) => {
|
||||
const defaultMetric = phase === 'secondary' ? 'attainment_8_score' : 'rwm_high_pct';
|
||||
const defaultMetric = phase === 'secondary' ? 'attainment_8_score' : 'rwm_expected_pct';
|
||||
updateFilters({ phase, metric: defaultMetric });
|
||||
};
|
||||
|
||||
@@ -273,6 +273,11 @@ export function RankingsView({
|
||||
<a href={schoolUrl(ranking.urn, ranking.school_name)} className={styles.schoolLink}>
|
||||
{ranking.school_name}
|
||||
</a>
|
||||
{/* On phones the Area column is hidden; the LA moves here
|
||||
so the metric value fits on screen without swiping. */}
|
||||
{ranking.local_authority && (
|
||||
<span className={styles.schoolCellArea}>{ranking.local_authority}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={styles.areaCell}>{ranking.local_authority || '-'}</td>
|
||||
<td className={styles.typeCell}>{ranking.school_type || '-'}</td>
|
||||
|
||||
Reference in New Issue
Block a user