docs/solutions/ui-bugs/2026-05-03-slate-react-native-input-and-shell-backed-selection-need-single-owners.md
The rendering-strategy runtime had two authority leaks: the native DOM input
listener and React onInput could both repair the same browser text insertion,
and a later selectionchange could import an empty browser selection over a
model-backed shell select-all.
rendering-strategy-runtime expected one typed a, but the model became
aa after clicking at the end of DOM-present text and typing.ControlOrMeta+A set the root attribute to shell-backed, but
the model selection collapsed to [0, 0] before paste/copy logic ran.input produced two insert_text operations.selectionchange and imported the
browser's collapsed selection.selectionchange would be too blunt; toolbar, paste, and
browser-drag rows need real DOM selection import.Mark DOM input events handled by the direct native listener and let React
onInput skip only its text-repair fallback for that same event:
const handledDOMInputEventsRef = useRef<WeakSet<Event>>(new WeakSet())
const markHandledDOMInput = useCallback((event: Event) => {
handledDOMInputEventsRef.current.add(event)
}, [])
const skipNativeTextInputRepair = handledDOMInputEventsRef.current.has(
event.nativeEvent
)
Then keep the rest of React onInput behavior intact: app onInput, deferred
operations, and history events still run. Only the duplicate native text repair
is skipped.
For shell-backed select-all, move selection ownership to the model explicitly:
setEditableModelSelectionPreference({
inputController,
preferModelSelection: true,
selectionSource: 'shell-backed',
})
Finally, teach selectionchange import to respect that owner:
if (
state.selectionSource === 'shell-backed' &&
isEditableModelSelectionPreferred(inputController)
) {
return
}
The native DOM listener is the low-level owner for browser text repair once it
has accepted an input event. React still observes the event, but it must not
replay the same insertion into the model.
Shell-backed select-all is not a DOM selection waiting to be imported. It is a
model-backed selection over content that may not have complete mounted DOM.
Letting a follow-up browser selectionchange overwrite it with a collapsed
range destroys copy/paste before clipboard code can apply the shell policy.
The fix keeps the ownership split narrow:
onInput still runsselectionSource; a visual flag alone is not authority.slate-react and slate-browser before Playwright rows that import
built packages from dist.