[web] switch from .didGain/LoseAccessibilityFocus to .focus (#53360)

This is a repeat of https://github.com/flutter/engine/pull/53134, which was merged prematurely.

> [!WARNING]  
> Only land this after:
> * https://github.com/flutter/flutter/pull/149840 lands in the framework.
> * You have personally manually tested the change together with the latest framework on all browsers.

## Original PR description

Stop using `SemanticsAction.didGain/LoseAccessibilityFocus` on the web, start using `SemanticsAction.focus`. This is because on the web, a11y focus is not observable, only input focus is. Sending `SemanticsAction.focus` will guarantee that the framework move focus to the respective widget. There currently is no "unfocus" signal, because it seems to be already covered: either another widget gains focus, or an HTML DOM element outside the Flutter view does, both of which have their respective signals already.

More details in the discussion in the issue https://github.com/flutter/flutter/issues/83809.

Fixes https://github.com/flutter/flutter/issues/83809
Fixes https://github.com/flutter/flutter/issues/148285
Fixes https://github.com/flutter/flutter/issues/143337
diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart
index 57bc1fa3..481c9c3 100644
--- a/lib/ui/semantics.dart
+++ b/lib/ui/semantics.dart
@@ -220,6 +220,9 @@
   /// must immediately become editable, opening a virtual keyboard, if needed.
   /// Buttons must respond to tap/click events from the keyboard.
   ///
+  /// Widget reaction to this action must be idempotent. It is possible to
+  /// receive this action more than once, or when the widget is already focused.
+  ///
   /// Focus behavior is specific to the platform and to the assistive technology
   /// used. Typically on desktop operating systems, such as Windows, macOS, and
   /// Linux, moving accessibility focus will also move the input focus. On
diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart
index 622cf3f..42f6cfa 100644
--- a/lib/web_ui/lib/src/engine/dom.dart
+++ b/lib/web_ui/lib/src/engine/dom.dart
@@ -2748,6 +2748,30 @@
   }
 }
 
+/// This is a pseudo-type for DOM elements that have the boolean `disabled`
+/// property.
+///
+/// This type cannot be part of the actual type hierarchy because each DOM type
+/// defines its `disabled` property ad hoc, without inheriting it from a common
+/// type, e.g. [DomHTMLInputElement] and [DomHTMLTextAreaElement].
+///
+/// To use, simply cast any element known to have the `disabled` property to
+/// this type using `as DomElementWithDisabledProperty`, then read and write
+/// this property as normal.
+@JS()
+@staticInterop
+class DomElementWithDisabledProperty extends DomHTMLElement {}
+
+extension DomElementWithDisabledPropertyExtension on DomElementWithDisabledProperty {
+  @JS('disabled')
+  external JSBoolean? get _disabled;
+  bool? get disabled => _disabled?.toDart;
+
+  @JS('disabled')
+  external set _disabled(JSBoolean? value);
+  set disabled(bool? value) => _disabled = value?.toJS;
+}
+
 @JS()
 @staticInterop
 class DomHTMLInputElement extends DomHTMLElement {}
diff --git a/lib/web_ui/lib/src/engine/semantics/focusable.dart b/lib/web_ui/lib/src/engine/semantics/focusable.dart
index 35fff64..331e1cd 100644
--- a/lib/web_ui/lib/src/engine/semantics/focusable.dart
+++ b/lib/web_ui/lib/src/engine/semantics/focusable.dart
@@ -81,9 +81,6 @@
 
   /// The listener for the "focus" DOM event.
   DomEventListener domFocusListener,
-
-  /// The listener for the "blur" DOM event.
-  DomEventListener domBlurListener,
 });
 
 /// Implements accessibility focus management for arbitrary elements.
@@ -135,7 +132,6 @@
         semanticsNodeId: semanticsNodeId,
         element: previousTarget.element,
         domFocusListener: previousTarget.domFocusListener,
-        domBlurListener: previousTarget.domBlurListener,
       );
       return;
     }
@@ -148,14 +144,12 @@
     final _FocusTarget newTarget = (
       semanticsNodeId: semanticsNodeId,
       element: element,
-      domFocusListener: createDomEventListener((_) => _setFocusFromDom(true)),
-      domBlurListener: createDomEventListener((_) => _setFocusFromDom(false)),
+      domFocusListener: createDomEventListener((_) => _didReceiveDomFocus()),
     );
     _target = newTarget;
 
     element.tabIndex = 0;
     element.addEventListener('focus', newTarget.domFocusListener);
-    element.addEventListener('blur', newTarget.domBlurListener);
   }
 
   /// Stops managing the focus of the current element, if any.
@@ -170,10 +164,9 @@
     }
 
     target.element.removeEventListener('focus', target.domFocusListener);
-    target.element.removeEventListener('blur', target.domBlurListener);
   }
 
-  void _setFocusFromDom(bool acquireFocus) {
+  void _didReceiveDomFocus() {
     final _FocusTarget? target = _target;
 
     if (target == null) {
@@ -184,9 +177,7 @@
 
     EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
       target.semanticsNodeId,
-      acquireFocus
-        ? ui.SemanticsAction.didGainAccessibilityFocus
-        : ui.SemanticsAction.didLoseAccessibilityFocus,
+      ui.SemanticsAction.focus,
       null,
     );
   }
@@ -229,7 +220,7 @@
       // a dialog, and nothing else in the dialog is focused. The Flutter
       // framework expects that the screen reader will focus on the first (in
       // traversal order) focusable element inside the dialog and send a
-      // didGainAccessibilityFocus action. Screen readers on the web do not do
+      // SemanticsAction.focus action. Screen readers on the web do not do
       // that, and so the web engine has to implement this behavior directly. So
       // the dialog will look for a focusable element and request focus on it,
       // but now there may be a race between this method unsetting the focus and
diff --git a/lib/web_ui/lib/src/engine/semantics/semantics.dart b/lib/web_ui/lib/src/engine/semantics/semantics.dart
index c48851d..0918ef4 100644
--- a/lib/web_ui/lib/src/engine/semantics/semantics.dart
+++ b/lib/web_ui/lib/src/engine/semantics/semantics.dart
@@ -2218,8 +2218,6 @@
       'mousemove',
       'mouseleave',
       'mouseup',
-      'keyup',
-      'keydown',
     ];
 
     if (pointerEventTypes.contains(event.type)) {
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 bb79ea1..3618306 100644
--- a/lib/web_ui/lib/src/engine/semantics/text_field.dart
+++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart
@@ -2,11 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
 import 'package:ui/ui.dart' as ui;
-import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
 
-import '../browser_detection.dart' show isIosSafari;
 import '../dom.dart';
 import '../platform_dispatcher.dart';
 import '../text_editing/text_editing.dart';
@@ -123,7 +120,10 @@
     // Android).
     // Otherwise, the keyboard stays on screen even when the user navigates to
     // a different screen (e.g. by hitting the "back" button).
-    domElement?.blur();
+    // Keep this consistent with how DefaultTextEditingStrategy does it. As of
+    // right now, the only difference is that semantic text fields do not
+    // participate in form autofill.
+    DefaultTextEditingStrategy.scheduleFocusFlutterView(activeDomElement, activeDomElementView);
     domElement = null;
     activeTextField = null;
     _queuedStyle = null;
@@ -162,7 +162,7 @@
     if (hasAutofillGroup) {
       placeForm();
     }
-    activeDomElement.focus();
+    activeDomElement.focus(preventScroll: true);
   }
 
   @override
@@ -207,69 +207,40 @@
 /// [EngineSemanticsOwner.gestureMode]. However, in Chrome on Android it ignores
 /// browser gestures when in pointer mode. In Safari on iOS pointer events are
 /// used to detect text box invocation. This is because Safari issues touch
-/// events even when Voiceover is enabled.
+/// events even when VoiceOver is enabled.
 class TextField extends PrimaryRoleManager {
   TextField(SemanticsObject semanticsObject) : super.blank(PrimaryRole.textField, semanticsObject) {
-    _setupDomElement();
+    _initializeEditableElement();
   }
 
-  /// The element used for editing, e.g. `<input>`, `<textarea>`.
-  DomHTMLElement? editableElement;
-
-  /// Same as [editableElement] but null-checked.
-  DomHTMLElement get activeEditableElement {
-    assert(
-      editableElement != null,
-      'The textField does not have an active editable element',
-    );
-    return editableElement!;
-  }
+  /// The element used for editing, e.g. `<input>`, `<textarea>`, which is
+  /// different from the host [element].
+  late final DomHTMLElement editableElement;
 
   @override
   bool focusAsRouteDefault() {
-    final DomHTMLElement? editableElement = this.editableElement;
-    if (editableElement == null) {
-      return false;
-    }
-    editableElement.focus();
+    editableElement.focus(preventScroll: true);
     return true;
   }
 
-  /// Timer that times when to set the location of the input text.
-  ///
-  /// This is only used for iOS. In iOS, virtual keyboard shifts the screen.
-  /// There is no callback to know if the keyboard is up and how much the screen
-  /// has shifted. Therefore instead of listening to the shift and passing this
-  /// information to Flutter Framework, we are trying to stop the shift.
-  ///
-  /// In iOS, the virtual keyboard shifts the screen up if the focused input
-  /// element is under the keyboard or very close to the keyboard. Before the
-  /// focus is called we are positioning it offscreen. The location of the input
-  /// in iOS is set to correct place, 100ms after focus. We use this timer for
-  /// timing this delay.
-  Timer? _positionInputElementTimer;
-  static const Duration _delayBeforePlacement = Duration(milliseconds: 100);
-
   void _initializeEditableElement() {
-    assert(editableElement == null,
-        'Editable element has already been initialized');
-
     editableElement = semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline)
         ? createDomHTMLTextAreaElement()
         : createDomHTMLInputElement();
+    _updateEnabledState();
 
     // On iOS, even though the semantic text field is transparent, the cursor
     // and text highlighting are still visible. The cursor and text selection
     // are made invisible by CSS in [StyleManager.attachGlobalStyles].
     // But there's one more case where iOS highlights text. That's when there's
     // and autocorrect suggestion. To disable that, we have to do the following:
-    activeEditableElement
+    editableElement
       ..spellcheck = false
       ..setAttribute('autocorrect', 'off')
       ..setAttribute('autocomplete', 'off')
       ..setAttribute('data-semantics-role', 'text-field');
 
-    activeEditableElement.style
+    editableElement.style
       ..position = 'absolute'
       // `top` and `left` are intentionally set to zero here.
       //
@@ -284,141 +255,19 @@
       ..left = '0'
       ..width = '${semanticsObject.rect!.width}px'
       ..height = '${semanticsObject.rect!.height}px';
-    append(activeEditableElement);
-  }
+    append(editableElement);
 
-  void _setupDomElement() {
-    switch (ui_web.browser.browserEngine) {
-      case ui_web.BrowserEngine.blink:
-      case ui_web.BrowserEngine.firefox:
-        _initializeForBlink();
-      case ui_web.BrowserEngine.webkit:
-        _initializeForWebkit();
-    }
-  }
-
-  /// Chrome on Android reports text field activation as a "click" event.
-  ///
-  /// When in browser gesture mode, the focus is forwarded to the framework as
-  /// a tap to initialize editing.
-  void _initializeForBlink() {
-    _initializeEditableElement();
-    activeEditableElement.addEventListener('focus',
-        createDomEventListener((DomEvent event) {
-          if (EngineSemantics.instance.gestureMode != GestureMode.browserGestures) {
-            return;
-          }
-
-          EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
-              semanticsObject.id, ui.SemanticsAction.didGainAccessibilityFocus, null);
-        }));
-    activeEditableElement.addEventListener('blur',
-        createDomEventListener((DomEvent event) {
-          if (EngineSemantics.instance.gestureMode != GestureMode.browserGestures) {
-            return;
-          }
-
-          EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
-              semanticsObject.id, ui.SemanticsAction.didLoseAccessibilityFocus, null);
-        }));
-  }
-
-  /// Safari on iOS reports text field activation via pointer events.
-  ///
-  /// This emulates a tap recognizer to detect the activation. Because pointer
-  /// events are present regardless of whether accessibility is enabled or not,
-  /// this mode is always enabled.
-  ///
-  /// In iOS, the virtual keyboard shifts the screen up if the focused input
-  /// element is under the keyboard or very close to the keyboard. To avoid the shift,
-  /// the creation of the editable element is delayed until a tap is detected.
-  ///
-  /// In the absence of an editable DOM element, role of 'textbox' is assigned to the
-  /// semanticsObject.element to communicate to the assistive technologies that
-  /// the user can start editing by tapping on the element. Once a tap is detected,
-  /// the editable element gets created and the role of textbox is removed from
-  /// semanicsObject.element to avoid confusing VoiceOver.
-  void _initializeForWebkit() {
-    // Safari for desktop is also initialized as the other browsers.
-    if (ui_web.browser.operatingSystem == ui_web.OperatingSystem.macOs) {
-      _initializeForBlink();
-      return;
-    }
-
-    setAttribute('role', 'textbox');
-    setAttribute('contenteditable', 'false');
-    setAttribute('tabindex', '0');
-
-    num? lastPointerDownOffsetX;
-    num? lastPointerDownOffsetY;
-
-    addEventListener('pointerdown',
-        createDomEventListener((DomEvent event) {
-          final DomPointerEvent pointerEvent = event as DomPointerEvent;
-          lastPointerDownOffsetX = pointerEvent.clientX;
-          lastPointerDownOffsetY = pointerEvent.clientY;
-        }), true);
-
-    addEventListener('pointerup',
-        createDomEventListener((DomEvent event) {
-      final DomPointerEvent pointerEvent = event as DomPointerEvent;
-
-      if (lastPointerDownOffsetX != null) {
-        assert(lastPointerDownOffsetY != null);
-        final num deltaX = pointerEvent.clientX - lastPointerDownOffsetX!;
-        final num deltaY = pointerEvent.clientY - lastPointerDownOffsetY!;
-
-        // This should match the similar constant defined in:
-        //
-        // lib/src/gestures/constants.dart
-        //
-        // The value is pre-squared so we have to do less math at runtime.
-        const double kTouchSlop = 18.0 * 18.0; // Logical pixels squared
-
-        if (deltaX * deltaX + deltaY * deltaY < kTouchSlop) {
-          // Recognize it as a tap that requires a keyboard.
-          EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
-              semanticsObject.id, ui.SemanticsAction.tap, null);
-          _invokeIosWorkaround();
-        }
-      } else {
-        assert(lastPointerDownOffsetY == null);
-      }
-
-      lastPointerDownOffsetX = null;
-      lastPointerDownOffsetY = null;
-    }), true);
-  }
-
-  void _invokeIosWorkaround() {
-    if (editableElement != null) {
-      return;
-    }
-
-    _initializeEditableElement();
-    activeEditableElement.style.transform = 'translate(${offScreenOffset}px, ${offScreenOffset}px)';
-    _positionInputElementTimer?.cancel();
-    _positionInputElementTimer = Timer(_delayBeforePlacement, () {
-      editableElement?.style.transform = '';
-      _positionInputElementTimer = null;
-    });
-
-    // Can not have both activeEditableElement and semanticsObject.element
-    // represent the same text field. It will confuse VoiceOver, so `role` needs to
-    // be assigned and removed, based on whether or not editableElement exists.
-    activeEditableElement.focus();
-    removeAttribute('role');
-
-    activeEditableElement.addEventListener('blur',
-        createDomEventListener((DomEvent event) {
-      setAttribute('role', 'textbox');
-      activeEditableElement.remove();
+    editableElement.addEventListener('focus', createDomEventListener((DomEvent event) {
+      // IMPORTANT: because this event listener can be triggered by either or
+      // both a "focus" and a "click" DOM events, this code must be idempotent.
+      EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
+          semanticsObject.id, ui.SemanticsAction.focus, null);
+    }));
+    editableElement.addEventListener('click', createDomEventListener((DomEvent event) {
+      editableElement.focus(preventScroll: true);
+    }));
+    editableElement.addEventListener('blur', createDomEventListener((DomEvent event) {
       SemanticsTextEditingStrategy._instance?.deactivate(this);
-
-      // Focus on semantics element before removing the editable element, so that
-      // the user can continue navigating the page with the assistive technology.
-      element.focus();
-      editableElement = null;
     }));
   }
 
@@ -426,55 +275,36 @@
   void update() {
     super.update();
 
-    // Ignore the update if editableElement has not been created yet.
-    // On iOS Safari, when the user dismisses the keyboard using the 'done' button,
-    // we recieve a `blur` event from the browswer and a semantic update with
-    // [hasFocus] set to true from the framework. In this case, we ignore the update
-    // and wait for a tap event before invoking the iOS workaround and creating
-    // the editable element.
-    if (editableElement != null) {
-      activeEditableElement.style
-        ..width = '${semanticsObject.rect!.width}px'
-        ..height = '${semanticsObject.rect!.height}px';
+    _updateEnabledState();
+    editableElement.style
+      ..width = '${semanticsObject.rect!.width}px'
+      ..height = '${semanticsObject.rect!.height}px';
 
-      if (semanticsObject.hasFocus) {
-        if (domDocument.activeElement !=
-            activeEditableElement) {
-          semanticsObject.owner.addOneTimePostUpdateCallback(() {
-            activeEditableElement.focus();
-          });
-        }
-        SemanticsTextEditingStrategy._instance?.activate(this);
-      } else if (domDocument.activeElement ==
-          activeEditableElement) {
-        if (!isIosSafari) {
-          SemanticsTextEditingStrategy._instance?.deactivate(this);
-          // Only apply text, because this node is not focused.
-        }
-        activeEditableElement.blur();
+    if (semanticsObject.hasFocus) {
+      if (domDocument.activeElement != editableElement && semanticsObject.isEnabled) {
+        semanticsObject.owner.addOneTimePostUpdateCallback(() {
+          editableElement.focus(preventScroll: true);
+        });
       }
+      SemanticsTextEditingStrategy._instance?.activate(this);
     }
 
-    final DomElement element = editableElement ?? this.element;
     if (semanticsObject.hasLabel) {
-      element.setAttribute(
-        'aria-label',
-        semanticsObject.label!,
-      );
+      if (semanticsObject.isLabelDirty) {
+        editableElement.setAttribute('aria-label', semanticsObject.label!);
+      }
     } else {
-      element.removeAttribute('aria-label');
+      editableElement.removeAttribute('aria-label');
     }
   }
 
+  void _updateEnabledState() {
+    (editableElement as DomElementWithDisabledProperty).disabled = !semanticsObject.isEnabled;
+  }
+
   @override
   void dispose() {
     super.dispose();
-    _positionInputElementTimer?.cancel();
-    _positionInputElementTimer = null;
-    // on iOS, the `blur` event listener callback will remove the element.
-    if (!isIosSafari) {
-      editableElement?.remove();
-    }
     SemanticsTextEditingStrategy._instance?.deactivate(this);
   }
 }
diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
index f1c45b4..e76b19a 100644
--- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
@@ -1228,7 +1228,7 @@
   }
 
   /// The [FlutterView] in which [activeDomElement] is contained.
-  EngineFlutterView? get _activeDomElementView => _viewForElement(activeDomElement);
+  EngineFlutterView? get activeDomElementView => _viewForElement(activeDomElement);
 
   EngineFlutterView? _viewForElement(DomElement element) =>
     EnginePlatformDispatcher.instance.viewManager.findViewForElement(element);
@@ -1411,9 +1411,9 @@
         inputConfiguration.autofillGroup?.formElement != null) {
       _styleAutofillElements(activeDomElement, isOffScreen: true);
       inputConfiguration.autofillGroup?.storeForm();
-      _moveFocusToFlutterView(activeDomElement, _activeDomElementView);
+      scheduleFocusFlutterView(activeDomElement, activeDomElementView);
     } else {
-      _moveFocusToFlutterView(activeDomElement, _activeDomElementView, removeElement: true);
+      scheduleFocusFlutterView(activeDomElement, activeDomElementView, removeElement: true);
 		}
     domElement = null;
   }
@@ -1498,7 +1498,7 @@
     event as DomFocusEvent;
 
     final DomElement? willGainFocusElement = event.relatedTarget as DomElement?;
-    if (willGainFocusElement == null || _viewForElement(willGainFocusElement) == _activeDomElementView) {
+    if (willGainFocusElement == null || _viewForElement(willGainFocusElement) == activeDomElementView) {
       moveFocusToActiveDomElement();
     }
   }
@@ -1574,17 +1574,20 @@
     activeDomElement.focus(preventScroll: true);
   }
 
-  /// Moves the focus to the [EngineFlutterView].
+  /// Move the focus to the given [EngineFlutterView] in the next timer event.
   ///
-  /// The delay gives the engine the opportunity to focus another <input /> element.
-  /// The delay should help prevent the keyboard from jumping when the focus goes from
-  /// one text field to another.
-  static void _moveFocusToFlutterView(
+  /// The timer gives the engine the opportunity to focus on another element.
+  /// Shifting focus immediately can cause the keyboard to jump.
+  static void scheduleFocusFlutterView(
     DomElement element,
     EngineFlutterView? view, {
     bool removeElement = false,
   }) {
     Timer(Duration.zero, () {
+      // If by the time the timer fired the focused element is no longer the
+      // editing element whose editing session was disabled, there's no need to
+      // move the focus, as it is likely that another widget already took the
+      // focus.
       if (element == domDocument.activeElement) {
         view?.dom.rootElement.focus(preventScroll: true);
       }
@@ -2204,6 +2207,9 @@
         command = const TextInputSetCaretRect();
 
       default:
+        if (_debugPrintTextInputCommands) {
+          print('Received unknown command on flutter/textinput channel: ${call.method}');
+        }
         EnginePlatformDispatcher.instance.replyToPlatformMessage(callback, null);
         return;
     }
diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart
index 64fa0a5..9f312f3 100644
--- a/lib/web_ui/test/engine/semantics/semantics_test.dart
+++ b/lib/web_ui/test/engine/semantics/semantics_test.dart
@@ -1841,7 +1841,7 @@
 
     pumpSemantics(isFocused: true);
     expect(capturedActions, <CapturedAction>[
-      (0, ui.SemanticsAction.didGainAccessibilityFocus, null),
+      (0, ui.SemanticsAction.focus, null),
     ]);
     capturedActions.clear();
 
@@ -1852,10 +1852,12 @@
       isEmpty,
     );
 
+    // The web doesn't send didLoseAccessibilityFocus as on the web,
+    // accessibility focus is not observable, only input focus is. As of this
+    // writing, there is no SemanticsAction.unfocus action, so the test simply
+    // asserts that no actions are being sent as a result of blur.
     element.blur();
-    expect(capturedActions, <CapturedAction>[
-      (0, ui.SemanticsAction.didLoseAccessibilityFocus, null),
-    ]);
+    expect(capturedActions, isEmpty);
 
     semantics().semanticsEnabled = false;
   });
@@ -1886,15 +1888,14 @@
 
 
     final SemanticsObject node = owner().debugSemanticsTree![0]!;
+    final TextField textFieldRole = node.primaryRole! as TextField;
+    final DomHTMLInputElement inputElement = textFieldRole.editableElement as DomHTMLInputElement;
 
     // TODO(yjbanov): this used to attempt to test that value="hello" but the
     //                test was a false positive. We should revise this test and
     //                make sure it tests the right things:
     //                https://github.com/flutter/flutter/issues/147200
-    expect(
-      (node.element as DomHTMLInputElement).value,
-      isNull,
-    );
+    expect(inputElement.value, '');
 
     expect(node.primaryRole?.role, PrimaryRole.textField);
     expect(
@@ -1905,42 +1906,6 @@
 
     semantics().semanticsEnabled = false;
   });
-
-  // TODO(yjbanov): this test will need to be adjusted for Safari when we add
-  //                Safari testing.
-  test('sends a focus action when text field is activated', () async {
-    final SemanticsActionLogger logger = SemanticsActionLogger();
-    semantics()
-      ..debugOverrideTimestampFunction(() => _testTime)
-      ..semanticsEnabled = true;
-
-    final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder();
-    updateNode(
-      builder,
-      actions: 0 | ui.SemanticsAction.didGainAccessibilityFocus.index,
-      flags: 0 | ui.SemanticsFlag.isTextField.index,
-      value: 'hello',
-      transform: Matrix4.identity().toFloat64(),
-      rect: const ui.Rect.fromLTRB(0, 0, 100, 50),
-    );
-
-    owner().updateSemantics(builder.build());
-
-    final DomElement textField =
-        owner().semanticsHost.querySelector('input[data-semantics-role="text-field"]')!;
-
-    expect(owner().semanticsHost.ownerDocument?.activeElement, isNot(textField));
-
-    textField.focus();
-
-    expect(owner().semanticsHost.ownerDocument?.activeElement, textField);
-    expect(await logger.idLog.first, 0);
-    expect(await logger.actionLog.first, ui.SemanticsAction.didGainAccessibilityFocus);
-
-    semantics().semanticsEnabled = false;
-  }, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638
-      // TODO(yjbanov): https://github.com/flutter/flutter/issues/50590
-      skip: ui_web.browser.browserEngine != ui_web.BrowserEngine.blink);
 }
 
 void _testCheckables() {
@@ -2221,7 +2186,7 @@
 
     pumpSemantics(isFocused: true);
     expect(capturedActions, <CapturedAction>[
-      (0, ui.SemanticsAction.didGainAccessibilityFocus, null),
+      (0, ui.SemanticsAction.focus, null),
     ]);
     capturedActions.clear();
 
@@ -2231,15 +2196,12 @@
     pumpSemantics(isFocused: false);
     expect(capturedActions, isEmpty);
 
-    // If the element is blurred by the browser, then we do want to notify the
-    // framework. This is because screen reader can be focused on something
-    // other than what the framework is focused on, and notifying the framework
-    // about the loss of focus on a node is information that the framework did
-    // not have before.
+    // The web doesn't send didLoseAccessibilityFocus as on the web,
+    // accessibility focus is not observable, only input focus is. As of this
+    // writing, there is no SemanticsAction.unfocus action, so the test simply
+    // asserts that no actions are being sent as a result of blur.
     element.blur();
-    expect(capturedActions, <CapturedAction>[
-      (0, ui.SemanticsAction.didLoseAccessibilityFocus, null),
-    ]);
+    expect(capturedActions, isEmpty);
 
     semantics().semanticsEnabled = false;
   });
@@ -2405,17 +2367,19 @@
 
     pumpSemantics(isFocused: true);
     expect(capturedActions, <CapturedAction>[
-      (0, ui.SemanticsAction.didGainAccessibilityFocus, null),
+      (0, ui.SemanticsAction.focus, null),
     ]);
     capturedActions.clear();
 
     pumpSemantics(isFocused: false);
     expect(capturedActions, isEmpty);
 
+    // The web doesn't send didLoseAccessibilityFocus as on the web,
+    // accessibility focus is not observable, only input focus is. As of this
+    // writing, there is no SemanticsAction.unfocus action, so the test simply
+    // asserts that no actions are being sent as a result of blur.
     element.blur();
-    expect(capturedActions, <CapturedAction>[
-      (0, ui.SemanticsAction.didLoseAccessibilityFocus, null),
-    ]);
+    expect(capturedActions, isEmpty);
 
     semantics().semanticsEnabled = false;
   });
@@ -3245,7 +3209,7 @@
     expect(
       capturedActions,
       <CapturedAction>[
-        (2, ui.SemanticsAction.didGainAccessibilityFocus, null),
+        (2, ui.SemanticsAction.focus, null),
       ],
     );
 
@@ -3307,7 +3271,7 @@
     expect(
       capturedActions,
       <CapturedAction>[
-        (3, ui.SemanticsAction.didGainAccessibilityFocus, null),
+        (3, ui.SemanticsAction.focus, null),
       ],
     );
 
@@ -3457,7 +3421,7 @@
     pumpSemantics(); // triggers post-update callbacks
     expect(domDocument.activeElement, element);
     expect(capturedActions, <CapturedAction>[
-      (1, ui.SemanticsAction.didGainAccessibilityFocus, null),
+      (1, ui.SemanticsAction.focus, null),
     ]);
     capturedActions.clear();
 
@@ -3470,9 +3434,11 @@
     // Browser blurs the element
     element.blur();
     expect(domDocument.activeElement, isNot(element));
-    expect(capturedActions, <CapturedAction>[
-      (1, ui.SemanticsAction.didLoseAccessibilityFocus, null),
-    ]);
+    // The web doesn't send didLoseAccessibilityFocus as on the web,
+    // accessibility focus is not observable, only input focus is. As of this
+    // writing, there is no SemanticsAction.unfocus action, so the test simply
+    // asserts that no actions are being sent as a result of blur.
+    expect(capturedActions, isEmpty);
     capturedActions.clear();
 
     // Request focus again
@@ -3480,7 +3446,7 @@
     pumpSemantics(); // triggers post-update callbacks
     expect(domDocument.activeElement, element);
     expect(capturedActions, <CapturedAction>[
-      (1, ui.SemanticsAction.didGainAccessibilityFocus, null),
+      (1, ui.SemanticsAction.focus, null),
     ]);
     capturedActions.clear();
 
diff --git a/lib/web_ui/test/engine/semantics/semantics_tester.dart b/lib/web_ui/test/engine/semantics/semantics_tester.dart
index f9a626c..e14a2c6 100644
--- a/lib/web_ui/test/engine/semantics/semantics_tester.dart
+++ b/lib/web_ui/test/engine/semantics/semantics_tester.dart
@@ -75,6 +75,7 @@
     bool? hasPaste,
     bool? hasDidGainAccessibilityFocus,
     bool? hasDidLoseAccessibilityFocus,
+    bool? hasFocus,
     bool? hasCustomAction,
     bool? hasDismiss,
     bool? hasMoveCursorForwardByWord,
@@ -242,6 +243,9 @@
     if (hasDidLoseAccessibilityFocus ?? false) {
       actions |= ui.SemanticsAction.didLoseAccessibilityFocus.index;
     }
+    if (hasFocus ?? false) {
+      actions |= ui.SemanticsAction.focus.index;
+    }
     if (hasCustomAction ?? false) {
       actions |= ui.SemanticsAction.customAction.index;
     }
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 aee5e1b..116c8f4 100644
--- a/lib/web_ui/test/engine/semantics/text_field_test.dart
+++ b/lib/web_ui/test/engine/semantics/text_field_test.dart
@@ -2,9 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-@TestOn('chrome || safari || firefox')
-library;
-
+import 'dart:async';
 import 'dart:typed_data';
 
 import 'package:test/bootstrap/browser.dart';
@@ -16,7 +14,8 @@
 import '../../common/test_initialization.dart';
 import 'semantics_tester.dart';
 
-final InputConfiguration singlelineConfig = InputConfiguration(viewId: kImplicitViewId);
+final InputConfiguration singlelineConfig =
+    InputConfiguration(viewId: kImplicitViewId);
 
 final InputConfiguration multilineConfig = InputConfiguration(
   viewId: kImplicitViewId,
@@ -25,7 +24,8 @@
 );
 
 EngineSemantics semantics() => EngineSemantics.instance;
-EngineSemanticsOwner owner() => EnginePlatformDispatcher.instance.implicitView!.semantics;
+EngineSemanticsOwner owner() =>
+    EnginePlatformDispatcher.instance.implicitView!.semantics;
 
 const MethodCodec codec = JSONMethodCodec();
 
@@ -89,53 +89,66 @@
       semantics().semanticsEnabled = false;
     });
 
-  test('renders a text field', () {
-    createTextFieldSemantics(value: 'hello');
+    test('renders a text field', () {
+      createTextFieldSemantics(value: 'hello');
 
-    expectSemanticsTree(owner(), '''
-<sem>
-  <input />
-</sem>''');
+      expectSemanticsTree(owner(), '''
+  <sem>
+    <input />
+  </sem>''');
 
-    // TODO(yjbanov): this used to attempt to test that value="hello" but the
-    //                test was a false positive. We should revise this test and
-    //                make sure it tests the right things:
-    //                https://github.com/flutter/flutter/issues/147200
-    final SemanticsObject node = owner().debugSemanticsTree![0]!;
-    expect(
-      (node.element as DomHTMLInputElement).value,
-      isNull,
-    );
-  });
+      // TODO(yjbanov): this used to attempt to test that value="hello" but the
+      //                test was a false positive. We should revise this test and
+      //                make sure it tests the right things:
+      //                https://github.com/flutter/flutter/issues/147200
+      final SemanticsObject node = owner().debugSemanticsTree![0]!;
+      final TextField textFieldRole = node.primaryRole! as TextField;
+      final DomHTMLInputElement inputElement =
+          textFieldRole.editableElement as DomHTMLInputElement;
+      expect(inputElement.tagName.toLowerCase(), 'input');
+      expect(inputElement.value, '');
+      expect(inputElement.disabled, isFalse);
+    });
 
-    // TODO(yjbanov): this test will need to be adjusted for Safari when we add
-    //                Safari testing.
-    test('sends a didGainAccessibilityFocus/didLoseAccessibilityFocus action when browser requests focus/blur', () async {
+    test('renders a disabled text field', () {
+      createTextFieldSemantics(isEnabled: false, value: 'hello');
+      expectSemanticsTree(owner(), '''<sem><input /></sem>''');
+      final SemanticsObject node = owner().debugSemanticsTree![0]!;
+      final TextField textFieldRole = node.primaryRole! as TextField;
+      final DomHTMLInputElement inputElement =
+          textFieldRole.editableElement as DomHTMLInputElement;
+      expect(inputElement.tagName.toLowerCase(), 'input');
+      expect(inputElement.disabled, isTrue);
+    });
+
+    test('sends a SemanticsAction.focus action when browser requests focus',
+        () async {
       final SemanticsActionLogger logger = SemanticsActionLogger();
       createTextFieldSemantics(value: 'hello');
 
-      final DomElement textField = owner().semanticsHost
+      final DomElement textField = owner()
+          .semanticsHost
           .querySelector('input[data-semantics-role="text-field"]')!;
 
-      expect(owner().semanticsHost.ownerDocument?.activeElement, isNot(textField));
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, isNot(textField));
 
       textField.focus();
 
       expect(owner().semanticsHost.ownerDocument?.activeElement, textField);
       expect(await logger.idLog.first, 0);
-      expect(await logger.actionLog.first, ui.SemanticsAction.didGainAccessibilityFocus);
+      expect(await logger.actionLog.first, ui.SemanticsAction.focus);
 
       textField.blur();
 
-      expect(owner().semanticsHost.ownerDocument?.activeElement, isNot(textField));
-      expect(await logger.idLog.first, 0);
-      expect(await logger.actionLog.first, ui.SemanticsAction.didLoseAccessibilityFocus);
-    }, // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638
-       // TODO(yjbanov): https://github.com/flutter/flutter/issues/50590
-    skip: ui_web.browser.browserEngine != ui_web.BrowserEngine.blink);
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, isNot(textField));
+      // TODO(yjbanov): https://github.com/flutter/flutter/issues/46638
+    }, skip: ui_web.browser.browserEngine == ui_web.BrowserEngine.firefox);
 
-    test('Syncs semantic state from framework', () {
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+    test('Syncs semantic state from framework', () async {
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
 
       int changeCount = 0;
       int actionCount = 0;
@@ -158,11 +171,12 @@
       );
 
       final TextField textField = textFieldSemantics.primaryRole! as TextField;
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
+      expect(owner().semanticsHost.ownerDocument?.activeElement,
+          strategy.domElement);
       expect(textField.editableElement, strategy.domElement);
-      expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting');
-      expect(textField.activeEditableElement.style.width, '10px');
-      expect(textField.activeEditableElement.style.height, '15px');
+      expect(textField.editableElement.getAttribute('aria-label'), 'greeting');
+      expect(textField.editableElement.style.width, '10px');
+      expect(textField.editableElement.style.height, '15px');
 
       // Update
       createTextFieldSemantics(
@@ -171,13 +185,36 @@
         rect: const ui.Rect.fromLTWH(0, 0, 12, 17),
       );
 
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
-      expect(strategy.domElement, null);
-      expect(textField.activeEditableElement.getAttribute('aria-label'), 'farewell');
-      expect(textField.activeEditableElement.style.width, '12px');
-      expect(textField.activeEditableElement.style.height, '17px');
+      // The web engine used to explicitly blur() elements when the framework
+      // sent an node update with isFocused == false. This is no longer done, as
+      // blurring an element without focusing on another element confuses screen
+      // readers. However, if another element gains focus (e.g. because the
+      // framework focuses on a different widget), then the current element will
+      // be blurred automatically, without needing to call DomElement.blur().
+      expect(
+        owner().semanticsHost.ownerDocument?.activeElement,
+        strategy.domElement,
+      );
+      expect(textField.editableElement.getAttribute('aria-label'), 'farewell');
+      expect(textField.editableElement.style.width, '12px');
+      expect(textField.editableElement.style.height, '17px');
 
       strategy.disable();
+      expect(strategy.domElement, null);
+
+      // Transitively disabling the strategy calls
+      // DefaultTextEditingStrategy.scheduleFocusFlutterView, which uses a timer
+      // before shifting focus. So initially the editable DOM element should be
+      // in place, and is cleared after the timer fires.
+      expect(
+        owner().semanticsHost.ownerDocument?.activeElement,
+        textField.editableElement,
+      );
+      await Future<void>.delayed(Duration.zero);
+      expect(
+        owner().semanticsHost.ownerDocument?.activeElement,
+        EnginePlatformDispatcher.instance.implicitView!.dom.rootElement,
+      );
 
       // There was no user interaction with the <input> element,
       // so we should expect no engine-to-framework feedback.
@@ -203,7 +240,7 @@
 
       final TextField textField = textFieldSemantics.primaryRole! as TextField;
       final DomHTMLInputElement editableElement =
-          textField.activeEditableElement as DomHTMLInputElement;
+          textField.editableElement as DomHTMLInputElement;
 
       expect(editableElement, strategy.domElement);
       expect(editableElement.value, '');
@@ -216,7 +253,8 @@
     test(
         'Updates editing state when receiving framework messages from the text input channel',
         () {
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
 
       strategy.enable(
         singlelineConfig,
@@ -233,7 +271,7 @@
 
       final TextField textField = textFieldSemantics.primaryRole! as TextField;
       final DomHTMLInputElement editableElement =
-          textField.activeEditableElement as DomHTMLInputElement;
+          textField.editableElement as DomHTMLInputElement;
 
       // No updates expected on semantic updates
       expect(editableElement, strategy.domElement);
@@ -248,7 +286,8 @@
         'selectionBase': 2,
         'selectionExtent': 3,
       });
-      sendFrameworkMessage(codec.encodeMethodCall(setEditingState), testTextEditing);
+      sendFrameworkMessage(
+          codec.encodeMethodCall(setEditingState), testTextEditing);
 
       // Editing state should now be updated
       expect(editableElement.value, 'updated');
@@ -259,7 +298,8 @@
     });
 
     test('Gives up focus after DOM blur', () {
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
 
       strategy.enable(
         singlelineConfig,
@@ -273,15 +313,18 @@
 
       final TextField textField = textFieldSemantics.primaryRole! as TextField;
       expect(textField.editableElement, strategy.domElement);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
+      expect(owner().semanticsHost.ownerDocument?.activeElement,
+          strategy.domElement);
 
       // The input should not refocus after blur.
-      textField.activeEditableElement.blur();
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+      textField.editableElement.blur();
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
       strategy.disable();
     });
 
-    test('Does not dispose and recreate dom elements in persistent mode', () {
+    test('Does not dispose and recreate dom elements in persistent mode',
+        () async {
       strategy.enable(
         singlelineConfig,
         onChange: (_, __) {},
@@ -297,7 +340,8 @@
         isFocused: true,
       );
       expect(strategy.domElement, isNotNull);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
+      expect(owner().semanticsHost.ownerDocument?.activeElement,
+          strategy.domElement);
 
       strategy.disable();
       expect(strategy.domElement, isNull);
@@ -307,7 +351,11 @@
       expect(owner().semanticsHost.contains(textField.editableElement), isTrue);
       // Editing element is not enabled.
       expect(strategy.isEnabled, isFalse);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+      await Future<void>.delayed(Duration.zero);
+      expect(
+        owner().semanticsHost.ownerDocument?.activeElement,
+        EnginePlatformDispatcher.instance.implicitView!.dom.rootElement,
+      );
     });
 
     test('Refocuses when setting editing state', () {
@@ -322,11 +370,13 @@
         isFocused: true,
       );
       expect(strategy.domElement, isNotNull);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
+      expect(owner().semanticsHost.ownerDocument?.activeElement,
+          strategy.domElement);
 
       // Blur the element without telling the framework.
       strategy.activeDomElement.blur();
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
 
       // The input will have focus after editing state is set and semantics updated.
       strategy.setEditingState(EditingState(text: 'foo'));
@@ -344,7 +394,8 @@
         value: 'hello',
         isFocused: true,
       );
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
+      expect(owner().semanticsHost.ownerDocument?.activeElement,
+          strategy.domElement);
 
       strategy.disable();
     });
@@ -364,7 +415,8 @@
       final DomHTMLTextAreaElement textArea =
           strategy.domElement! as DomHTMLTextAreaElement;
 
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
+      expect(owner().semanticsHost.ownerDocument?.activeElement,
+          strategy.domElement);
 
       strategy.enable(
         singlelineConfig,
@@ -373,7 +425,8 @@
       );
 
       textArea.blur();
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
+      expect(
+          owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
 
       strategy.disable();
       // It doesn't remove the textarea from the DOM.
@@ -427,6 +480,7 @@
         children: <SemanticsNodeUpdate>[
           builder.updateNode(
             id: 1,
+            isEnabled: true,
             isTextField: true,
             value: 'Hello',
             isFocused: focusFieldId == 1,
@@ -434,6 +488,7 @@
           ),
           builder.updateNode(
             id: 2,
+            isEnabled: true,
             isTextField: true,
             value: 'World',
             isFocused: focusFieldId == 2,
@@ -444,7 +499,9 @@
       return builder.apply();
     }
 
-    test('Changes focus from one text field to another through a semantics update', () {
+    test(
+        'Changes focus from one text field to another through a semantics update',
+        () {
       strategy.enable(
         singlelineConfig,
         onChange: (_, __) {},
@@ -468,422 +525,13 @@
         expect(strategy.domElement, tester.getTextField(2).editableElement);
       }
     });
-  }, skip: isIosSafari);
-
-  group('$SemanticsTextEditingStrategy in iOS', () {
-    late HybridTextEditing testTextEditing;
-    late SemanticsTextEditingStrategy strategy;
-
-    setUp(() {
-      testTextEditing = HybridTextEditing();
-      SemanticsTextEditingStrategy.ensureInitialized(testTextEditing);
-      strategy = SemanticsTextEditingStrategy.instance;
-      testTextEditing.debugTextEditingStrategyOverride = strategy;
-      testTextEditing.configuration = singlelineConfig;
-      ui_web.browser.debugBrowserEngineOverride = ui_web.BrowserEngine.webkit;
-      ui_web.browser.debugOperatingSystemOverride = ui_web.OperatingSystem.iOs;
-      semantics()
-        ..debugOverrideTimestampFunction(() => _testTime)
-        ..semanticsEnabled = true;
-    });
-
-    tearDown(() {
-      ui_web.browser.debugBrowserEngineOverride = null;
-      ui_web.browser.debugOperatingSystemOverride = null;
-      semantics().semanticsEnabled = false;
-    });
-
-    test('does not render a text field', () {
-      expect(owner().semanticsHost.querySelector('flt-semantics[role="textbox"]'), isNull);
-      createTextFieldSemanticsForIos(value: 'hello');
-      expect(owner().semanticsHost.querySelector('flt-semantics[role="textbox"]'), isNotNull);
-    });
-
-    test('tap detection works', () async {
-      final SemanticsActionLogger logger = SemanticsActionLogger();
-      createTextFieldSemanticsForIos(value: 'hello');
-
-      final DomElement textField = owner().semanticsHost
-          .querySelector('flt-semantics[role="textbox"]')!;
-
-      simulateTap(textField);
-      expect(await logger.idLog.first, 0);
-      expect(await logger.actionLog.first, ui.SemanticsAction.tap);
-    });
-
-    test('Syncs semantic state from framework', () {
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
-
-      int changeCount = 0;
-      int actionCount = 0;
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {
-          changeCount++;
-        },
-        onAction: (_) {
-          actionCount++;
-        },
-      );
-
-      // Create
-      final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
-        value: 'hello',
-        label: 'greeting',
-        isFocused: true,
-        rect: const ui.Rect.fromLTWH(0, 0, 10, 15),
-      );
-
-      final TextField textField = textFieldSemantics.primaryRole! as TextField;
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
-      expect(textField.editableElement, strategy.domElement);
-      expect(textField.activeEditableElement.getAttribute('aria-label'), 'greeting');
-      expect(textField.activeEditableElement.style.width, '10px');
-      expect(textField.activeEditableElement.style.height, '15px');
-
-      // Update
-      createTextFieldSemanticsForIos(
-        value: 'bye',
-        label: 'farewell',
-        rect: const ui.Rect.fromLTWH(0, 0, 12, 17),
-      );
-      final DomElement textBox =
-          owner().semanticsHost.querySelector('flt-semantics[role="textbox"]')!;
-
-      expect(strategy.domElement, null);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, textBox);
-      expect(textBox.getAttribute('aria-label'), 'farewell');
-
-      strategy.disable();
-
-      // There was no user interaction with the <input> element,
-      // so we should expect no engine-to-framework feedback.
-      expect(changeCount, 0);
-      expect(actionCount, 0);
-    });
-
-    test(
-        'Does not overwrite text value and selection editing state on semantic updates',
-        () {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
-          value: 'hello',
-          textSelectionBase: 1,
-          textSelectionExtent: 3,
-          isFocused: true,
-          rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
-
-      final TextField textField = textFieldSemantics.primaryRole! as TextField;
-      final DomHTMLInputElement editableElement =
-          textField.activeEditableElement as DomHTMLInputElement;
-
-      expect(editableElement, strategy.domElement);
-      expect(editableElement.value, '');
-      expect(editableElement.selectionStart, 0);
-      expect(editableElement.selectionEnd, 0);
-
-      strategy.disable();
-    });
-
-    test(
-        'Updates editing state when receiving framework messages from the text input channel',
-        () {
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
-
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
-          value: 'hello',
-          textSelectionBase: 1,
-          textSelectionExtent: 3,
-          isFocused: true,
-          rect: const ui.Rect.fromLTWH(0, 0, 10, 15));
-
-      final TextField textField = textFieldSemantics.primaryRole! as TextField;
-      final DomHTMLInputElement editableElement =
-          textField.activeEditableElement 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), testTextEditing);
-
-      // Editing state should now be updated
-      // expect(editableElement.value, 'updated');
-      expect(editableElement.selectionStart, 2);
-      expect(editableElement.selectionEnd, 3);
-
-      strategy.disable();
-    });
-
-    test('Gives up focus after DOM blur', () {
-      expect(owner().semanticsHost.ownerDocument?.activeElement, domDocument.body);
-
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-      final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-      );
-
-      final TextField textField = textFieldSemantics.primaryRole! as TextField;
-      expect(textField.editableElement, strategy.domElement);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
-
-      // The input should not refocus after blur.
-      textField.activeEditableElement.blur();
-      final DomElement textBox =
-          owner().semanticsHost.querySelector('flt-semantics[role="textbox"]')!;
-      expect(owner().semanticsHost.ownerDocument?.activeElement, textBox);
-
-      strategy.disable();
-    });
-
-    test('Disposes and recreates dom elements in persistent mode', () {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      // It doesn't create a new DOM element.
-      expect(strategy.domElement, isNull);
-
-      // During the semantics update the DOM element is created and is focused on.
-      final SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-      );
-      expect(strategy.domElement, isNotNull);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
-
-      strategy.disable();
-      expect(strategy.domElement, isNull);
-
-      // It removes the DOM element.
-      final TextField textField = textFieldSemantics.primaryRole! as TextField;
-      expect(owner().semanticsHost.contains(textField.editableElement), isFalse);
-      // Editing element is not enabled.
-      expect(strategy.isEnabled, isFalse);
-      // Focus is on the semantic object
-      final DomElement textBox =
-          owner().semanticsHost.querySelector('flt-semantics[role="textbox"]')!;
-      expect(owner().semanticsHost.ownerDocument?.activeElement, textBox);
-    });
-
-    test('Refocuses when setting editing state', () {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-      );
-      expect(strategy.domElement, isNotNull);
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
-
-      // Blur the element without telling the framework.
-      strategy.activeDomElement.blur();
-      final DomElement textBox =
-          owner().semanticsHost.querySelector('flt-semantics[role="textbox"]')!;
-      expect(owner().semanticsHost.ownerDocument?.activeElement, textBox);
-
-      // The input will have focus after editing state is set and semantics updated.
-      strategy.setEditingState(EditingState(text: 'foo'));
-
-      // NOTE: at this point some browsers, e.g. some versions of Safari will
-      //       have set the focus on the editing element as a result of setting
-      //       the test selection range. Other browsers require an explicit call
-      //       to `element.focus()` for the element to acquire focus. So far,
-      //       this discrepancy hasn't caused issues, so we're not checking for
-      //       any particular focus state between setEditingState and
-      //       createTextFieldSemantics. However, this is something for us to
-      //       keep in mind in case this causes issues in the future.
-
-      createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-      );
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
-
-      strategy.disable();
-    });
-
-    test('Works in multi-line mode', () {
-      strategy.enable(
-        multilineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-      createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-        isMultiline: true,
-      );
-
-      final DomHTMLTextAreaElement textArea =
-          strategy.domElement! as DomHTMLTextAreaElement;
-      expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement);
-
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      expect(owner().semanticsHost.contains(textArea), isTrue);
-
-      textArea.blur();
-      final DomElement textBox =
-          owner().semanticsHost.querySelector('flt-semantics[role="textbox"]')!;
-
-      expect(owner().semanticsHost.ownerDocument?.activeElement, textBox);
-
-      strategy.disable();
-      // It removes the textarea from the DOM.
-      expect(owner().semanticsHost.contains(textArea), isFalse);
-      // Editing element is not enabled.
-      expect(strategy.isEnabled, isFalse);
-    });
-
-    test('Does not position or size its DOM element', () {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      // Send width and height that are different from semantics values on
-      // purpose.
-      final Matrix4 transform = Matrix4.translationValues(14, 15, 0);
-      final EditableTextGeometry geometry = EditableTextGeometry(
-        height: 12,
-        width: 13,
-        globalTransform: transform.storage,
-      );
-      const ui.Rect semanticsRect = ui.Rect.fromLTRB(0, 0, 100, 50);
-
-      testTextEditing.acceptCommand(
-        TextInputSetEditableSizeAndTransform(geometry: geometry),
-        () {},
-      );
-
-      createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-      );
-
-      // Checks that the placement attributes come from semantics and not from
-      // EditableTextGeometry.
-      void checkPlacementIsSetBySemantics() {
-        expect(strategy.activeDomElement.style.transform,
-            isNot(equals(transform.toString())));
-        expect(strategy.activeDomElement.style.width, '${semanticsRect.width}px');
-        expect(strategy.activeDomElement.style.height, '${semanticsRect.height}px');
-      }
-
-      checkPlacementIsSetBySemantics();
-      strategy.placeElement();
-      checkPlacementIsSetBySemantics();
-    });
-
-    test('Changes focus from one text field to another through a semantics update', () {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-
-      // Switch between the two fields a few times.
-      for (int i = 0; i < 1; i++) {
-        final SemanticsTester tester = SemanticsTester(owner());
-        createTwoFieldSemanticsForIos(tester, focusFieldId: 1);
-
-        expect(tester.apply().length, 3);
-        expect(owner().semanticsHost.ownerDocument?.activeElement,
-            tester.getTextField(1).editableElement);
-        expect(strategy.domElement, tester.getTextField(1).editableElement);
-
-        createTwoFieldSemanticsForIos(tester, focusFieldId: 2);
-        expect(tester.apply().length, 3);
-        expect(owner().semanticsHost.ownerDocument?.activeElement,
-            tester.getTextField(2).editableElement);
-        expect(strategy.domElement, tester.getTextField(2).editableElement);
-      }
-    });
-
-    test('input transform is correct', () async {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-      createTextFieldSemanticsForIos(
-        value: 'hello',
-        isFocused: true,
-        );
-      expect(strategy.activeDomElement.style.transform, 'translate(${offScreenOffset}px, ${offScreenOffset}px)');
-      // See [_delayBeforePlacement].
-      await Future<void>.delayed(const Duration(milliseconds: 120) , (){});
-      expect(strategy.activeDomElement.style.transform, '');
-    });
-
-    test('disposes the editable element, if there is one', () {
-      strategy.enable(
-        singlelineConfig,
-        onChange: (_, __) {},
-        onAction: (_) {},
-      );
-      SemanticsObject textFieldSemantics = createTextFieldSemanticsForIos(
-        value: 'hello',
-      );
-      TextField textField = textFieldSemantics.primaryRole! as TextField;
-      expect(textField.editableElement, isNull);
-      textField.dispose();
-      expect(textField.editableElement, isNull);
-
-      textFieldSemantics = createTextFieldSemanticsForIos(
-        value: 'hi',
-        isFocused: true,
-      );
-      textField = textFieldSemantics.primaryRole! as TextField;
-
-      expect(textField.editableElement, isNotNull);
-      textField.dispose();
-      expect(textField.editableElement, isNull);
-    });
-  }, skip: !isSafari);
+  });
 }
 
-
 SemanticsObject createTextFieldSemantics({
   required String value,
   String label = '',
+  bool isEnabled = true,
   bool isFocused = false,
   bool isMultiline = false,
   ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
@@ -892,135 +540,24 @@
 }) {
   final SemanticsTester tester = SemanticsTester(owner());
   tester.updateNode(
-    id: 0,
-    label: label,
-    value: value,
-    isTextField: true,
-    isFocused: isFocused,
-    isMultiline: isMultiline,
-    hasTap: true,
-    rect: rect,
-    textDirection: ui.TextDirection.ltr,
-    textSelectionBase: textSelectionBase,
-    textSelectionExtent: textSelectionExtent
-  );
+      id: 0,
+      isEnabled: isEnabled,
+      label: label,
+      value: value,
+      isTextField: true,
+      isFocused: isFocused,
+      isMultiline: isMultiline,
+      hasTap: true,
+      rect: rect,
+      textDirection: ui.TextDirection.ltr,
+      textSelectionBase: textSelectionBase,
+      textSelectionExtent: textSelectionExtent);
   tester.apply();
   return tester.getSemanticsObject(0);
 }
 
-void simulateTap(DomElement element) {
-  element.dispatchEvent(createDomPointerEvent(
-    'pointerdown',
-    <Object?, Object?>{
-      'clientX': 125,
-      'clientY': 248,
-    },
-  ));
-  element.dispatchEvent(createDomPointerEvent(
-    'pointerup',
-    <Object?, Object?>{
-      'clientX': 126,
-      'clientY': 248,
-    },
-  ));
-}
-
-/// An editable DOM element won't be created on iOS unless a tap is detected.
-/// This function mimics the workflow by simulating a tap and sending a second
-/// semantic update.
-SemanticsObject createTextFieldSemanticsForIos({
-  required String value,
-  String label = '',
-  bool isFocused = false,
-  bool isMultiline = false,
-  ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
-  int textSelectionBase = 0,
-  int textSelectionExtent = 0,
-}) {
-  final SemanticsObject textFieldSemantics = createTextFieldSemantics(
-    value: value,
-    isFocused: isFocused,
-    label: label,
-    isMultiline: isMultiline,
-    rect: rect,
-    textSelectionBase: textSelectionBase,
-    textSelectionExtent: textSelectionExtent,
-  );
-
-  if (isFocused) {
-    final TextField textField = textFieldSemantics.primaryRole! as TextField;
-
-    simulateTap(textField.semanticsObject.element);
-
-    return createTextFieldSemantics(
-      value: value,
-      isFocused: isFocused,
-      label: label,
-      isMultiline: isMultiline,
-      rect: rect,
-      textSelectionBase: textSelectionBase,
-      textSelectionExtent: textSelectionExtent,
-    );
-  }
-  return textFieldSemantics;
-}
-
-/// See [createTextFieldSemanticsForIos].
-Map<int, SemanticsObject> createTwoFieldSemanticsForIos(SemanticsTester builder,
-    {int? focusFieldId}) {
-  builder.updateNode(
-    id: 0,
-    children: <SemanticsNodeUpdate>[
-      builder.updateNode(
-        id: 1,
-        isTextField: true,
-        value: 'Hello',
-        label: 'Hello',
-        isFocused: false,
-        rect: const ui.Rect.fromLTWH(0, 0, 10, 10),
-      ),
-      builder.updateNode(
-        id: 2,
-        isTextField: true,
-        value: 'World',
-        label: 'World',
-        isFocused: false,
-        rect: const ui.Rect.fromLTWH(20, 20, 10, 10),
-      ),
-    ],
-  );
-  builder.apply();
-  final String label = focusFieldId == 1 ? 'Hello' : 'World';
-  final DomElement textBox =
-      owner().semanticsHost.querySelector('flt-semantics[aria-label="$label"]')!;
-
-  simulateTap(textBox);
-
-  builder.updateNode(
-    id: 0,
-    children: <SemanticsNodeUpdate>[
-      builder.updateNode(
-        id: 1,
-        isTextField: true,
-        value: 'Hello',
-        label: 'Hello',
-        isFocused: focusFieldId == 1,
-        rect: const ui.Rect.fromLTWH(0, 0, 10, 10),
-      ),
-      builder.updateNode(
-        id: 2,
-        isTextField: true,
-        value: 'World',
-        label: 'World',
-        isFocused: focusFieldId == 2,
-        rect: const ui.Rect.fromLTWH(20, 20, 10, 10),
-      ),
-    ],
-  );
-  return builder.apply();
-}
-
 /// Emulates sending of a message by the framework to the engine.
-void sendFrameworkMessage(ByteData? message, HybridTextEditing testTextEditing) {
+void sendFrameworkMessage(
+    ByteData? message, HybridTextEditing testTextEditing) {
   testTextEditing.channel.handleTextInput(message, (ByteData? data) {});
 }