Repository navigation
Description
Steps to reproduce
Steps to reproduce
- Run the sample below on web (
flutter run -d chrome) or paste into dartpad.dev. It drives the whole sequence on timers, so no interaction is needed — focus moves programmatically to avoid clicks closing the input connection. - Wait ~4 seconds.
- Click the first field and type, observe it is dead
- Click the second field, observe it still works
- Click back to first field, observe it is still dead
Diagnosis
Disclaimer the below was generated with Claude, but verified by hand
Line references are Flutter 3.44.5. Engine paths are engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart.
Setup. For a field with autofill, initializeTextEditing deliberately does not insert the editing element directly (:1402); insertion happens via placeForm() → EngineAutofillForm.wakeUp() (:1573, :301), which also sets _appendedToForm = true. On teardown, disable() leaves the element inside the form and registers the form in the global dormantForms map (:348, :2516) so the browser can still offer autofill. A later session adopts that dormant form and swaps its new element in via oldFocusedElement.replaceWith(focusedElement).
Corruption. TextInputUpdateConfig (:2199) rebuilds the whole InputConfiguration from the message, which constructs a fresh EngineAutofillForm whose formElement is null — that field is only ever assigned inside wakeUp(). applyConfiguration() (:1418) installs it on the strategy, discarding the reference to the form the element is actually inside.
disable() then decides where the element belongs by consulting the current configuration (:1550):
// If focused element is a part of a form, it needs to stay on the DOM
// until the autofill context of the form is finalized.
// More details on `TextInput.finishAutofillContext` call.
if (_appendedToForm && inputConfiguration.autofillGroup?.formElement != null) {
_styleAutofillElements(activeDomElement, isOffScreen: true);
inputConfiguration.autofillGroup?.goDormant();
EnginePlatformDispatcher.instance.viewManager.safeBlur(activeDomElement);
} else {
EnginePlatformDispatcher.instance.viewManager.safeRemove(activeDomElement);
}_appendedToForm is still true, but formElement is now null, so the element is removed from a form that remains in the DOM and remains registered in dormantForms — and goDormant() never runs, so the stale entry from the previous session survives, with its elements[uniqueIdentifier] pointing at the now-detached element.
Perpetuation. Every later session adopts that stale form (:301):
if (firstWakeUp) {
if (existingForm != null) {
formElement = existingForm.formElement; // adopted, not re-inserted
elements.addAll(existingForm.elements);
} else { ...create and insert... }
}
if (!formElement!.contains(focusedElement)) {
final DomElement oldFocusedElement = elements[focusedAutofill.uniqueIdentifier]!;
elements[focusedAutofill.uniqueIdentifier] = focusedElement;
oldFocusedElement.replaceWith(focusedElement);
}contains() == false is the normal post-adoption case and the swap is the intended way in — but oldFocusedElement now has no parent, and ChildNode.replaceWith on a parentless node is a silent no-op per spec. The new element never enters the document, and no error is raised.
Why it never recovers. _startEditing() sets isEditing = true before strategy.enable() (:2577), and TextInputShow is gated on if (!textEditing.isEditing) (:2221). Meanwhile the framework holds a live TextInputConnection, so refocusing sends only TextInput.show, which is ignored. Other fields are unaffected because their formIdentifier differs, and their setClient forces stopEditing() first.
Why only a rebuild recovers it. formIdentifier is the sorted join of the fields' uniqueIdentifiers, which for a single-field form is the EditableTextState's autofillId — 'EditableText-$hashCode' (packages/flutter/lib/src/widgets/editable_text.dart:5242). New TextInputConnections reuse it; only a new EditableTextState produces a key that misses dormantForms and causes a fresh form to be created and inserted.
Paths that send updateConfig on a live connection (web): a readOnly change (editable_text.dart:3457) — reachable through the very ordinary TextField(enabled: ...), since enabled: false maps to readOnly: true (text_field.dart:1689) — plus obscureText and keyboardType changes (editable_text.dart:3464) and a viewId change (:3356). A password-visibility toggle on a focused field is the most common instance.
Expected results
Field A accepts input again, exactly as it did in step 2.
Actual results
Field A shows its focus highlight and a blinking caret but accepts no input, permanently. Clicking it, tabbing to it, or focusing other fields and returning never recovers it. Field B continues to work normally. Nothing is logged — no exception on window.onerror or console.error.
The engine has no editing element in the document:
document.querySelectorAll('input.flt-text-editing').length // 0 for field Aand field A's autofill <form> is left in the DOM containing only its submit button:
[...document.querySelectorAll('form')].forEach((f, i) =>
console.log(i, [...f.children].map(c => c.tagName + '.' + c.className).join(' | ')));
// 0 INPUT.submitBtn ← field A, field removed
// 1 INPUT.flt-text-editing | INPUT.submitBtn ← field B, healthyKeystrokes land on <flutter-view> with defaultPrevented: false, and no beforeinput/input events fire, because there is no editable element to receive them.
The field only recovers if its EditableTextState is destroyed and recreated (which changes autofillId, and hence the form key) — for example by a layout change that rebuilds the subtree. A page reload also clears it.
Code sample
Code sample
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) =>
const MaterialApp(home: Scaffold(body: Repro()));
}
class Repro extends StatefulWidget {
const Repro({super.key});
@override
State<Repro> createState() => _ReproState();
}
class _ReproState extends State<Repro> {
final a = FocusNode(debugLabel: 'A');
final b = FocusNode(debugLabel: 'B');
bool obscure = false;
@override
void initState() {
super.initState();
// Session 1 (clean): A's autofill form is created, then goes dormant.
Timer(const Duration(milliseconds: 500), a.requestFocus);
Timer(const Duration(milliseconds: 1000), b.requestFocus);
// Session 2: adopts the dormant form, then updateConfig replaces the
// engine's EngineAutofillForm while the connection is live.
Timer(const Duration(milliseconds: 1500), a.requestFocus);
Timer(const Duration(milliseconds: 2000), () => setState(() => obscure = true));
Timer(const Duration(milliseconds: 2500), b.requestFocus);
// Session 3: A now has no editing element and accepts no input.
Timer(const Duration(milliseconds: 3000), a.requestFocus);
}
@override
Widget build(BuildContext context) => Column(
children: [
TextField(focusNode: a, obscureText: obscure),
TextField(focusNode: b),
],
);
}
Note that no autofill is configured explicitly. TextField.autofillHints defaults to const <String>[] (text_field.dart:314), which is non-null and therefore enables autofill, so every Material TextField on web is on the affected code path by default.
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[Paste your logs here]Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.44.5, on Arch Linux 7.2.3-arch1-3, locale en_GB.UTF-8) [33ms]
• Flutter version 3.44.5 on channel stable at /home/raphael/.flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision f94f4fc76b (9 weeks ago), 2026-07-06 11:19:24 -0700
• Engine revision 83675ed276
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [1,448ms]
• Android SDK at /home/raphael/Android/Sdk
• Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
• Platform android-36, build-tools 36.1.0
• ANDROID_HOME = /home/raphael/Android/Sdk
• Java binary at: /usr/lib/jvm/java-17-openjdk/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 17.0.20.1+1)
• All Android licenses accepted.
[✓] Chrome - develop for the web [6ms]
• CHROME_EXECUTABLE = /usr/bin/chromium
[✓] Linux toolchain - develop for Linux desktop [137ms]
• clang version 22.1.8
• cmake version 4.4.3
• ninja version 1.13.2
• pkg-config version 3.0.7
! Unable to access driver information using 'eglinfo'.
It is likely available from your distribution (e.g.: apt install mesa-utils)
[✓] Connected device (2 available) [158ms]
• Linux (desktop) • linux • linux-x64 • Arch Linux 7.2.3-arch1-3
• Chrome (web) • chrome • web-javascript • Chromium 152.0.7977.82 Arch Linux
[✓] Network resources [170ms]
• All expected network resources are available.
• No issues found!
Activity
mbcorona commentedon Sep 10, 2026
Thanks for the detailed report @tustvold .
Routing to the text input team for further investigation.
gollintim commentedon Oct 3, 2026
Same root cause, second symptom: a fatal
Null check operator used on a null valueinstead of a dead field.We hit this in production on Flutter 3.44.0 stable (dart2js, Chrome 152/154 desktop) on two sign-in screens. Our case is: a focused autofill field gets
TextInput.updateConfig(here, togglingobscureTextfor a "show password" button), and then any geometry update arrives while it is still focused, such as a scroll or a window resize. That throws:Minimal repro
flutter run -d chrome --release, desktop Chrome.Result: the error above, every time, and the field loses DOM focus (
document.activeElementis<body>). A second<form>is now in the DOM. Steps 1–2 followed by step 4 without step 3 never throw.What we believe happens (3.44.0 sources)
updateConfig→TextInputUpdateConfig→applyConfiguration()installs a freshEngineAutofillFormwithformElement == null(the same starting point as this issue).setEditableSizeAndTransform→placeElement()→placeForm()→wakeUp()seesfirstWakeUp. With no dormant entry it calls_createFormElementAndFields(focusedElement, …), which moves the currently focused<input>into a new<form>.blurwithrelatedTarget == null→handleBlur→sendTextConnectionClosedToFrameworkIfAny()→disable()→domElement = null.placeElement(),focusedFormElement?.focusWithoutScroll()is guarded ([web] Use defensive null check in text editing placeElement #180795), but the very next line,moveFocusToActiveDomElement(), is not. It readsdomElement!and throws.The defensive comment above those two lines describes exactly this hazard; it covers the first call and not the second. Guarding
moveFocusToActiveDomElement()would stop the throw, but the underlying problem is the same as this issue: afterupdateConfigthe autofill form object no longer matches the DOM.Only
GloballyPositionedTextEditingStrategy(Chrome/Edge, Firefox) andSafariDesktopTextEditingStrategyreach it. The iOS and Android strategies overrideplaceElement()withoutplaceForm().App-side workaround (what we shipped): in a desktop browser, change the config with the field detached. Unfocus, apply the change,
requestFocus()again in a post-frame callback, then restore the selection, because a programmatic focus selects all text on web.flutter-zl commentedon Oct 8, 2026
this is fixed by #193269.
github-actions commentedon Oct 22, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.