Phenomenon
Firefox for Android (Fenix) has community reports that when a contenteditable element is nested inside another contenteditable, beforeinput’s getTargetRanges() sometimes returns a StaticRange anchored in the outer host instead of the inner focused editor. The failure is subtle because the keyboard still drives edits in the inner surface from the user’s perspective, but JavaScript that trusts targetRanges[0] for preventDefault() + manual DOM updates may run against the outer tree—merging lines, deleting outer paragraphs, or inserting adjacent to outer nodes.
Reproduction Steps
- Build HTML: outer
div contenteditable="true"containing static text and an innerdiv contenteditable="true". - Open the page in Firefox for Android.
- Tap inside the inner editor and place the caret at the end of “Inner”.
- Attach a
beforeinputlogger ondocumentor the outer element that printsgetTargetRanges()[0]’sstartContainerchain and compares todocument.activeElement. - Type a character or press Backspace.
- On affected versions, compare logs:
activeElementmay be the inner host whilestartContainer’sclosest('[contenteditable]')resolves to the outer host.
(Exact builds vary; treat as a regression-sensitive area and re-test after Fenix / Gecko updates.)
Observed Behavior
- Non-empty but wrong scope: Unlike empty
targetRanges, the array has entries—so code paths that only fall back whenlength === 0never run. - Custom editors break: ProseMirror, Lexical, or hand-rolled
beforeinputfilters that unwrapStaticRangeintoRangeand replace contents corrupt the outer document. - Selection vs targetRanges mismatch:
window.getSelection()may still point inside the inner host whiletargetRangesdoes not; naive “prefer targetRanges” policies fail.
Expected Behavior
Per Input Events intent, getTargetRanges() for an edit operation scoped to the inner host should use boundary nodes inside that host (or its descendants), consistent with the focused contenteditable that will receive default actions.
Impact
- Data loss in outer regions: Unintended deletion of outer paragraphs when the user meant to edit the inner caption.
- Editor SDK bugs: Frameworks assume
targetRangesis authoritative when non-empty; mobile Firefox violates that assumption for nesting.
Browser Comparison
- Firefox Android: Reports of wrong-scope
targetRangesfor nesting; verify on current release. - Chrome Android / Safari iOS: Test separately; different engines, different nesting bugs.
- Desktop Firefox: Often behaves; do not extrapolate from desktop passes.
Solutions
- Containment check:
function rangeInsideHost(range, host) {
return host && range && host.contains(range.startContainer) && host.contains(range.endContainer);
}
editorRoot.addEventListener('beforeinput', (e) => {
const host = document.activeElement?.closest?.('[contenteditable="true"]');
const tr = e.getTargetRanges?.() ?? [];
if (tr.length && host && !rangeInsideHost(tr[0], host)) {
e.preventDefault(); // optional: block bad default
// Fall back to Selection-based logic scoped to `host` only
}
});
-
Refuse nested contenteditable in product if Fenix must be supported without complex guards.
-
Track Fenix #27569 and Gecko tickets for resolution status.