Editor textarea: short content unless a screen reader is attached (fixes IME jitter in WebKit) - #333909
Conversation
`writeNativeTextAreaContent` already treats AccessibilitySupport.Unknown as "no screen reader": it skips render-time writes unless isScreenReaderOptimized() (microsoft#192278). `getScreenReaderContent` disagreed and handed Unknown the paged content, so a one-row textarea carried several rows of text and had to be scrolled to the caret's row. WebKit scrolls a focused textarea to keep its caret visible on every IME composition update, landing a few pixels short of the row-aligned offset `_render` writes; the next render writes it back, and the composed text jitters on every keystroke (microsoft/monaco-editor#4796). With the short content there is no overflow to scroll and nothing to fight over. Behaviour with a screen reader attached (Enabled) is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… sizing it The composition branch of `_render` wrote `scrollTop`/`scrollLeft` before `_doRender` gave the textarea its size for the row, while the non-composition branch a few lines down does it the other way round. A scroll offset is clamped against the element's size at the time it is written, so the order the sibling branch uses is the one that cannot lose part of the write. No behavioural difference was measured for the jitter in monaco-editor#4796 — with the short content there is nothing to scroll — this only makes the two branches agree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new condition bypasses required Android composition handling when accessibility support is unknown.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Aligns textarea content and wrapping with confirmed screen-reader support to prevent WebKit IME jitter.
Changes:
- Uses short textarea content unless accessibility support is enabled.
- Restricts textarea wrapping to screen-reader mode.
File summaries
| File | Description |
|---|---|
src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts |
Updates textarea content and wrapping behavior. |
Review details
Suppressed comments (1)
src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts:571
- This inline explanation exceeds the project's one-line limit for comments inside methods. It can be stated concisely without restating the preceding screen-reader wrapping rationale.
// Without one, the textarea only ever holds the short content `getScreenReaderContent` writes,
// and there is nothing to wrap.
- Files reviewed: 1/1 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| getScreenReaderContent: (): TextAreaState => { | ||
| if (this._accessibilitySupport === AccessibilitySupport.Disabled) { | ||
| // We know for a fact that a screen reader is not attached | ||
| if (this._accessibilitySupport !== AccessibilitySupport.Enabled) { |
| // No screen reader is known to be attached. `writeNativeTextAreaContent` already | ||
| // treats this case as "no screen reader" and skips render-time writes (#192278); | ||
| // handing it the paged content anyway put several rows of text into a one-row | ||
| // textarea, and during IME composition WebKit scrolls that textarea to keep its | ||
| // caret visible, fighting the row alignment `_render` maintains (monaco-editor#4796). |
|
@microsoft-github-policy-service agree |
Android + Unknown went through the short-content branch and returned EMPTY before reaching the Android word handling that composition diffing relies on. The Android cases now behave exactly as before; only the non-Android Unknown case takes the short content. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Addressed in 9b39c2c: the Android cases now behave exactly as before — |
…p back (#724) (#746) Monaco's hidden textarea becomes a one-row overlay on the composed line during IME composition, but it holds a page of text, so Monaco scrolls it to the caret's row. WebKit scrolls it back to "caret just visible" — 3px short of the row — on every composition update, and the next render scrolls it forward again. That is the jitter: the composed text sinks and snaps back on every keystroke, on every line but the first of the page. Monaco already treats `accessibilitySupport: 'auto'` in a browser as "no screen reader" when deciding whether to write into the textarea (vscode#192278), but not when deciding what to write; it hands that state a page. A build-time patch makes the two decisions agree, so the overlay holds one short line, has no overflow, and gives the browser nothing to scroll. Proposed upstream as microsoft/vscode#333909; the patch and its test go when a Monaco release carries it. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Fixes microsoft/monaco-editor#4796 — "line jump up and down when type chinese, only in safari".
What happens
In browsers without
EditContext(WebKit, Firefox) the editor takes input throughTextAreaEditContext. During IME composition the textarea becomes a one-row overlay on the composed line, but it holds a page of text, so_renderscrolls it to the caret's row:scrollTop = nlBefore * lineHeight.WebKit then scrolls that textarea itself, on every composition update, by the minimum needed to keep the caret visible — which is a few pixels short of row-aligned because the caret box is shorter than the line box. The next render writes the row-aligned value again. Instrumented on macOS (Monaco 0.55.1 in WKWebView,
lineHeight21, caret on line 2 of the page):102 self-scrolls over three composed lines, every one off by exactly 3px (line 3: 42 → 39). The first line of the page never jitters because its target offset is 0.
"editor.accessibilitySupport": "off"removes it entirely, because that path writes a short single-line string into the textarea and nothing needs scrolling. Full trace in the issue.The change
writeNativeTextAreaContentalready treatsAccessibilitySupport.Unknownas "no screen reader": it skips render-time writes unlessisScreenReaderOptimized()(#192278).getScreenReaderContentmade the opposite call for the same state and handed it the paged content. This aligns the two:Unknowngets the short content theDisabledbranch already produces, and the textarea is not sized for wrapping either. With no overflow there is nothing for the browser to scroll and nothing to fight over.Android is left exactly as it was: its
Unknowncase still reaches the Android word branch that composition diffing relies on (the first review caught that the initial version returnedEMPTYbefore it).Enabled— a screen reader known to be attached, or"editor.accessibilitySupport": "on", which is what the docs ask screen-reader users on the web to set — is untouched.Second commit: write order in the composition branch
The composition branch of
_renderwrotescrollTop/scrollLeftbefore_doRendersized the textarea, while the non-composition branch a few lines down writes them after. A scroll offset is clamped against the element's size at the moment it is written, so the sibling branch's order is the one that cannot lose part of the write. This was the first thing I tried against the jitter and it made no measurable difference on its own (the offsets were never clamped; they were moved afterwards — see the issue), so it is here as consistency, not as the fix.Things I tried first, and why they are not the fix
_doRender(the sibling non-composition branch does it in that order): no effect. The write is never clamped; the value is moved afterwards.scrollevent: removes the jitter, but keeps the alignment depending on a value the browser owns._doRendercouplesheightandline-height, and the change is much larger than this one.Verification
Built Monaco with this exact change into the app that reproduces the issue (macOS 26, WKWebView, system Chinese IME): composition on any line no longer jitters, ASCII typing, select-all, copy, paste, undo and cross-line cursor movement behave as before.
src/vs/editor/test/browser/controller/textAreaInput.test.tsmocksgetScreenReaderContent, so no existing assertion covers this decision.Not verified: Firefox (the other browser on this code path) and screen-reader behaviour with
Unknown— by design that case now behaves likeDisabled, which is the everyday desktop path without a screen reader.🤖 Generated with Claude Code