Files
NetMesh/domain/imeControlledInput.ts

73 lines
2.8 KiB
TypeScript
Raw Permalink Normal View History

/**
* Pure helpers for controlled text inputs that must not fight CJK IME composition.
*
* Deferred parent updates (e.g. startTransition) against `value={external}` reset
* the DOM mid-composition and break Windows IMEs (candidate dismiss / pinyin echo).
*/
export function shouldCommitImeControlledChange(input: {
isComposingSession: boolean;
nativeEventIsComposing?: boolean;
/**
* When true, the open (or just-ended) composition was discarded by an external
* value change (e.g. navigation cleared the filter). Suppress commits so a
* post-compositionend `onChange` cannot reassert the stale composed draft.
*/
compositionExternallySuperseded?: boolean;
}): boolean {
if (input.compositionExternallySuperseded) return false;
return !input.isComposingSession && input.nativeEventIsComposing !== true;
}
export function shouldAdoptExternalImeControlledValue(input: {
isComposingSession: boolean;
draftValue: string;
externalValue: string;
/**
* Committed external value captured at compositionstart. When provided during
* an open composition, an external change relative to this baseline (e.g.
* different-directory navigation setting filter to "") supersedes the draft
* so compositionend cannot re-commit stale text over the navigation clear.
*/
valueAtComposeStart?: string;
}): boolean {
if (input.draftValue === input.externalValue) return false;
if (!input.isComposingSession) return true;
// Mid-composition: only adopt when parent moved away from the compose-start
// baseline. Without a baseline, never fight the IME for self-driven drafts.
if (input.valueAtComposeStart !== undefined) {
return input.externalValue !== input.valueAtComposeStart;
}
return false;
}
/**
* Resolve an input event when a composition may have been externally superseded
* (navigation clear mid-IME). Browsers often re-fire the composed text via
* `onChange` with `isComposing=false` immediately after `compositionend`; that
* follow-up must not reassert the stale draft.
*
* - While still composing after supersede: keep ignoring event values (draft stays
* on the external value) and keep the latch armed.
* - Once composition has fully ended: ignore that one post-composition change and
* clear the latch so subsequent ordinary typing commits normally.
*/
export function resolveSupersededImeInputEvent(input: {
compositionExternallySuperseded: boolean;
isComposingSession: boolean;
nativeEventIsComposing?: boolean;
}): {
ignoreEventValue: boolean;
clearSupersedeLatch: boolean;
} {
if (!input.compositionExternallySuperseded) {
return { ignoreEventValue: false, clearSupersedeLatch: false };
}
const compositionFullyEnded =
!input.isComposingSession && input.nativeEventIsComposing !== true;
return {
ignoreEventValue: true,
clearSupersedeLatch: compositionFullyEnded,
};
}