Skip to content

feat: autoExpand option to grow the sheet on paste / autofill overflow - #149

Merged
righ merged 6 commits into
masterfrom
righ/feat-auto-expand
Oct 5, 2026
Merged

righ merged 6 commits into
masterfrom
righ/feat-auto-expand

Conversation

@righ

@righ righ commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds options.autoExpand — 'none' | 'vertical' | 'horizontal' | 'both' (default 'none', i.e. current behavior). When a paste or an autofill runs past the last row/column, the sheet appends the missing rows/columns instead of clipping the overflow.

<GridSheet initialCells={cells} options={{ autoExpand: 'both', limits: { maxRows: 1000 } }} />
  • Paste: internal copy, cut→paste, and clipboard text/HTML (copy / copyAsync / move / writeRawCellMatrix).
  • Autofill (Handsontable autoInsertRow-style, but provisional): while the fill drag's pointer is past the grid content's edge — in the whitespace of a larger box or outside it — and the grid is scrolled to that edge, provisional ghost rows/columns are added one at a time, faster the further past the edge (200ms → 20ms). They're display-only (no sheet change, no history); releasing grows the sheet up to the reached row/column, unreached ghosts vanish. Growth stops when the pointer is back over a cell.
  • New rows/columns copy the last row's/column's layout (like inserting below the last one).
  • Capped by limits.maxRows / maxCols; the rest is still clipped.
  • One undo step: the growth and the write are recorded as a new BATCH history entry. If the operation is refused after growing (e.g. a move cutting through a merge), the growth is rolled back.
  • A paste/autofill that grew the sheet scrolls the written range's far corner into view, so the growth is visible (the viewport keeps its size).

Implementation notes

  • engine: Sheet.autoExpand, getAutoExpansion(), withAutoExpand() / withAutoExpandAsync(), history batching in _pushHistory; undo/redo switch extracted into _undoHistory / _redoHistory so BATCH can recurse. Autofill clamps the drag target to what the sheet can grow to.
  • react-core (Tabular): ghost rows/cols are rendered as real table rows/cells (.gs-ghost-cell, .gs-ghost-th) so sticky headers and column alignment work; virtualize() keeps the trailing rows/cols rendered when scrolled past the content. While growing, the target follows the last ghost and the grid stays pinned to that edge; StoreObserver / ScrollHandle edge auto-scroll no longer snap the target back to the last real row/col there.
  • Fix (pre-existing): releasing an autofill drag over a ScrollHandle submitted the fill twice (two undo steps). StoreObserver's capture-phase mouseup is now the only submitter, as its comment already intended.

Limitations

  • No provisional ghost rows during autofill on sheets tall enough to hit the scroll-height cap (~600k+ rows); paste expansion still works there.
  • Undoing a very large (>20k cells) expanding paste runs synchronously, without the progress bar.

Test plan

  • Jest: packages/engine/src/auto-expand.spec.ts (directions, limits, BATCH undo/redo, writeRawCellMatrix, move, autofill, rollback on refused op) — engine suite 78/78
  • e2e: e2e/auto-expand.spec.ts (paste grow + undo, none clips without scrolling, vertical only, grown cells scrolled into view; autofill: provisional rows held past the edge then committed in one undo, vanish when dragged back, keep growing when leaving an enlarged box before its whitespace fills (down/right), stop once the pointer is back over a cell)
  • Full e2e: 107/107
  • Storybook: Restriction/AutoExpand (mode switcher, resizable, centered, limits 1000 × 100)

🤖 Generated with Claude Code

righ and others added 2 commits October 4, 2026 04:47
`options.autoExpand` ('none' | 'vertical' | 'horizontal' | 'both', default
'none') lets a paste or an autofill that runs past the last row/column append
the missing rows/columns instead of clipping them, capped by
limits.maxRows / maxCols.

- engine: Sheet.autoExpand; copy / copyAsync / move / writeRawCellMatrix and
  Autofill grow the sheet first. New BATCH history so the growth and the write
  undo/redo as one step; the growth is rolled back if the operation is refused
  (e.g. by a merge).
- react-core: while dragging the autofill handle, ghost rows/columns are
  rendered past the edge (as real table rows/cells, so sticky headers stay) so
  the drag can reach cells that don't exist yet. A paste that grew the sheet
  scrolls the pasted corner into view.
- fix: releasing an autofill drag over a ScrollHandle submitted the fill twice
  (two undo steps); StoreObserver's mouseup is now the only submitter.
- docs, storybook story (Restriction/AutoExpand), jest + e2e specs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
righ and others added 4 commits October 4, 2026 18:11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The autoExpand ghost area is now drawn outside the grid — portaled to <body>,
positioned fixed right past the last row/column (only once that edge is
scrolled into view) — instead of as extra table rows inside the scroll area.
Cells in the fill target are highlighted; StoreObserver doesn't edge-scroll
while the pointer is over the ghost layer. Reverts the in-table ghost rendering
and the virtualize() "scrolled past the content" tweaks it needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…autofill

Replaces the outside-the-sheet ghost layer with provisional ghost rows/columns
rendered inside the grid (as table rows/cells, so sticky headers stay):

- While the fill drag's pointer is past the grid content's edge (the whitespace
  of a larger box, or outside it) and the grid is scrolled to that edge, add one
  ghost row/col at a time, faster the further past the edge (200ms → 20ms).
- Ghosts are display-only (no sheet change, no history); releasing grows the
  sheet up to the reached row/col in one undo step, unreached ghosts vanish.
- The target follows the last ghost while past the edge, the grid stays pinned
  to that edge as it grows, and growth stops once the pointer is back over a
  cell. The edge ScrollHandle no longer snaps the target back to the last real
  row/col there.
- An autofill that grew the sheet scrolls the filled corner into view.
- Storybook AutoExpand story: limits raised to 1000 rows x 100 cols.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@righ
righ merged commit 725951f into master Oct 5, 2026
4 checks passed
@righ
righ deleted the righ/feat-auto-expand branch October 5, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant