[lexical] Bug Fix: keep caret above the on-screen keyboard after Enter#8486
Merged
etrepum merged 3 commits intoMay 10, 2026
Merged
Conversation
scrollIntoViewIfNeeded compared the selection rect against window.innerHeight (the layout viewport). On mobile the on-screen keyboard does not change innerHeight, only the visual viewport, so the new caret created by Enter often lands behind the keyboard. The browser's native auto-scroll only kicks in once the user types a character or two; in a list this leads users to press Enter again, which exits the list. Read window.visualViewport.offsetTop / .height when available so the caret lands in the keyboard-clear area; fall back to innerHeight when visualViewport is unavailable. getBoundingClientRect already reports in layout-viewport coords, so offsetTop + height is the visible bottom in those same coords and the surrounding scroll math is unchanged. Adds an e2e test that stubs window.visualViewport via a real EventTarget (dispatching a resize event to mirror the real keyboard-appears sequence), fills a long bullet list past overflow, presses Enter, and asserts the new caret stays within visualViewport.offsetTop + visualViewport.height. The test fails before the fix (caret around innerHeight) and passes after (caret within the visual viewport). The existing Auto scroll tests are unaffected: they exercise the in-editor overflow branch which is untouched.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
previously approved these changes
May 9, 2026
etrepum
reviewed
May 9, 2026
etrepum
left a comment
Collaborator
There was a problem hiding this comment.
It looks like this new e2e test does not work consistently
…against collab + sub-pixel rounding Two flake sources surfaced in CI on the previous commit: - Collab mode: clicking the .block-controls dropdown timed out because the dropdown sat outside the 400-px-wide viewport in the split collab layout. Switch from the local UI-click toggleBulletList to the shared keyboard-shortcut version (Ctrl/Cmd+Shift+8), which doesn't depend on toolbar position or viewport size. - Firefox layout rounding: the assertion compared a fractional caret bottom (e.g. 300.27) against an integer visualViewport bound (300), failing by 0.27 px even though Lexical scrolled correctly. Allow a 1-px tolerance — the original bug overshoots by ~280 px so this still catches the regression. Verified locally on chromium / firefox / webkit, both rich-text and rich-text-with-collab modes (10x / 5x / 3x repeats, 0 flakes).
Contributor
Author
|
need to re-run the affected jobs. |
etrepum
approved these changes
May 10, 2026
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
On mobile, after pressing Enter (in a list, paragraph, heading, table cell ...), the new caret often lands behind the on-screen keyboard. The browser's own auto-scroll only kicks in once the user starts typing, typically on the second character.
In a list this is particularly painful: users press Enter again, thinking nothing happened, which exits the list and destroys the item they were trying to add.
Before / After
Before.mp4
after.mp4
Root cause
scrollIntoViewIfNeeded(LexicalUtils.ts) compares the selection rect againstwindow.innerHeightin the body-scroll branch. On a desktop, layout viewport is more or less = visual viewport, so this is correct.On mobile with the keyboard open, the layout viewport stays full-screen while the visual viewport shrinks to the area above the keyboard. The function therefore concludes the caret is already in view (
currentBottom <= innerHeight) and does not scroll, even though visually the caret is behind the keyboard.When the user starts typing, the browser's native auto-scroll-into-visual-viewport heuristic eventually catches up, that's why "the second character fixes it."
Fix
In the body-scroll branch, read
window.visualViewportwhen available; fall back toinnerHeightfor environments without it.getBoundingClientRectalready reports in layout-viewport coordinates, andvisualViewport.offsetTop + heightis the visible-area bottom in those same coordinates, so the surrounding math (currentBottom > targetBottom->scrollBy(0, diff)) stays consistent.The CSS
scroll-paddingaccounting from #8218 is unchanged.The non-body branch (in-editor overflow) is untouched.
Scope
The bug affects every Enter/return key that creates a new block: paragraphs, list items (
<li>), headings, table cells, custom decorator nodes, and more generally any post-update selection change that hits the body-scroll branch.Lists make it particularly user-visible because the natural workaround (press Enter again) destroys data. Fixing the underlying scroll math fixes all of them at once.
Test Plan
Auto scroll respects mobile visual viewport > Pressing Enter scrolls new caret above the on-screen keyboard(inpackages/lexical-playground/__tests__/e2e/AutoScroll.spec.mjs):window.visualViewportvia a realEventTargetthat reportsheight: 300and dispatches theresizeevent to mirror the real "keyboard appears" sequence.caretRect.bottom <= visualViewport.offsetTop + visualViewport.height(so iOS-style non-zerooffsetTopis also covered).caretRect.bottom ~ 600(sat atinnerHeight, behind the simulated keyboard);caretRect.bottom ~ 280.addScroll(...)) hit the unchangedelsebranch.Known limitations of the test setup
scrollIntoViewIfNeededdoesn't depend on that timing, but a future iOS-specific test might want to.offsetTop > 0cases (visual viewport offset when the URL bar collapses) are exercised by the assertion math but not by the stub values