feat: sticky left columns (stickyLeftColumnNumber) - #386
Open
shahidullahkhankhattak wants to merge 1 commit into
Open
feat: sticky left columns (stickyLeftColumnNumber)#386shahidullahkhankhattak wants to merge 1 commit into
shahidullahkhankhattak wants to merge 1 commit into
Conversation
Adds optional stickyLeftColumnNumber to pin the first N data columns after the gutter so they stay visible during horizontal scroll. Closes nick-keller#326 Co-authored-by: Cursor <cursoragent@cursor.com>
✅ Deploy Preview for react-datasheet-grid canceled.
|
rodrigoescandon
added a commit
to rodrigoescandon/react-datasheet-grid
that referenced
this pull request
Aug 5, 2026
Root cause: .dsg-row is a plain block container. .dsg-cell children are position: absolute (removed from normal flow) except cells that need to stick, which use position: sticky and therefore stay in normal in-flow layout. With only the gutter column sticky (pre-nick-keller#386 baseline) there was exactly one in-flow child per row, which happened to land at the correct spot by luck. PR nick-keller#386 added up to stickyLeftColumnNumber more position:sticky cells per row; being in-flow, block layout stacked them vertically -- one row-height per extra sticky cell -- which is exactly the observed bug (pinned header cell displaced several rows down, empty/ misaligned pinned cells in the first rows). This is the same class of issue the pre-existing .dsg-cell-sticky-right `transform: translateY(-100%)` hack works around for exactly 2 in-flow cells (gutter + sticky-right); it doesn't generalize to N sticky-left columns. Fix: keep sticky-left cells position: absolute (out of flow, like ordinary cells) instead of position: sticky, and compute their `left` in JS as the column virtualizer's own `col.start` (its natural, unpinned position) plus the container's current horizontal scrollLeft, tracked in Grid.tsx via the container's onScroll handler. This keeps them visually pinned without touching row flow layout at all, and leaves the existing gutter/sticky-right mechanism untouched. Using `col.start` (rather than the `columnRights` prop) as the offset anchor also fixes a related bug found during verification: columnRights can be transiently undefined before react-resize-detector reports a real container width (it deliberately skips its on-mount measurement), which made every sticky-left cell fall back to left: 0 and collapse onto the gutter. col.start is always populated -- it falls back to the same 100px estimate ordinary cells use -- so sticky-left cells stay aligned with their neighbors in that window too. - src/style.css: drop `position: sticky` from .dsg-cell-sticky-left (z-index kept for stacking above scrolled-under cells); add comment explaining why, referencing the sticky-right translateY hack. - src/components/Grid.tsx: track scrollLeft state via the container's onScroll; add getStickyLeftOffset(colStart) = colStart + scrollLeft; wire it into both the header-row and body-row sticky-left cells. - dist/: rebuilt (npm run build) and verified npm test passes (16 suites / 100 tests, including tests/stickyColumns.test.tsx). - package.json: version -> 4.11.6-grid.2. Verified live in the consumer app (base-de-datos-grid, packages/ payload-grid-view) against Directorio's 5391-row grid with stickyLeftColumnNumber={2}: row 1 ('"El Chango" Cabral') and the header render aligned; horizontal scroll keeps the gutter, open-record arrow, and Nombre columns pinned with opaque backgrounds while other columns slide underneath, at both small and large scroll offsets; alignment holds during vertical scroll while horizontally scrolled; dark theme (html[data-theme='dark'] .pgv-grid) stays legible with opaque cell backgrounds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rodrigoescandon
added a commit
to rodrigoescandon/react-datasheet-grid
that referenced
this pull request
Aug 5, 2026
Root cause of the earlier JS-driven approaches (98f4543, f3168a2): any scroll-event-driven positioning -- even one that bypasses React and writes a CSS var straight to the DOM -- still runs on the main thread in response to a dispatched 'scroll' event. On macOS momentum/fast-wheel scrolling the compositor paints frames faster than the main thread can dispatch and handle those events, so the sticky-left cells' visual position could render a frame (or several) behind the actual scrollLeft, producing visible drift/vanish/snap-back under fast scrolling. Fix: make sticky-left cells (gutter + pinned data columns) real in-flow `position: sticky` elements again, pinned by the browser's compositor with no JS involved in the scroll path at all -- categorically immune to the main-thread-lag failure mode, not just less prone to it. This requires solving the original blocker from PR nick-keller#386: .dsg-row is a plain block container, and in-flow sticky cells (unlike ordinary cells, which are position: absolute and therefore out of flow) stack vertically under block layout -- one row-height per sticky cell -- which is what broke PR nick-keller#386 for N > 1 pinned columns in the first place. Fix layout instead of abandoning sticky: 1. src/style.css: `.dsg-row { display: flex }`. Ordinary cells are position: absolute and therefore not flex items at all -- completely unaffected. The in-flow sticky cells (gutter, sticky-left, sticky-right) become flex items on one horizontal line instead of stacking. 2. src/components/Grid.tsx: fixed a latent DOM-order bug in the column virtualizer's rangeExtractor. It force-includes sticky column indices via repeated `result.unshift(stickyCol)` in an ascending loop, which builds the array in DESCENDING order (each unshift lands in front of the last) whenever those columns fall outside the naturally-virtualized range (i.e. once scrolled past them). That was harmless when sticky-left cells were position: absolute (DOM order didn't affect visual position, see f3168a2) but would render pinned columns in reverse visual order under flex. Now collects the missing indices and unshifts them together to preserate ascending order. 3. src/style.css: sticky cells get `left: col.start` (unchanged, still set inline per-cell in Cell.tsx) plus `flex: none` to keep their exact measured width regardless of available flex-line space. col.start for a sticky column is the cumulative width of columns 0..i-1, which -- since the sticky set is always the contiguous range starting at 0 -- is exactly the cumulative width of the *prior sticky columns*, precisely what `position: sticky`'s `left` needs. 4. src/style.css: removed `.dsg-cell-sticky-right`'s `transform: translateY(-100%)`. That hack compensated for exactly two in-flow children (gutter, sticky-right) stacking vertically under block layout; under flex, in-flow children lay out horizontally on one line, so the vertical stacking it compensated for no longer happens and the transform would now be actively wrong. 5. src/components/Grid.tsx, src/style.css: removed the --dsg-sticky-x custom-property/transform mechanism (and the onScroll handler that drove it) entirely -- no longer needed now that positioning is native CSS sticky. 6. src/style.css: gave `.dsg-cell-sticky-left` its own border-right/border-bottom (using --dsg-border-color) instead of relying on the shared box-shadow trick ordinary cells use to draw grid lines -- that trick depends on the two cells' edges staying pixel- aligned as the grid scrolls, which doesn't hold for a sticky cell with an opaque background sitting over a non-sticky neighbor. This was previously patched only in the consumer app's CSS (base-de-datos-grid/packages/payload-grid-view); folded the same fix into the fork so the engine is correct standalone. Verified in the consumer app (base-de-datos-grid, Directorio, 5391 rows, stickyLeftColumnNumber={2}): gutter + open-record arrow + Nombre stay rock-solid pinned and opaque through rapid back-and-forth horizontal scrolling, scrolling to the far right, and vertical scrolling while horizontally offset (rows 51-74 checked) -- both light and dark themes. Column resize (PR nick-keller#350) still works and correctly reflows the sticky layout. npm test: 100/100 (one paste.test.tsx case flaked once under full- suite timing pressure -- an act()-wrapping warning unrelated to this change -- and passed both in isolation and on a full-suite rerun). - src/components/Grid.tsx: rangeExtractor DOM-order fix; dropped handleScroll/--dsg-sticky-x wiring, onScroll passed through directly. - src/style.css: .dsg-row display: flex; .dsg-cell-sticky-left/-right/ -gutter position: sticky with flex: none; sticky-left border-right/ -bottom; removed --dsg-sticky-x var and sticky-right's translateY hack. - dist/: rebuilt (npm run build). - package.json: version -> 4.11.6-grid.4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds optional
stickyLeftColumnNumberso the first N data columns after the gutter stay visible during horizontal scroll. Includes a small test for the sticky cell class.Closes #326