Phenomenon
In Chrome 121 on Windows, a regression was identified where the input event (and consequently React’s onInput) does not trigger after a beforeinput event if the insertion happens at the absolute start of a text node or block (offset 0). This is particularly disruptive for high-level editor frameworks that rely on the input event to reconcile their internal model with the DOM.
Reproduction Steps
- Create a
contenteditablecontainer with some text (e.g., “world”). - Programmatically or manually place the caret at
offset 0(before “w”). - Type a single character (e.g., “H”).
- Inspect the event log.
Observed Behavior
keydownevent: Fires normally.beforeinputevent: Fires withinputType: "insertText"anddata: "H".- DOM Change: The character “H” is correctly inserted into the DOM by the browser’s default behavior.
inputevent: MISSING.- Result: Frameworks like Slate or React do not detect the change, leading to a state-DOM mismatch.
Expected Behavior
The browser should dispatch an input event immediately after the DOM has been modified by the insertText operation, regardless of the caret’s offset.
Impact
- Data Loss: Since
onInputdoesn’t fire, the application state is not updated with the new character. If the user continues typing, the whole block might eventually be overwritten or corrupted. - Undo/Redo Breakdown: The undo stack may skip this specific character insertion, making the history inconsistent.
- Framework Failures: Core logic in Slate.js and other virtual-model-based editors fails to trigger, stopping all side effects (like syntax highlighting or auto-save).
Browser Comparison
- Chrome 120 (and below): Works correctly.
- Chrome 122+: Fixed.
- Firefox/Safari: Works correctly; does not exhibit this regression.
References & Solutions
Mitigation: beforeinput Fallback
If you must support Chrome 121, you can use beforeinput to manually trigger a reconciliation if the input event doesn’t follow.
let inputExpected = false;
element.addEventListener('beforeinput', (e) => {
if (e.inputType === 'insertText' && getSelectionOffset() === 0) {
inputExpected = true;
// Set a short timeout to check if 'input' actually fired
setTimeout(() => {
if (inputExpected) {
console.warn('Detected missing input event in Chrome 121. Forcing sync.');
forceSyncModelWithDOM();
inputExpected = false;
}
}, 10);
}
});
element.addEventListener('input', (e) => {
inputExpected = false;
});