fix(web): address trajectory pagination review
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user