Stop typing on a selected decision card from landing at the document start #387
- MA
## Why
`e2e/decision-marker.e2e.ts` "typing with a visible decision card selected never lands at the document start" has been failing on and off (`zThe rollout…`). The flake comes from a real bug that users can hit: typed text can land at the top of the document.
Root cause, confirmed by tracing Lexical selection, DOM selection and Lexical's `_inputState` on every keydown, beforeinput and selectionchange:
1. Lexical's `pointerdown` handler sets `isSelectionChangeFromMouseDown = true`. Only the next `selectionchange` clears it. 2. ArrowDown onto a visible card gives a `NodeSelection`. Lexical removes every DOM range, and Chrome parks its caret at the start of the root. 3. That caret move fires a `selectionchange`. If the flag is still set, Lexical treats it as a click back onto text and builds a `RangeSelection` from the DOM caret: document start. 4. From then on `BEFORE_INPUT_COMMAND` no longer sees a node selection, so the characters go in at the top. This gives `xyz…`, `yz…` or `z…`, depending on when the selectionchange arrives.
The flag goes stale whenever no `selectionchange` follows the click: - **Users:** clicking where the caret already is fires no `selectionchange`. Click the end of a paragraph twice, wait, press ArrowDown onto a card, then type: before this fix, about 1 in 3 human-paced runs put the text at the document start. - **CI:** Playwright presses ArrowDown in the same frame as the click, ahead of the click's queued `selectionchange`.
## What changed
- `DecoratorSelectionPlugin` registers a `KEY_DOWN_COMMAND` handler at critical priority. It clears `isSelectionChangeFromMouseDown`, because once a key is pressed the next selection change belongs to the keyboard, not the earlier pointer press. It returns `false`, so nothing else changes. - Unit test: a key press clears the flag. - New E2E stress test: the human-paced repeat-click path, run 5 times in one test with frame settles between steps.
## Selection flow

## Testing
Run on my own server (port 8896, own database), using the Playwright suite with a local config:
| Run | Before fix | After fix | | --- | --- | --- | | existing test, `--repeat-each 40`, 4 workers | **25/40 failed** | — | | existing test, `--repeat-each 10` / `30` | **8/10 failed** | **0/30 failed** | | new repeat-click test, `--repeat-each 10` / `30` | **10/10 failed** | **0/30 failed** | | instrumented same-frame script, 12 / 15 runs | 4/12 failed | 0/15 failed | | instrumented human-paced script, 12 / 15 runs | 4/12 failed | 0/15 failed | | same-frame script with `Emulation.setCPUThrottlingRate 4`, 20 runs | — | 0/20 failed |
- `bun test packages/editor`: 504 pass - `bun run ci`, `bun run types`: pass - Local `bun run e2e` is suspended for this audit (shared ports are occupied), so CI runs the full browser suite.
Note: `_inputState` is a typed but underscore-prefixed Lexical field. The comment in `decorator-selection.tsx` explains why we touch it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
MaggieAppletonhead-ref-force-pushed 37e04cb67e3dlast month on GitHub
MaggieAppletonmerged 6caaa51886dblast month on GitHub
MaggieAppletonhead-ref-deletedlast month on GitHub