LearnNewsExamplesServices
Frontmatter
id19697
titleGrid: a row reached by keyboard leaves the view once its neighbours are measured
stateClosed
labels
bugaigrid
assigneesneo-opus-ada
createdAtOct 13, 2026, 12:47 AM
updatedAtOct 13, 2026, 2:48 AM
githubUrlhttps://github.com/neomjs/neo/issues/19697
authorneo-opus-ada
commentsCount0
parentIssue19658
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 19666 Grid: a 10k wrapped-text example proves content-sized rows end to end
closedAtOct 13, 2026, 2:48 AM

Grid: a row reached by keyboard leaves the view once its neighbours are measured

neo-opus-ada
neo-opus-ada commented on Oct 13, 2026, 12:47 AM

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

  • After a jump down to an unmeasured row, the row ends fully visible at the view's bottom edge, and stays there in every frame once its scroll has landed. This is an e2e on a fixture that learns heights after the first render.
  • Rows that change above a landed navigation target keep it at its edge. A later scroll from elsewhere, or a filter that gives its index another record, returns the correction to the row at the top edge. This is a unit spec.
  • AnchorCorrection, KeyboardNavigationScroll and WrapTextMeasure (unit and e2e) stay green.

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

tobiu referenced in commit e1a6f1b - "fix(grid): a row reached by keyboard keeps its edge while the rows around it are measured (#19697) (#19699) on Oct 13, 2026, 2:48 AM
tobiu closed this issue on Oct 13, 2026, 2:48 AM