Context
Found while building the acceptance journey of epic #19658 (#19666), on its 100k-row example, which is not on dev yet. At 1400×900, "Jump to row 50000" (view.scrollByRows(0, 49999), the keyboard navigation path) settles with rows 49955–49986 rendered, the last of them already below the view. Row 50000 is not even mounted, and it stays that way for the 2 s sampled. #19663's author saw the same independently: scrollByRows(0, 299) and (0, 64) intermittently never displayed the requested record, and left the jump and final-row boundary to #19666.
The Problem
In content-sized mode, Body#scrollByRows scrolls down so the target's bottom meets the view's bottom, at offsets computed from the estimate for rows not measured yet. The rows then render and are measured. grid.View's private #changeRowHeights (#19664) keeps the row at the top edge still. So every row between the top edge and the target that grows from the 32px estimate (to 62–119px in the rows sampled on the example) pushes the target further down.
The navigated row is the one the user is looking at, but the correction anchors another one.
The Architectural Reality
src/grid/Body.mjs, scrollByRows: the keyboard navigation path of selection.grid.RowModel and CellModel, and of the example's jump field. In content-sized mode, scrolling up puts the row's top at the view's top. Scrolling down puts its bottom at the view's bottom, unless the row is taller than the view, which shows its top.
src/grid/View.mjs, #changeRowHeights: the one place height changes move the scroll offset, as scrollCorrection plus scrollEpoch in the next update's meta (#19664).
View#scrollTop follows every scroll that reaches the App worker (View#syncBodies), the navigation's own included.
- Design authority:
scrollByRows' stated alignment ("its bottom meets the viewport bottom") and the epic's predicate that keyboard navigation and the final row work.
The Fix
View#scrollRowIntoView(index, edge) takes over the content-sized branch of scrollByRows. It scrolls to the aligned offset and keeps the row there as its navigation target.
- While a landed navigation target exists,
#changeRowHeights moves the scroll offset so that the target meets its edge again, instead of holding the row at the top edge.
- A scroll from elsewhere ends the target, as do a store load and a local filter, which give its index another record.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback / edge case |
Docs |
Evidence |
View#scrollRowIntoView(index, edge), consumed by Body#scrollByRows |
scrollByRows' stated alignment (#19661), this ticket |
Scrolls through DomAccess.scrollTo so the row at index meets the top edge, or the bottom edge unless it is taller than the view, and makes it the navigation target |
Already aligned: the target lands at once, since no scroll arrives |
JSDoc on scrollRowIntoView and #alignedScrollTop |
e2e AnchorCorrection: "a jump down to an unseen row keeps it at the bottom edge while the rows around it are learned" |
The navigation target's lifetime (#navigationTarget) |
This ticket, #19699 review |
Lands when the view's scroll arrives within 1px of the aligned offset. While landed, #changeRowHeights realigns its edge instead of holding the row at the top edge |
Ends on any other scroll, and on a load or a local filter, which give its index another record. Sort and record mutations fire a load; a new store with records runs the load handler |
JSDoc on #navigationTarget, onStoreFilter and onStoreLoad |
unit AnchorCorrection: "a row reached by keyboard keeps its edge while rows above it change, until another scroll" and "a filter ends a keyboard navigation's target, whose row index names another record now" |
Body#scrollByRows(index, step), the keyboard path of RowModel and CellModel |
#19661 |
Content-sized: gridContainer.view.scrollRowIntoView(newIndex, step < 0 ? 'top' : 'bottom') |
Fixed-height grids keep their arithmetic unchanged |
JSDoc on scrollByRows |
KeyboardNavigationScroll stays green |
Acceptance Criteria
Out of Scope
- Measuring the jump target before its first paint (#19663 leaves it out).
- Fixed-height grids, whose
scrollByRows is unchanged.
Avoided Traps
- Re-running
scrollByRows after the heights settle: the target would visibly jump away and back.
- Anchoring the target before its scroll lands: the App worker's offset still describes the old position, so the relative correction would land elsewhere.
Related
#19658 (epic) · #19664 (anchor correction) · #19661 (scrollByRows in content-sized mode) · #19666 (the journey that found it) · #19692 (the other defect from the same journey) · #19688 (#19663's PR adds callbacks to #changeRowHeights, but leaves the method itself and scrollByRows alone)
Live latest-open sweep: checked the latest 20 open issues at 2026-10-12T22:46:49Z, and the latest 5 again right before filing; no equivalent found. #19515 is column navigation.
A2A claim sweep: the 15 unread messages; no competing claim. #19663's author left this boundary to #19666, and the seam is acknowledged.
MC sweep: the query "keyboard navigation jump to row target leaves view out of view after row heights measured scrollByRows bottom aligned content-sized grid" returned 6 results; no prior decision found. #19661's design names the alignment rule only.
Own-assignment sweep: 9 open; #19666 and #19658 are the parents, and #19692 is the sibling. None overlap.
Origin Session ID: daac81df-5205-405e-a3af-aec56e5cca45
Context
Found while building the acceptance journey of epic #19658 (#19666), on its 100k-row example, which is not on
devyet. At 1400×900, "Jump to row 50000" (view.scrollByRows(0, 49999), the keyboard navigation path) settles with rows 49955–49986 rendered, the last of them already below the view. Row 50000 is not even mounted, and it stays that way for the 2 s sampled. #19663's author saw the same independently:scrollByRows(0, 299)and(0, 64)intermittently never displayed the requested record, and left the jump and final-row boundary to #19666.The Problem
In content-sized mode,
Body#scrollByRowsscrolls down so the target's bottom meets the view's bottom, at offsets computed from the estimate for rows not measured yet. The rows then render and are measured.grid.View's private#changeRowHeights(#19664) keeps the row at the top edge still. So every row between the top edge and the target that grows from the 32px estimate (to 62–119px in the rows sampled on the example) pushes the target further down.The navigated row is the one the user is looking at, but the correction anchors another one.
The Architectural Reality
src/grid/Body.mjs,scrollByRows: the keyboard navigation path ofselection.grid.RowModelandCellModel, and of the example's jump field. In content-sized mode, scrolling up puts the row's top at the view's top. Scrolling down puts its bottom at the view's bottom, unless the row is taller than the view, which shows its top.src/grid/View.mjs,#changeRowHeights: the one place height changes move the scroll offset, asscrollCorrectionplusscrollEpochin the next update's meta (#19664).View#scrollTopfollows every scroll that reaches the App worker (View#syncBodies), the navigation's own included.scrollByRows' stated alignment ("its bottom meets the viewport bottom") and the epic's predicate that keyboard navigation and the final row work.The Fix
View#scrollRowIntoView(index, edge)takes over the content-sized branch ofscrollByRows. It scrolls to the aligned offset and keeps the row there as its navigation target.#changeRowHeightsmoves the scroll offset so that the target meets its edge again, instead of holding the row at the top edge.Contract Ledger
View#scrollRowIntoView(index, edge), consumed byBody#scrollByRowsscrollByRows' stated alignment (#19661), this ticketDomAccess.scrollToso the row atindexmeets thetopedge, or thebottomedge unless it is taller than the view, and makes it the navigation targetscrollRowIntoViewand#alignedScrollTopAnchorCorrection: "a jump down to an unseen row keeps it at the bottom edge while the rows around it are learned"#navigationTarget)#changeRowHeightsrealigns its edge instead of holding the row at the top edge#navigationTarget,onStoreFilterandonStoreLoadAnchorCorrection: "a row reached by keyboard keeps its edge while rows above it change, until another scroll" and "a filter ends a keyboard navigation's target, whose row index names another record now"Body#scrollByRows(index, step), the keyboard path ofRowModelandCellModelgridContainer.view.scrollRowIntoView(newIndex, step < 0 ? 'top' : 'bottom')scrollByRowsKeyboardNavigationScrollstays greenAcceptance Criteria
AnchorCorrection,KeyboardNavigationScrollandWrapTextMeasure(unit and e2e) stay green.Out of Scope
scrollByRowsis unchanged.Avoided Traps
scrollByRowsafter the heights settle: the target would visibly jump away and back.Related
#19658 (epic) · #19664 (anchor correction) · #19661 (
scrollByRowsin content-sized mode) · #19666 (the journey that found it) · #19692 (the other defect from the same journey) · #19688 (#19663's PR adds callbacks to#changeRowHeights, but leaves the method itself andscrollByRowsalone)Live latest-open sweep: checked the latest 20 open issues at 2026-10-12T22:46:49Z, and the latest 5 again right before filing; no equivalent found. #19515 is column navigation. A2A claim sweep: the 15 unread messages; no competing claim. #19663's author left this boundary to #19666, and the seam is acknowledged. MC sweep: the query "keyboard navigation jump to row target leaves view out of view after row heights measured scrollByRows bottom aligned content-sized grid" returned 6 results; no prior decision found. #19661's design names the alignment rule only. Own-assignment sweep: 9 open; #19666 and #19658 are the parents, and #19692 is the sibling. None overlap.
Origin Session ID: daac81df-5205-405e-a3af-aec56e5cca45