Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(tables): Shift+Space preserves anchor for continued keyboard use
Previously Shift+Space cleared selectionAnchor, making all subsequent
keyboard shortcuts (arrows, enter, tab) inoperable until a cell was
clicked. Now it preserves the anchor and only clears focus, so users
can arrow-navigate and Shift+Space to toggle multiple rows in sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • Loading branch information
waleedlatif1 and claude committed Mar 17, 2026
commit 3e51e1c9aa90353002001d05e4f899a08cc99716
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,24 @@ export function Table({
return
}

if (e.key === ' ' && e.shiftKey) {
const a = selectionAnchorRef.current
if (!a || editingCellRef.current) return
e.preventDefault()
setSelectionFocus(null)
setCheckedRows((prev) => {
const next = new Set(prev)
if (next.has(a.rowIndex)) {
next.delete(a.rowIndex)
} else {
next.add(a.rowIndex)
}
return next
})
lastCheckboxRowRef.current = a.rowIndex
return
Comment on lines +722 to +737
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shift+Space can add gap-row positions to checkedRows

a.rowIndex is the row coordinate from selectionAnchor, which is valid for any grid position — including gap positions that have no corresponding entry in positionMap. When the user navigates to a gap row and presses Shift+Space, that position is unconditionally added to checkedRows:

setCheckedRows((prev) => {
  const next = new Set(prev)
  if (next.has(a.rowIndex)) {
    next.delete(a.rowIndex)
  } else {
    next.add(a.rowIndex)   // gap position added here
  }
  return next
})

The downstream operations (copy, cut, delete) all gate on pMap.get(pos) and skip gaps, so no data corruption occurs. However, the gap row's checkbox will render as visually checked (because checkedRows.has(position) is true in PositionGapRows), and the gap position inflates checkedRows.size. This means isAllRowsSelected can under-count (checkedRows.size >= rows.length might be true while some real rows aren't checked because the set is padded with phantom positions).

Consider guarding with a positionMap lookup:

Suggested change
if (e.key === ' ' && e.shiftKey) {
const a = selectionAnchorRef.current
if (!a || editingCellRef.current) return
e.preventDefault()
setSelectionFocus(null)
setCheckedRows((prev) => {
const next = new Set(prev)
if (next.has(a.rowIndex)) {
next.delete(a.rowIndex)
} else {
next.add(a.rowIndex)
}
return next
})
lastCheckboxRowRef.current = a.rowIndex
return
if (e.key === ' ' && e.shiftKey) {
const a = selectionAnchorRef.current
if (!a || editingCellRef.current) return
e.preventDefault()
setSelectionFocus(null)
const pMap = positionMapRef.current
if (!pMap.has(a.rowIndex)) return
setCheckedRows((prev) => {

}
Comment thread
waleedlatif1 marked this conversation as resolved.

const anchor = selectionAnchorRef.current
if (!anchor || editingCellRef.current) return

Expand Down Expand Up @@ -773,23 +791,6 @@ export function Table({
return
}

if (e.key === ' ' && e.shiftKey) {
e.preventDefault()
setSelectionAnchor(null)
setSelectionFocus(null)
setCheckedRows((prev) => {
const next = new Set(prev)
if (next.has(anchor.rowIndex)) {
next.delete(anchor.rowIndex)
} else {
next.add(anchor.rowIndex)
}
return next
})
lastCheckboxRowRef.current = anchor.rowIndex
return
}

if (e.key === 'Tab') {
e.preventDefault()
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
Expand Down
Loading