Skip to content

Extend selection across paragraphs with Shift+ArrowUp/Down - #21

Open
rogegoncalves wants to merge 1 commit into
extend-hq:mainfrom
rogegoncalves:fix/shift-arrow-cross-paragraph
Open

rogegoncalves wants to merge 1 commit into
extend-hq:mainfrom
rogegoncalves:fix/shift-arrow-cross-paragraph

Conversation

@rogegoncalves

Copy link
Copy Markdown

Summary

Shift+ArrowUp/Down couldn't extend a selection across paragraphs (the keyboard half of #20):

  • In regular contenteditable paragraphs it stopped at the paragraph boundary.
  • From a paragraph on the wrapped route, the first Shift+ArrowDown reached the next paragraph, but a second one dropped the selection, and Backspace then did nothing.

Root cause

  1. The contenteditable paragraph onKeyDown only handles vertical arrows without Shift (isPlainVerticalArrowKey). With Shift the browser handles the key and keeps the selection inside the paragraph's own editing host.
  2. The wrapped textarea handler takes the first step itself: it focuses the viewer root and calls setSelectionFromDocxBoundaries(anchor, focus). The next Shift+Arrow reaches the viewer root, and onViewerKeyDown doesn't handle arrows. The native extension moves the DOM selection's anchor out of the paragraphs, so on keyup setActiveRangeFromSelection can't resolve the range and clears activeTextRange.

Fix

onViewerKeyDown handles Shift+ArrowUp/Down with the helpers the Shift+click support uses (resolveParagraphBoundaryFromSelectionPoint, resolveBoundaryFromPoint, setSelectionFromDocxBoundaries). Anchor and focus come from the DOM selection; setSelectionFromDocxBoundaries sets it with setBaseAndExtent, so the direction is kept. The focus boundary moves one line. On the edge line of its paragraph, it moves into the next or previous paragraph, found in the model with nextParagraphLocation / lastParagraphLocationInNode as the wrapped route does, at the caret's x on that paragraph's first or last line. That paragraph is scrolled into view first, so page breaks and paragraph spacing don't stop the move.

  • Inside a single paragraph nothing changes: the browser extends natively, and the textarea handler still owns wrapped paragraphs.
  • Once the selection spans paragraphs, the key is always handled, even when the boundary can't move (end of document, or a table next), so the range is never dropped.
  • Tables are left out. An earlier version that let the selection extend from a paragraph into a table made Backspace pull the first cell's text into the paragraph, so this stops before tables and leaves them to the cell selection.

Known limitation: the column comes from the current caret, so after passing a short line the following lines use the shorter column. Word remembers the original column.

Refs #20.

Verification

  • New browser test in tests/visual/fidelity-smoke.spec.ts. It presses Shift+ArrowDown twice, Shift+ArrowUp once and Backspace across regular paragraphs, then Shift+ArrowDown twice from a heading on the wrapped route, checking activeTextRange after each key. It fails without the fix (the second Shift+ArrowDown doesn't extend) and passed 10 of 10 runs with it.
  • pnpm run test:visual: 12 passed. pnpm run typecheck: clean. vitest run: 748 passed, 15 skipped, the same as on main.
  • Also checked in our app (a Next.js editor on 0.11.0, Chromium, with our own selection workaround removed), pressing Backspace after each gesture to see the exact range: Shift+ArrowDown/Up ×4 across single-line paragraphs, through a 5-line paragraph, up to a table (40 presses), extending a Shift+click selection with Shift+ArrowDown, and inside a single paragraph (unchanged). Shift+click and drag behave the same with and without the patch.

🤖 Generated with Claude Code

Each paragraph is its own editing host, so the native Shift+Arrow
extension stopped at the paragraph edge. From a wrapped paragraph the
textarea took the first step, but the next key reached the viewer root,
where the native extension moved the DOM selection out of the paragraphs
and the active range was cleared.

onViewerKeyDown now moves the focus boundary one line: inside the
paragraph, or into the next/previous paragraph (from the model) on the
edge line, and applies it with setSelectionFromDocxBoundaries. Moves
inside a single paragraph stay native, tables keep their cell selection,
and once the selection spans paragraphs the key is always handled so the
range is never dropped.

Refs extend-hq#20

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

vercel Bot commented Oct 18, 2026

Copy link
Copy Markdown

@rogegoncalves is attempting to deploy a commit to the Extend Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
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