Phenomenon
A layout engine regression in Blink (reported April 2024) specifically affects RTL (Right-to-Left) languages within scrolling contenteditable containers. When text overflows the horizontal bounds, the browser fails to correctly calculate the scrollLeft offset to keep the caret in view. Furthermore, the “Visual to Logical” mapping breaks, causing the blinking bar to appear at incorrect pixel coordinates relative to the characters.
Reproduction Steps
- Create a
<div>withcontenteditable="true",dir="rtl", andoverflow: auto; width: 200px;. - Input a long string of RTL characters (e.g., Hebrew or Arabic) until it wraps or overflows horizontally.
- Observe the behavior of the caret as it reaches the left boundary (the end of the flow for RTL).
- Try to click in the middle of the text to reposition the caret.
Observed Behavior
- Scrolling Failure: The container does not automatically scroll to keep the caret visible as it moves leftward.
- Caret Misalignment: In some cases, the caret appears several pixels away from the character it belongs to, or disappears entirely if it moves into the “negative” scroll area incorrectly.
- Pasting Error: Pasting RTL text into an existing RTL block often inserts the content at the wrong logical index.
Expected Behavior
The browser should calculate caret coordinates based on the dir attribute and the computed BiDi (Bidirectional) layout, ensuring scrollIntoView() logic works correctly for the “left” edge (which is the trailing edge in RTL).
Impact
- Unusable RTL Editors: Users cannot see what they are typing in narrow containers (like sidebars or comment boxes).
- Selection Corruption: Dragging to select RTL text results in “jagged” or inverted selections that do not match the mouse movement.
Browser Comparison
- Chrome 124+: Significant scrolling and caret placement regressions reported.
- Safari: Handles RTL scrolling correctly; better BiDi layout consistency.
- Firefox: Most stable for RTL; correctly maps visual offsets to logical indices.
References & Solutions
Mitigation: scrollIntoView Polyfill
Manually trigger scrolling based on the selection coordinates if the browser fails to do so.
element.addEventListener('input', () => {
const selection = window.getSelection();
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
const containerRect = element.getBoundingClientRect();
if (rect.left < containerRect.left) {
// Force scroll for RTL end edge
element.scrollLeft += (rect.left - containerRect.left) - 10;
}
});