Review a React virtualized results list

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+ <div
35+ 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+ <article
41+ 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+}