Repository navigation
feat: cell merge (engine model + rendering, selection, context menu) - #148
Merged
Merged
Conversation
Store a merge as `merge: { rows, cols }` on its top-left (anchor) cell; covered
cells are derived. Adds Sheet.merge / unmerge / getMerges / getMergeAt /
expandAreaByMerges, keeps merges coherent across insert/remove rows & cols
(grow, shrink, hand the anchor to the next surviving cell), move (incl.
cross-sheet) and undo/redo, and refuses to sort while a merge spans rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Render: the host cell (anchor, or the first rendered cell when the anchor is scrolled out) stretches its content over the range; inner grid lines are painted with the cell surface. - Selection: pointing a covered cell chooses the anchor, arrows/Enter/Tab step over merges, selections grow to include every merge they touch. - Context menu: "Merge cells" / "Unmerge cells". - Storybook Basic/Merge + e2e; e2e `go()` honours STORYBOOK_URL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New `operations.Merge` flag (262144), included in `ReadOnly`. - USER merge is refused when any cell of the merge-expanded area carries Merge, or a Write-protected cell would lose its value; USER unmerge is refused when the anchor carries Merge. SYSTEM calls are unrestricted. - `Sheet.canMerge(area)` / `canUnmerge(area)` drive the context-menu state. - Init-time (column/row default) prevention is read without populating cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor drops keydowns that arrive within a frame of the previous one. Shift/Alt counted as such a keydown, so the key they modify could be swallowed when pressed quickly after them (e.g. Shift+Arrow right after a click). Exempt them like Meta/Control. Fixes the CI-only failure of the merge Shift+Arrow e2e, which no longer needs a workaround. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Copy-paste carries source merges, clipped to the destination (and tiled when it repeats). Values-only paste keeps the destination layout and drops values that would land on covered cells. - A paste or cut-paste dissolves destination merges it cuts through (anchor outside the destination); a merge whose anchor is overwritten is replaced. Refused (USER) when such a merge is Merge-protected. - Cut refuses a source that cuts through a merge; redo of a move re-applies the dissolved merges. - With no selection, copying a merge anchor copies the whole merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 of 3 tasks
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.
Summary
Adds merged cells — the engine model plus rendering, selection, and context-menu operations in react-core.
Engine (
@gridsheet/engine)merge: { rows, cols }(newMergeSpantype); covered cells are derived. Works frombuildInitialCellstoo ({ B2: { merge: { rows: 2, cols: 3 } } }).SheetAPI:merge({ area }),unmerge({ area }),getMerges(),getMergeAt(point),expandAreaByMerges(area).mergekeeps only the anchor's value; covered values are discarded (undoable). A partially overlapped merge is absorbed.sortRowsrefuses while any merge spans rows.UI (
@gridsheet/react-core,@gridsheet/webstyles)--gs-surface; the grid lines are the--gs-borderbackdrop showing through, sotransparentwould not hide them.Copy / cut / paste
Merge-protected.Prevention
operations.Mergeflag (262144), included inReadOnly(soReadOnly's numeric value changes).Merge, or when aWrite-protected cell would lose its value. A USER unmerge is refused when the anchor carriesMerge. SYSTEM (programmatic) calls are not restricted.Sheet.canMerge(area)/canUnmerge(area)decide whether the context-menu items are enabled.Also fixed
Editordebounced bare Shift/Alt keydowns (like any key), so the key they modify could be dropped when pressed within a frame — e.g. a quick Shift+Arrow right after a click selected nothing. Shift/Alt are now exempt, like Meta/Control.Not in this PR
<mergeCells>import/export.Test plan
packages/engine/src/merge.spec.ts(40 cases: merge/unmerge, undo/redo, formulas, insert/remove, sort, move incl. cross-sheet, prevention, copy/paste, cut/paste).e2e/merge.spec.ts(9 cases: rendering, pointing + arrows/Enter, Shift+arrow / drag / Tab, selection growth, context-menu merge/unmerge + undo/redo, scrolled-out anchor, Merge prevention, copy-paste, cut-paste) against Storybook Basic/Merge. The full e2e suite passed (99/99).typecheck:all, react-core typecheck.e2e/utils.ts:go()now honoursSTORYBOOK_URL, so the suite can run against a Storybook on a port other than 5233.🤖 Generated with Claude Code