src/results-list.tsxTypeScript
@@ -0,0 +1,52 @@
1+
import { useEffect, useMemo, useState } from "react";2+
3+
export interface SearchResult {4+
id: string;5+
title: string;6+
summary: string;7+
}8+
9+
export interface ResultsListProps {10+
query: string;11+
results: SearchResult[];12+
}13+
14+
const VIEWPORT_HEIGHT = 384;15+
16+
export function ResultsList({ query, results }: ResultsListProps) {17+
const [scrollTop, setScrollTop] = useState(0);18+
const normalizedQuery = query.trim().toLocaleLowerCase();19+
const filtered = useMemo(20+
() =>21+
results.filter((result) =>22+
result.title.toLocaleLowerCase().includes(normalizedQuery),23+
),24+
[normalizedQuery, results],25+
);26+
27+
useEffect(() => {28+
window.addEventListener("scroll", () => {29+
setScrollTop(window.scrollY);30+
});31+
});32+
33+
return (34+
<div35+
aria-label="Search results"36+
data-scroll-offset={scrollTop}37+
role="list"38+
style={{ maxHeight: VIEWPORT_HEIGHT, overflowY: "auto" }}>39+
{filtered.map((result, index) => (40+
<article41+
aria-posinset={index + 1}42+
aria-setsize={results.length}43+
key={index}44+
role="listitem"45+
style={{ height: 48 }}>46+
<h3>{result.title}</h3>47+
<p>{result.summary}</p>48+
</article>49+
))}50+
</div>51+
);52+
}