Revert "Revert "[web] Don't overwrite editing state with semantic updates (#38271)" (#38562)"

This reverts commit 5713a216076f91e4f6891d1c1a75a2947f2026ce.
diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart
index 3e36ffb..289248e 100644
--- a/lib/web_ui/lib/src/engine/semantics/text_field.dart
+++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart
@@ -364,11 +364,7 @@
     // element, so that both the framework and the browser agree on what's
     // currently focused.
     bool needsDomFocusRequest = false;
-    final EditingState editingState = EditingState(
-      text: semanticsObject.value,
-      baseOffset: semanticsObject.textSelectionBase,
-      extentOffset: semanticsObject.textSelectionExtent,
-    );
+
     if (semanticsObject.hasFocus) {
       if (!_hasFocused) {
         _hasFocused = true;
@@ -378,14 +374,9 @@
       if (domDocument.activeElement != editableElement) {
         needsDomFocusRequest = true;
       }
-      // Focused elements should have full text editing state applied.
-      SemanticsTextEditingStrategy.instance.setEditingState(editingState);
     } else if (_hasFocused) {
       SemanticsTextEditingStrategy.instance.deactivate(this);
 
-      // Only apply text, because this node is not focused.
-      editingState.applyTextToDomElement(editableElement);
-
       if (_hasFocused && domDocument.activeElement == editableElement) {
         // Unlike `editableElement.focus()` we don't need to schedule `blur`
         // post-update because `document.activeElement` implies that the
diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart
index 91129dc..fb87725 100644
--- a/lib/web_ui/test/engine/semantics/text_field_test.dart
+++ b/lib/web_ui/test/engine/semantics/text_field_test.dart
@@ -4,6 +4,8 @@
 
 @TestOn('chrome || safari || firefox')
 
+import 'dart:typed_data';
+
 import 'package:test/bootstrap/browser.dart';
 import 'package:test/test.dart';
 
@@ -48,6 +50,11 @@
       testTextEditing.configuration = singlelineConfig;
     });
 
+    /// Emulates sending of a message by the framework to the engine.
+    void sendFrameworkMessage(ByteData? message) {
+      testTextEditing.channel.handleTextInput(message, (ByteData? data) {});
+    }
+
   test('renders a text field', () async {
     semantics()
       ..debugOverrideTimestampFunction(() => _testTime)
@@ -127,7 +134,7 @@
       // TODO(yjbanov): https://github.com/flutter/flutter/issues/50754
       skip: browserEngine != BrowserEngine.blink);
 
-    test('Syncs editing state from framework', () async {
+    test('Syncs semantic state from framework', () async {
       semantics()
         ..debugOverrideTimestampFunction(() => _testTime)
         ..semanticsEnabled = true;
@@ -159,7 +166,6 @@
       expect(domDocument.activeElement, flutterViewEmbedder.glassPaneElement);
       expect(appHostNode.activeElement, strategy.domElement);
       expect(textField.editableElement, strategy.domElement);
-      expect((textField.editableElement as dynamic).value, 'hello');
       expect(textField.editableElement.getAttribute('aria-label'), 'greeting');
       expect(textField.editableElement.style.width, '10px');
       expect(textField.editableElement.style.height, '15px');
@@ -174,7 +180,6 @@
       expect(domDocument.activeElement, domDocument.body);
       expect(appHostNode.activeElement, null);
       expect(strategy.domElement, null);
-      expect((textField.editableElement as dynamic).value, 'bye');
       expect(textField.editableElement.getAttribute('aria-label'), 'farewell');
       expect(textField.editableElement.style.width, '12px');
       expect(textField.editableElement.style.height, '17px');
@@ -188,6 +193,92 @@
       expect(actionCount, 0);
     });
 
+    test(
+        'Does not overwrite text value and selection editing state on semantic updates',
+        () async {
+      semantics()
+        ..debugOverrideTimestampFunction(() => _testTime)
+        ..semanticsEnabled = true;
+
+      strategy.enable(
+        singlelineConfig,
+        onChange: (_, __) {},
+        onAction: (_) {},
+      );
+
+      final SemanticsObject textFieldSemantics = createTextFieldSemantics(
+          value: 'hello',
+          textSelectionBase: 1,
+          textSelectionExtent: 3,
+          isFocused: true,
+          rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
+
+      final TextField textField =
+          textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
+      final DomHTMLInputElement editableElement =
+          textField.editableElement as DomHTMLInputElement;
+
+      expect(editableElement, strategy.domElement);
+      expect(editableElement.value, '');
+      expect(editableElement.selectionStart, 0);
+      expect(editableElement.selectionEnd, 0);
+
+      strategy.disable();
+      semantics().semanticsEnabled = false;
+    });
+
+    test(
+        'Updates editing state when receiving framework messages from the text input channel',
+        () async {
+      semantics()
+        ..debugOverrideTimestampFunction(() => _testTime)
+        ..semanticsEnabled = true;
+
+      expect(domDocument.activeElement, domDocument.body);
+      expect(appHostNode.activeElement, null);
+
+      strategy.enable(
+        singlelineConfig,
+        onChange: (_, __) {},
+        onAction: (_) {},
+      );
+
+      final SemanticsObject textFieldSemantics = createTextFieldSemantics(
+          value: 'hello',
+          textSelectionBase: 1,
+          textSelectionExtent: 3,
+          isFocused: true,
+          rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
+
+      final TextField textField =
+          textFieldSemantics.debugRoleManagerFor(Role.textField)! as TextField;
+      final DomHTMLInputElement editableElement =
+          textField.editableElement as DomHTMLInputElement;
+
+      // No updates expected on semantic updates
+      expect(editableElement, strategy.domElement);
+      expect(editableElement.value, '');
+      expect(editableElement.selectionStart, 0);
+      expect(editableElement.selectionEnd, 0);
+
+      // Update from framework
+      const MethodCall setEditingState =
+          MethodCall('TextInput.setEditingState', <String, dynamic>{
+        'text': 'updated',
+        'selectionBase': 2,
+        'selectionExtent': 3,
+      });
+      sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
+
+      // Editing state should now be updated
+      expect(editableElement.value, 'updated');
+      expect(editableElement.selectionStart, 2);
+      expect(editableElement.selectionEnd, 3);
+
+      strategy.disable();
+      semantics().semanticsEnabled = false;
+    });
+
     test('Gives up focus after DOM blur', () async {
       semantics()
         ..debugOverrideTimestampFunction(() => _testTime)
@@ -446,6 +537,8 @@
   bool isFocused = false,
   bool isMultiline = false,
   ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
+  int textSelectionBase = 0,
+  int textSelectionExtent = 0,
 }) {
   final SemanticsTester tester = SemanticsTester(semantics());
   tester.updateNode(
@@ -458,6 +551,8 @@
     hasTap: true,
     rect: rect,
     textDirection: ui.TextDirection.ltr,
+    textSelectionBase: textSelectionBase,
+    textSelectionExtent: textSelectionExtent
   );
   tester.apply();
   return tester.getSemanticsObject(0);