The edit/commit state machine shared by every value-controlled field — the part of a field that
has nothing to do with what it edits: the two baselines, the dirty flag, the live vs commit
channels, and the done-key policy.
The rules it owns, once, for every field:
Two baselines.committedText is the last ACCEPTED value; baselineText is the text when the
current session started. They differ because the live-change model applies every intermediate
change — "the value before" can then only mean before-the-session, not before-the-last-change.
Escape is two-level. Session open + TOUCHED → cancel (revert to the baseline, onDone with
reason: 'cancel'). Idle + dirty → a plain revert to the committed text, no commit channel.
An UNTOUCHED session → the key is NOT captured, so a host dialog still closes on it. "Touched",
not "changed": an edit that lands back on the starting text is still one to Escape out of.
Enter belongs to whoever owns the commit.changeOn: 'done' captures it (the field is the
commit point); 'live' lets it bubble to the host's default button, finishing any open session
first.
Tab commits then advances — un-captured, so the natural tab order still runs. Invalid text
CANCELS instead of committing, so focus never advances carrying an error.
Invalid never commits on any accept path; blur commits only when dirty; and a done key
plus the blur it causes are ONE gesture, delivered once (the one-shot guard).
The input owns its text keys. Delete / caret motion / the native chords are consumed
(EditCommitApi.handleTextKeys) so they never reach the app's command dispatch.
Hosts keep their own concerns — a formula field's reference bus and grammar, a plain field's
validation — and wire them in through UseEditCommitOptions.parse and
EditCommitSession.
The edit/commit state machine shared by every value-controlled field — the part of a field that has nothing to do with what it edits: the two baselines, the dirty flag, the live vs commit channels, and the done-key policy.
The rules it owns, once, for every field:
committedTextis the last ACCEPTED value;baselineTextis the text when the current session started. They differ because the live-change model applies every intermediate change — "the value before" can then only mean before-the-session, not before-the-last-change.onDonewithreason: 'cancel'). Idle + dirty → a plain revert to the committed text, no commit channel. An UNTOUCHED session → the key is NOT captured, so a host dialog still closes on it. "Touched", not "changed": an edit that lands back on the starting text is still one to Escape out of.changeOn: 'done'captures it (the field is the commit point);'live'lets it bubble to the host's default button, finishing any open session first.Hosts keep their own concerns — a formula field's reference bus and grammar, a plain field's validation — and wire them in through UseEditCommitOptions.parse and EditCommitSession.