fix(web): address trajectory pagination review

This commit is contained in:
_Kerman
2026-08-13 14:38:15 +08:00
parent 93d7eea12a
commit 2c837c267f
5 changed files with 49 additions and 12 deletions

View File

@@ -93,6 +93,15 @@
cursor: default;
}
.visuallyHidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.table:not([data-scroll-ready='true']) {
visibility: hidden;
}

View File

@@ -30,6 +30,7 @@ import css from './TrajectoryTable.module.css'
const BOTTOM_FOLLOW_THRESHOLD_PX = 2
const OLDER_LOAD_THRESHOLD_PX = 48
const HISTORY_LOAD_ROW_HEIGHT_PX = 30
const VIRTUALIZATION_THRESHOLD = 100
const VIRTUAL_OVERSCAN_ROWS = 12
const VIRTUAL_INITIAL_VIEWPORT_HEIGHT_PX = 600
@@ -364,7 +365,7 @@ export interface TrajectoryTableProps {
recordFocus?: { readonly index: number } | null
/** Whether the initial history tail is still loading. */
historyLoading?: boolean
/** Whether another surface is loading one older history page. */
/** Whether one older history page request is pending anywhere. */
olderHistoryLoading?: boolean
/** First loaded raw event, used to preserve scroll position after prepending a page. */
historyStartSeq?: number | undefined
@@ -1772,6 +1773,7 @@ export function TrajectoryTable({
const virtualRowStructure = useStableVirtualRowStructure(projectedVirtualRows)
const virtualizationEnabled = hasOlderRecords
|| records.length > VIRTUALIZATION_THRESHOLD
const virtualScrollMargin = hasOlderRecords ? HISTORY_LOAD_ROW_HEIGHT_PX : 0
const estimateVirtualRowSize = useCallback(
(index: number) => virtualRowStructure[index]?.height ?? 30,
[virtualRowStructure],
@@ -1790,6 +1792,7 @@ export function TrajectoryTable({
initialRect: { width: 0, height: VIRTUAL_INITIAL_VIEWPORT_HEIGHT_PX },
anchorTo: 'end',
overscan: VIRTUAL_OVERSCAN_ROWS,
scrollMargin: virtualScrollMargin,
scrollEndThreshold: BOTTOM_FOLLOW_THRESHOLD_PX,
})
const virtualIndexByRecordId = useMemo(() => {
@@ -1804,10 +1807,15 @@ export function TrajectoryTable({
return indexes
}, [projectedVirtualRows])
const virtualItems = virtualizationEnabled ? rowVirtualizer.getVirtualItems() : []
const virtualTop = virtualItems[0]?.start ?? 0
const virtualTop = Math.max(0, (virtualItems[0]?.start ?? 0) - virtualScrollMargin)
const virtualBottom = virtualItems.length === 0
? 0
: Math.max(0, rowVirtualizer.getTotalSize() - (virtualItems.at(-1)?.end ?? 0))
: Math.max(
0,
rowVirtualizer.getTotalSize()
+ virtualScrollMargin
- (virtualItems.at(-1)?.end ?? 0),
)
const renderedRecords = virtualizationEnabled
? virtualItems.flatMap((item) => {
const row = projectedVirtualRows[item.index]
@@ -2220,12 +2228,19 @@ export function TrajectoryTable({
</colgroup>
<tbody>
{hasOlderRecords && (
<tr className={css.historyLoadRow} data-history-load="">
<tr
className={css.historyLoadRow}
data-history-load=""
aria-rowindex={1}
>
<td colSpan={2}>
<button
type="button"
className={css.historyLoadButton}
disabled={olderBusy || onLoadOlder === undefined}
aria-label={olderBusy
? 'Loading earlier history…'
: 'Load earlier history'}
onClick={() => {
const pane = tablePaneRef.current
if (pane !== null) requestOlder(pane, false)
@@ -2234,12 +2249,12 @@ export function TrajectoryTable({
{olderBusy && (
<span className={css.historyLoadingSpinner} aria-hidden="true" />
)}
<span
role={olderBusy ? 'status' : undefined}
aria-live={olderBusy ? 'polite' : undefined}
>
<span aria-hidden="true">
{olderBusy ? 'Loading earlier history…' : 'Load earlier history'}
</span>
<span className={css.visuallyHidden} role="status" aria-live="polite">
{olderBusy ? 'Loading earlier history…' : ''}
</span>
</button>
</td>
</tr>

View File

@@ -418,7 +418,9 @@ describe('TrajectoryTable', () => {
await waitFor(() => { expect(onLoadOlder).toHaveBeenCalledOnce() })
expect(screen.getByRole('status').textContent).toContain('Loading earlier history…')
resolveOlder?.(true)
await waitFor(() => { expect(screen.queryByRole('status')).toBeNull() })
await waitFor(() => {
expect(screen.getByRole('status').textContent).toBe('')
})
scrollHeight = 260
view.rerender(
<TrajectoryTable
@@ -458,7 +460,10 @@ describe('TrajectoryTable', () => {
const table = screen.getByRole('table')
const loadButton = screen.getByRole('button', { name: 'Load earlier history' })
expect(table.querySelector('tbody > tr:first-child')?.contains(loadButton)).toBe(true)
const loadRow = table.querySelector('tbody > tr:first-child')
expect(loadRow?.contains(loadButton)).toBe(true)
expect(loadRow?.getAttribute('aria-rowindex')).toBe('1')
expect(screen.getByRole('status').textContent).toBe('')
expect(table.getAttribute('aria-rowcount')).toBe('4')
expect((await screen.findByRole('row', { name: /ASSISTANT/ })).getAttribute('aria-rowindex'))
.toBe('2')