Files
school_compare/nextjs-app/components/school/AdmissionsViewToggle.tsx
T

68 lines
2.0 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, type ReactNode } from 'react';
import styles from './schoolSections.module.css';
export interface AdmissionsView {
id: string;
/** Button text. "This year" and the "N-year trend" label are asserted by the
* e2e journey, so they are not free to drift. */
label: string;
content: ReactNode;
/** The year view sizes its tile grid to match the chart beside it, so each
* view keeps its own class rather than sharing one wrapper style. */
className?: string;
}
/**
* The only interactive part of the primary admissions section, and the only
* client component in components/school/.
*
* Every view is always present in the DOM and visibility is toggled with the
* `hidden` attribute, so the server-rendered markup passed in as content never
* ships as client JavaScript — this component carries the state and nothing
* else.
*
* It spans the header and the viewport because the segmented control sits
* inside .admissionsHeader beside the <h2> while the viewport is a sibling
* below it; wrapping only one would change the DOM the CSS depends on.
*/
export function AdmissionsViewToggle({
title,
subtitle,
views,
}: {
title: ReactNode;
subtitle: ReactNode;
views: AdmissionsView[];
}) {
const [active, setActive] = useState(views[0]?.id);
return (
<>
<div className={styles.admissionsHeader}>
<h2 className={styles.sectionTitle}>{title}</h2>
<div className={styles.admissionsSeg} role="group" aria-label="Admissions view">
{views.map((v) => (
<button
key={v.id}
type="button"
aria-pressed={active === v.id}
onClick={() => setActive(v.id)}
>
{v.label}
</button>
))}
</div>
</div>
{subtitle}
<div className={styles.admissionsViewport}>
{views.map((v) => (
<div key={v.id} className={v.className} hidden={active !== v.id}>
{v.content}
</div>
))}
</div>
</>
);
}