'use client'; /** * The scroller and its arrows. * * `children` are the server-rendered cards and `header` the server-rendered * heading and lede: both stay server components, passed through, so this file * owns a DOM ref and nothing else. That is what keeps all six links in the * initial HTML — a carousel that mounted cards on click would put four of the * six beyond a crawler and beyond a reader with no JavaScript. * * With JavaScript off this degrades to a horizontally scrollable row, which is * still usable by touch and trackpad. */ import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'; import styles from './NearbySchools.module.css'; /** Three cards fit the row, so fewer than four has nowhere to scroll to. * Below 640px the arrows are not rendered at all — see the stylesheet. */ const VISIBLE = 3; /** * Why a tolerance rather than `=== 0`. * * The scroller carries 2px of padding so focus rings are not clipped, and * scroll-snap treats that padding as the first card's snap position — a row at * rest reports scrollLeft 2, not 0. Sub-pixel rounding moves it again at other * zoom levels. An exact test leaves the back arrow live on first paint, * pointing nowhere. */ const EDGE = 8; export function NearbySchoolsCarousel({ count, labelledBy, header, children, }: { count: number; labelledBy: string; header: ReactNode; children: ReactNode; }) { const scroller = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); const scrollable = count > VISIBLE; const sync = useCallback(() => { const node = scroller.current; if (!node) return; const max = node.scrollWidth - node.clientWidth; setAtStart(node.scrollLeft <= EDGE); setAtEnd(node.scrollLeft >= max - EDGE); }, []); // `atEnd` is not only the forward arrow's disabled state: below 640px, where // no arrow is rendered, it is the only thing driving the scroll-fade. // Also on mount: the first measurement can only happen once there is layout. useEffect(sync, [sync]); const page = (direction: 1 | -1) => { const node = scroller.current; if (!node) return; // A page is what the reader can see, so the viewport is the step. node.scrollBy({ left: direction * node.clientWidth, behavior: 'smooth' }); }; return ( <>
{header} {scrollable && (
)}
); } function Chevron({ direction }: { direction: 'prev' | 'next' }) { return ( ); }