diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart
index b11e9e97a7458..622cf3f2a2dd1 100644
--- a/lib/web_ui/lib/src/engine/dom.dart
+++ b/lib/web_ui/lib/src/engine/dom.dart
@@ -658,7 +658,16 @@ extension DomElementExtension on DomElement {
   external JSNumber? get _tabIndex;
   double? get tabIndex => _tabIndex?.toDartDouble;
 
-  external JSVoid focus();
+  @JS('focus')
+  external JSVoid _focus(JSAny options);
+
+  void focus({bool? preventScroll, bool? focusVisible}) {
+    final Map<String, bool> options = <String, bool>{
+      if (preventScroll != null) 'preventScroll': preventScroll,
+      if (focusVisible != null) 'focusVisible': focusVisible,
+    };
+    _focus(options.toJSAnyDeep);
+  }
 
   @JS('scrollTop')
   external JSNumber get _scrollTop;
@@ -2249,9 +2258,11 @@ extension DomKeyboardEventExtension on DomKeyboardEvent {
   external JSBoolean? get _repeat;
   bool? get repeat => _repeat?.toDart;
 
+  // Safari injects synthetic keyboard events after auto-complete that don't
+  // have a `shiftKey` attribute, so this property must be nullable.
   @JS('shiftKey')
-  external JSBoolean get _shiftKey;
-  bool get shiftKey => _shiftKey.toDart;
+  external JSBoolean? get _shiftKey;
+  bool? get shiftKey => _shiftKey?.toDart;
 
   @JS('isComposing')
   external JSBoolean get _isComposing;
diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart
index 85bea97039e0a..f70456e42239c 100644
--- a/lib/web_ui/lib/src/engine/keyboard_binding.dart
+++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart
@@ -207,7 +207,7 @@ class FlutterHtmlKeyboardEvent {
   num? get timeStamp => _event.timeStamp;
   bool get altKey => _event.altKey;
   bool get ctrlKey => _event.ctrlKey;
-  bool get shiftKey => _event.shiftKey;
+  bool get shiftKey => _event.shiftKey ?? false;
   bool get metaKey => _event.metaKey;
   bool get isComposing => _event.isComposing;
 
diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart b/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart
index 3a10c4ab723c9..b3344099f7bd4 100644
--- a/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart
+++ b/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart
@@ -16,7 +16,7 @@ final class ViewFocusBinding {
   ///
   /// DO NOT rely on this bit as it will go away soon. You're warned :)!
   @visibleForTesting
-  static bool isEnabled = false;
+  static bool isEnabled = true;
 
   final FlutterViewManager _viewManager;
   final ui.ViewFocusChangeCallback _onViewFocusChange;
@@ -51,7 +51,7 @@ final class ViewFocusBinding {
     if (state == ui.ViewFocusState.focused) {
       // Only move the focus to the flutter view if nothing inside it is focused already.
       if (viewId != _viewId(domDocument.activeElement)) {
-        viewElement?.focus();
+        viewElement?.focus(preventScroll: true);
       }
     } else {
       viewElement?.blur();
@@ -70,7 +70,7 @@ final class ViewFocusBinding {
 
   late final DomEventListener _handleKeyDown = createDomEventListener((DomEvent event) {
     event as DomKeyboardEvent;
-    if (event.shiftKey) {
+    if (event.shiftKey ?? false) {
       _viewFocusDirection = ui.ViewFocusDirection.backward;
     }
   });
diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart
index f0b2b75c8e521..741761c434515 100644
--- a/lib/web_ui/lib/src/engine/pointer_binding.dart
+++ b/lib/web_ui/lib/src/engine/pointer_binding.dart
@@ -982,6 +982,22 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
         );
       _convertEventsToPointerData(data: pointerData, event: event, details: down);
       _callback(event, pointerData);
+
+      if (event.target == _viewTarget) {
+        // Ensure smooth focus transitions between text fields within the Flutter view.
+        // Without preventing the default and this delay, the engine may not have fully
+        // rendered the next input element, leading to the focus incorrectly returning to
+        // the main Flutter view instead.
+        // A zero-length timer is sufficient in all tested browsers to achieve this.
+        event.preventDefault();
+        Timer(Duration.zero, () {
+          EnginePlatformDispatcher.instance.requestViewFocusChange(
+            viewId: _view.viewId,
+            state: ui.ViewFocusState.focused,
+            direction: ui.ViewFocusDirection.undefined,
+          );
+        });
+      }
     });
 
     // Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
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 da49e877b2d7c..98b3400c5032d 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
@@ -332,7 +332,8 @@ class EngineAutofillForm {
 
     // In order to submit the form when Framework sends a `TextInput.commit`
     // message, we add a submit button to the form.
-    final DomHTMLInputElement submitButton = createDomHTMLInputElement();
+    // The -1 tab index value makes this element not reachable by keyboard.
+    final DomHTMLInputElement submitButton = createDomHTMLInputElement()..tabIndex = -1;
     _styleAutofillElements(submitButton, isOffScreen: true);
     submitButton.className = 'submitBtn';
     submitButton.type = 'submit';
@@ -1130,8 +1131,8 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy {
       // only after placing it to the correct position. Hence autofill menu
       // does not appear on top-left of the page.
       // Refocus on the elements after applying the geometry.
-      focusedFormElement!.focus();
-      activeDomElement.focus();
+      focusedFormElement!.focus(preventScroll: true);
+      moveFocusToActiveDomElement();
     }
   }
 }
@@ -1157,42 +1158,20 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
   ///
   /// This method is similar to the [GloballyPositionedTextEditingStrategy].
   /// The only part different: this method does not call `super.placeElement()`,
-  /// which in current state calls `domElement.focus()`.
+  /// which in current state calls `domElement.focus(preventScroll: true)`.
   ///
   /// Making an extra `focus` request causes flickering in Safari.
   @override
   void placeElement() {
     geometry?.applyToDomElement(activeDomElement);
     if (hasAutofillGroup) {
-      // We listen to pointerdown events on the Flutter View element and programatically
-      // focus our inputs. However, these inputs are focused before the pointerdown
-      // events conclude. Thus, the browser triggers a blur event immediately after
-      // focusing these inputs. This causes issues with Safari Desktop's autofill
-      // dialog (ref: https://github.com/flutter/flutter/issues/127960).
-      // In order to guarantee that we only focus after the pointerdown event concludes,
-      // we wrap the form autofill placement and focus logic in a zero-duration Timer.
-      // This ensures that our input doesn't have instantaneous focus/blur events
-      // occur on it and fixes the autofill dialog bug as a result.
-      Timer(Duration.zero, () {
-        placeForm();
-        // On Safari Desktop, when a form is focused, it opens an autofill menu
-        // immediately.
-        // Flutter framework sends `setEditableSizeAndTransform` for informing
-        // the engine about the location of the text field. This call may arrive
-        // after the first `show` call, depending on the text input widget's
-        // implementation. Therefore form is placed, when
-        // `setEditableSizeAndTransform` method is called and focus called on the
-        // form only after placing it to the correct position and only once after
-        // that. Calling focus multiple times causes flickering.
-        focusedFormElement!.focus();
-
-        // Set the last editing state if it exists, this is critical for a
-        // users ongoing work to continue uninterrupted when there is an update to
-        // the transform.
-        // If domElement is not focused cursor location will not be correct.
-        activeDomElement.focus();
-        lastEditingState?.applyToDomElement(activeDomElement);
-      });
+      placeForm();
+      // Set the last editing state if it exists, this is critical for a
+      // users ongoing work to continue uninterrupted when there is an update to
+      // the transform.
+      // If domElement is not focused cursor location will not be correct.
+      moveFocusToActiveDomElement();
+      lastEditingState?.applyToDomElement(activeDomElement);
     }
   }
 
@@ -1201,7 +1180,7 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
     if (geometry != null) {
       placeElement();
     }
-    activeDomElement.focus();
+    moveFocusToActiveDomElement();
   }
 }
 
@@ -1248,6 +1227,12 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
     return domElement!;
   }
 
+  /// The [FlutterView] in which [activeDomElement] is contained.
+  EngineFlutterView? get _activeDomElementView => _viewForElement(activeDomElement);
+
+  EngineFlutterView? _viewForElement(DomElement element) =>
+    EnginePlatformDispatcher.instance.viewManager.findViewForElement(element);
+
   late InputConfiguration inputConfiguration;
   EditingState? lastEditingState;
 
@@ -1285,7 +1270,8 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
   }) {
     assert(!isEnabled);
 
-    domElement = inputConfig.inputType.createDomElement();
+    // The -1 tab index value makes this element not reachable by keyboard.
+    domElement = inputConfig.inputType.createDomElement()..tabIndex = -1;
     applyConfiguration(inputConfig);
 
     _setStaticStyleAttributes(activeDomElement);
@@ -1363,15 +1349,16 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
     subscriptions.add(DomSubscription(domDocument, 'selectionchange',
             handleChange));
 
-    activeDomElement.addEventListener('beforeinput',
-        createDomEventListener(handleBeforeInput));
+    subscriptions.add(DomSubscription(activeDomElement, 'beforeinput',
+            handleBeforeInput));
 
-    addCompositionEventHandlers(activeDomElement);
+    if (this is! SafariDesktopTextEditingStrategy) {
+      // handleBlur causes Safari to reopen autofill dialogs after autofill,
+      // so we don't attach the listener there.
+      subscriptions.add(DomSubscription(activeDomElement, 'blur', handleBlur));
+    }
 
-    // Refocus on the activeDomElement after blur, so that user can keep editing the
-    // text field.
-    subscriptions.add(DomSubscription(activeDomElement, 'blur',
-            (_) { activeDomElement.focus(); }));
+    addCompositionEventHandlers(activeDomElement);
 
     preventDefaultForMouseEvents();
   }
@@ -1422,13 +1409,12 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
     // More details on `TextInput.finishAutofillContext` call.
     if (_appendedToForm &&
         inputConfiguration.autofillGroup?.formElement != null) {
-      // Subscriptions are removed, listeners won't be triggered.
-      activeDomElement.blur();
       _styleAutofillElements(activeDomElement, isOffScreen: true);
       inputConfiguration.autofillGroup?.storeForm();
+      _moveFocusToFlutterView(activeDomElement, _activeDomElementView);
     } else {
-      activeDomElement.remove();
-    }
+      _moveFocusToFlutterView(activeDomElement, _activeDomElementView, removeElement: true);
+		}
     domElement = null;
   }
 
@@ -1442,7 +1428,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
   }
 
   void placeElement() {
-    activeDomElement.focus();
+    moveFocusToActiveDomElement();
   }
 
   void placeForm() {
@@ -1508,6 +1494,15 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
     }
   }
 
+  void handleBlur(DomEvent event) {
+    event as DomFocusEvent;
+
+    final DomElement? willGainFocusElement = event.relatedTarget as DomElement?;
+    if (willGainFocusElement == null || _viewForElement(willGainFocusElement) == _activeDomElementView) {
+      moveFocusToActiveDomElement();
+    }
+  }
+
   void maybeSendAction(DomEvent e) {
     if (domInstanceOfString(e, 'KeyboardEvent')) {
       final DomKeyboardEvent event = e as DomKeyboardEvent;
@@ -1545,7 +1540,7 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
     }
 
     // Re-focuses after setting editing state.
-    activeDomElement.focus();
+    moveFocusToActiveDomElement();
   }
 
   /// Prevent default behavior for mouse down, up and move.
@@ -1572,6 +1567,31 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
       event.preventDefault();
     }));
   }
+
+  /// Moves the focus to the [activeDomElement].
+  void moveFocusToActiveDomElement() {
+    activeDomElement.focus(preventScroll: true);
+  }
+
+  /// Moves the focus to the [EngineFlutterView].
+  ///
+  /// 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(
+    DomElement element,
+    EngineFlutterView? view, {
+    bool removeElement = false,
+  }) {
+    Timer(Duration.zero, () {
+      if (element == domDocument.activeElement) {
+        view?.dom.rootElement.focus(preventScroll: true);
+      }
+      if (removeElement) {
+        element.remove();
+      }
+    });
+  }
 }
 
 /// IOS/Safari behaviour for text editing.
@@ -1605,17 +1625,6 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
   Timer? _positionInputElementTimer;
   static const Duration _delayBeforePlacement = Duration(milliseconds: 100);
 
-  /// This interval between the blur subscription and callback is considered to
-  /// be fast.
-  ///
-  /// This is only used for iOS. The blur callback may trigger as soon as the
-  /// creation of the subscription. Occasionally in this case, the virtual
-  /// keyboard will quickly show and hide again.
-  ///
-  /// Less than this interval allows the virtual keyboard to keep showing up
-  /// instead of hiding rapidly.
-  static const Duration _blurFastCallbackInterval = Duration(milliseconds: 200);
-
   /// Whether or not the input element can be positioned at this point in time.
   ///
   /// This is currently only used in iOS. It's set to false before focusing the
@@ -1671,8 +1680,11 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
     subscriptions.add(DomSubscription(domDocument, 'selectionchange',
             handleChange));
 
-    activeDomElement.addEventListener('beforeinput',
-        createDomEventListener(handleBeforeInput));
+    subscriptions.add(DomSubscription(activeDomElement, 'beforeinput',
+            handleBeforeInput));
+
+    subscriptions.add(DomSubscription(activeDomElement, 'blur',
+            handleBlur));
 
     addCompositionEventHandlers(activeDomElement);
 
@@ -1684,35 +1696,6 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
             }));
 
     _addTapListener();
-
-    // Record start time of blur subscription.
-    final Stopwatch blurWatch = Stopwatch()..start();
-
-    // On iOS, blur is trigerred in the following cases:
-    //
-    // 1. The browser app is sent to the background (or the tab is changed). In
-    //    this case, the window loses focus (see [windowHasFocus]),
-    //    so we close the input connection with the framework.
-    // 2. The user taps on another focusable element. In this case, we refocus
-    //    the input field and wait for the framework to manage the focus change.
-    // 3. The virtual keyboard is closed by tapping "done". We can't detect this
-    //    programmatically, so we end up refocusing the input field. This is
-    //    okay because the virtual keyboard will hide, and as soon as the user
-    //    taps the text field again, the virtual keyboard will come up.
-    // 4. Safari sometimes sends a blur event immediately after activating the
-    //    input field. In this case, we want to keep the focus on the input field.
-    //    In order to detect this, we measure how much time has passed since the
-    //    input field was activated. If the time is too short, we re-focus the
-    //    input element.
-    subscriptions.add(DomSubscription(activeDomElement, 'blur',
-            (_) {
-              final bool isFastCallback = blurWatch.elapsed < _blurFastCallbackInterval;
-              if (windowHasFocus && isFastCallback) {
-                activeDomElement.focus();
-              } else {
-                owner.sendTextConnectionClosedToFrameworkIfAny();
-              }
-            }));
   }
 
   @override
@@ -1772,7 +1755,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
 
   @override
   void placeElement() {
-    activeDomElement.focus();
+    moveFocusToActiveDomElement();
     geometry?.applyToDomElement(activeDomElement);
   }
 }
@@ -1824,31 +1807,20 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
         DomSubscription(domDocument, 'selectionchange',
             handleChange));
 
-    activeDomElement.addEventListener('beforeinput',
-        createDomEventListener(handleBeforeInput));
+    subscriptions.add(DomSubscription(activeDomElement, 'beforeinput',
+        handleBeforeInput));
 
-    addCompositionEventHandlers(activeDomElement);
+    subscriptions.add(DomSubscription(activeDomElement, 'blur',
+            handleBlur));
 
-    subscriptions.add(
-        DomSubscription(activeDomElement, 'blur',
-            (_) {
-              if (windowHasFocus) {
-                // Chrome on Android will hide the onscreen keyboard when you tap outside
-                // the text box. Instead, we want the framework to tell us to hide the
-                // keyboard via `TextInput.clearClient` or `TextInput.hide`. Therefore
-                // refocus as long as [windowHasFocus] is true.
-                activeDomElement.focus();
-              } else {
-                owner.sendTextConnectionClosedToFrameworkIfAny();
-              }
-            }));
+    addCompositionEventHandlers(activeDomElement);
 
     preventDefaultForMouseEvents();
   }
 
   @override
   void placeElement() {
-    activeDomElement.focus();
+    moveFocusToActiveDomElement();
     geometry?.applyToDomElement(activeDomElement);
   }
 }
@@ -1888,8 +1860,9 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
         DomSubscription(
             activeDomElement, 'keydown', maybeSendAction));
 
-    activeDomElement.addEventListener('beforeinput',
-        createDomEventListener(handleBeforeInput));
+    subscriptions.add(
+        DomSubscription(
+            activeDomElement, 'beforeinput', handleBeforeInput));
 
     addCompositionEventHandlers(activeDomElement);
 
@@ -1921,32 +1894,15 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
         DomSubscription(
             activeDomElement, 'select', handleChange));
 
-    // Refocus on the activeDomElement after blur, so that user can keep editing the
-    // text field.
-    subscriptions.add(
-        DomSubscription(
-            activeDomElement,
-            'blur',
-            (_) {
-              _postponeFocus();
-            }));
+    subscriptions.add(DomSubscription(activeDomElement, 'blur',
+            handleBlur));
 
     preventDefaultForMouseEvents();
   }
 
-  void _postponeFocus() {
-    // Firefox does not focus on the editing element if we call the focus
-    // inside the blur event, therefore we postpone the focus.
-    // Calling focus inside a Timer for `0` milliseconds guarantee that it is
-    // called after blur event propagation is completed.
-    Timer(Duration.zero, () {
-      activeDomElement.focus();
-    });
-  }
-
   @override
   void placeElement() {
-    activeDomElement.focus();
+    moveFocusToActiveDomElement();
     geometry?.applyToDomElement(activeDomElement);
     // Set the last editing state if it exists, this is critical for a
     // users ongoing work to continue uninterrupted when there is an update to
diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart
index ac93b4c0c0657..49b76226a6c87 100644
--- a/lib/web_ui/test/engine/text_editing_test.dart
+++ b/lib/web_ui/test/engine/text_editing_test.dart
@@ -25,6 +25,9 @@ EnginePlatformDispatcher get dispatcher => EnginePlatformDispatcher.instance;
 DomElement get defaultTextEditingRoot =>
     dispatcher.implicitView!.dom.textEditingHost;
 
+DomElement get implicitViewRootElement =>
+    dispatcher.implicitView!.dom.rootElement;
+
 /// Add unit tests for [FirefoxTextEditingStrategy].
 // TODO(mdebbar): https://github.com/flutter/flutter/issues/46891
 
@@ -67,13 +70,18 @@ Future<void> testMain() async {
     setUpTestViewDimensions: false
   );
 
-  tearDown(() {
+  setUp(() {
+    domDocument.activeElement?.blur();
+  });
+
+  tearDown(() async {
     lastEditingState = null;
     editingDeltaState = null;
     lastInputAction = null;
     cleanTextEditingStrategy();
     cleanTestFlags();
     clearBackUpDomElementIfExists();
+    await waitForTextStrategyStopPropagation();
   });
 
   group('$GloballyPositionedTextEditingStrategy', () {
@@ -86,13 +94,16 @@ Future<void> testMain() async {
       testTextEditing.configuration = singlelineConfig;
     });
 
-    test('Creates element when enabled and removes it when disabled', () {
+    test('Creates element when enabled and removes it when disabled', () async {
       expect(
         domDocument.getElementsByTagName('input'),
         hasLength(0),
       );
-      // The focus initially is on the body.
-      expect(domDocument.activeElement, domDocument.body);
+      expect(
+        domDocument.activeElement,
+        domDocument.body,
+        reason: 'The focus should initially be on the body',
+      );
       expect(defaultTextEditingRoot.ownerDocument?.activeElement,
           domDocument.body);
 
@@ -114,22 +125,24 @@ Future<void> testMain() async {
 
       expect(editingStrategy!.domElement, input);
       expect(input.getAttribute('type'), null);
+      expect(input.tabIndex, -1, reason: 'The input should not be reachable by keyboard');
 
       // Input is appended to the right point of the DOM.
       expect(defaultTextEditingRoot.contains(editingStrategy!.domElement), isTrue);
 
       editingStrategy!.disable();
+      await waitForTextStrategyStopPropagation();
       expect(
         defaultTextEditingRoot.querySelectorAll('input'),
         hasLength(0),
       );
-      // The focus is back to the body.
-      expect(domDocument.activeElement, domDocument.body);
+      // The focus is back to the flutter view.
+      expect(domDocument.activeElement, implicitViewRootElement);
       expect(defaultTextEditingRoot.ownerDocument?.activeElement,
-          domDocument.body);
+          implicitViewRootElement);
     });
 
-    test('inserts element in the correct view', () {
+    test('inserts element in the correct view', () async {
       final DomElement host = createDomElement('div');
       domDocument.body!.append(host);
       final EngineFlutterView view = EngineFlutterView(dispatcher, host);
@@ -152,6 +165,7 @@ Future<void> testMain() async {
 
       // Cleanup.
       editingStrategy!.disable();
+      await waitForTextStrategyStopPropagation();
       expect(textEditingHost.querySelectorAll('input'), hasLength(0));
       dispatcher.viewManager.unregisterView(view.viewId);
       view.dispose();
@@ -305,7 +319,7 @@ Future<void> testMain() async {
       expect(lastInputAction, isNull);
     });
 
-    test('Multi-line mode also works', () {
+    test('Multi-line mode also works', () async {
       // The textarea element is created lazily.
       expect(domDocument.getElementsByTagName('textarea'), hasLength(0));
       editingStrategy!.enable(
@@ -337,17 +351,23 @@ Future<void> testMain() async {
       checkTextAreaEditingState(textarea, 'bar\nbaz', 2, 7);
 
       editingStrategy!.disable();
+
+      await waitForTextStrategyStopPropagation();
+
       // The textarea should be cleaned up.
       expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0));
-      // The focus is back to the body.
-      expect(defaultTextEditingRoot.ownerDocument?.activeElement,
-          domDocument.body);
+
+      expect(
+        defaultTextEditingRoot.ownerDocument?.activeElement,
+        implicitViewRootElement,
+        reason: 'The focus should be back to the body',
+      );
 
       // There should be no input action.
       expect(lastInputAction, isNull);
     });
 
-    test('Same instance can be re-enabled with different config', () {
+    test('Same instance can be re-enabled with different config', () async {
       // Make sure there's nothing in the DOM yet.
       expect(domDocument.getElementsByTagName('input'), hasLength(0));
       expect(domDocument.getElementsByTagName('textarea'), hasLength(0));
@@ -363,6 +383,7 @@ Future<void> testMain() async {
 
       // Disable and check that all DOM elements were removed.
       editingStrategy!.disable();
+      await waitForTextStrategyStopPropagation();
       expect(defaultTextEditingRoot.querySelectorAll('input'), hasLength(0));
       expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0));
 
@@ -372,11 +393,13 @@ Future<void> testMain() async {
         onChange: trackEditingState,
         onAction: trackInputAction,
       );
+      await waitForTextStrategyStopPropagation();
       expect(defaultTextEditingRoot.querySelectorAll('input'), hasLength(0));
       expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(1));
 
       // Disable again and check that all DOM elements were removed.
       editingStrategy!.disable();
+      await waitForTextStrategyStopPropagation();
       expect(defaultTextEditingRoot.querySelectorAll('input'), hasLength(0));
       expect(defaultTextEditingRoot.querySelectorAll('textarea'), hasLength(0));
 
@@ -705,8 +728,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       checkInputEditingState(textEditing!.strategy.domElement, '', 0, 0);
 
       const MethodCall setEditingState =
@@ -723,8 +744,13 @@ Future<void> testMain() async {
       const MethodCall hide = MethodCall('TextInput.hide');
       sendFrameworkMessage(codec.encodeMethodCall(hide));
 
-      // Text editing should've stopped.
-      expect(domDocument.activeElement, domDocument.body);
+      await waitForTextStrategyStopPropagation();
+
+      expect(
+        domDocument.activeElement,
+        implicitViewRootElement,
+        reason: 'Text editing should have stopped',
+      );
 
       // Confirm that [HybridTextEditing] didn't send any messages.
       expect(spy.messages, isEmpty);
@@ -743,8 +769,11 @@ Future<void> testMain() async {
       });
       sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
 
-      // Editing shouldn't have started yet.
-      expect(domDocument.activeElement, domDocument.body);
+      expect(
+        domDocument.activeElement,
+        domDocument.body,
+        reason: 'Editing should not have started yet',
+      );
 
       const MethodCall show = MethodCall('TextInput.show');
       sendFrameworkMessage(codec.encodeMethodCall(show));
@@ -758,15 +787,19 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
 
       const MethodCall clearClient = MethodCall('TextInput.clearClient');
       sendFrameworkMessage(codec.encodeMethodCall(clearClient));
 
-      expect(domDocument.activeElement, domDocument.body);
+      await waitForTextStrategyStopPropagation();
+
+      expect(
+        domDocument.activeElement,
+        implicitViewRootElement,
+        reason: 'Text editing should have stopped',
+      );
 
       // Confirm that [HybridTextEditing] didn't send any messages.
       expect(spy.messages, isEmpty);
@@ -794,8 +827,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       const MethodCall setEditingState =
         MethodCall('TextInput.setEditingState', <String, dynamic>{
         'text': 'abcd',
@@ -863,9 +894,11 @@ Future<void> testMain() async {
       });
       sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
 
-      // Editing shouldn't have started yet.
-      expect(defaultTextEditingRoot.ownerDocument?.activeElement,
-          domDocument.body);
+      expect(
+        defaultTextEditingRoot.ownerDocument?.activeElement,
+        domDocument.body,
+        reason: 'Editing should not have started yet',
+      );
 
       const MethodCall show = MethodCall('TextInput.show');
       sendFrameworkMessage(codec.encodeMethodCall(show));
@@ -879,18 +912,14 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
       expect(textEditing!.isEditing, isTrue);
 
-      // DOM element is blurred.
-      textEditing!.strategy.domElement!.blur();
-
       // No connection close message sent.
       expect(spy.messages, hasLength(0));
       await Future<void>.delayed(Duration.zero);
+
       // DOM element still keeps the focus.
       expect(defaultTextEditingRoot.ownerDocument?.activeElement,
           textEditing!.strategy.domElement);
@@ -977,8 +1006,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
 
@@ -1035,8 +1062,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       // Form is added to DOM.
       expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
 
@@ -1091,8 +1116,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       // Form is added to DOM.
       expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
       final DomHTMLFormElement formElement =
@@ -1146,8 +1169,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       // Form is added to DOM.
       expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
       final DomHTMLFormElement formElement =
@@ -1174,50 +1195,57 @@ Future<void> testMain() async {
       expect(formsOnTheDom, hasLength(0));
     });
 
-    test('form is not placed and input is not focused until after tick on Desktop Safari', () async {
-      // Create a configuration with an AutofillGroup of four text fields.
-      final Map<String, dynamic> flutterMultiAutofillElementConfig =
-          createFlutterConfig('text',
-              autofillHint: 'username',
-              autofillHintsForFields: <String>[
-            'username',
-            'email',
-            'name',
-            'telephoneNumber'
-          ]);
-      final MethodCall setClient = MethodCall('TextInput.setClient',
-          <dynamic>[123, flutterMultiAutofillElementConfig]);
-      sendFrameworkMessage(codec.encodeMethodCall(setClient));
+    test('Moves the focus across input elements', () async {
+      final List<DomEvent> focusinEvents = <DomEvent>[];
+      final DomEventListener handleFocusIn = createDomEventListener(focusinEvents.add);
 
-      const MethodCall setEditingState1 =
+      final MethodCall setClient1 = MethodCall(
+        'TextInput.setClient',
+        <dynamic>[123, flutterSinglelineConfig],
+      );
+      final MethodCall setClient2 = MethodCall(
+        'TextInput.setClient',
+        <dynamic>[567, flutterSinglelineConfig],
+      );
+      const MethodCall setEditingState =
           MethodCall('TextInput.setEditingState', <String, dynamic>{
         'text': 'abcd',
         'selectionBase': 2,
         'selectionExtent': 3,
       });
-      sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
-
+      final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(
+        150,
+        50,
+        Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList(),
+      );
       const MethodCall show = MethodCall('TextInput.show');
-      sendFrameworkMessage(codec.encodeMethodCall(show));
+      const MethodCall clearClient = MethodCall('TextInput.clearClient');
 
-      final MethodCall setSizeAndTransform =
-          configureSetSizeAndTransformMethodCall(150, 50,
-              Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
+      domDocument.body!.addEventListener('focusin', handleFocusIn);
+      sendFrameworkMessage(codec.encodeMethodCall(setClient1));
+      sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
+      sendFrameworkMessage(codec.encodeMethodCall(show));
+      final DomElement firstInput = textEditing!.strategy.domElement!;
+      expect(domDocument.activeElement, firstInput);
 
-      // Prior to tick, form should not exist and no elements should be focused.
-      expect(defaultTextEditingRoot.querySelectorAll('form'), isEmpty);
-      expect(domDocument.activeElement, domDocument.body);
-
-      await waitForDesktopSafariFocus();
+      sendFrameworkMessage(codec.encodeMethodCall(setClient2));
+      sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
+      sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
+      sendFrameworkMessage(codec.encodeMethodCall(show));
+      final DomElement secondInput = textEditing!.strategy.domElement!;
+      expect(domDocument.activeElement, secondInput);
+      expect(firstInput, isNot(secondInput));
 
-      // Form is added to DOM.
-      expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
+      sendFrameworkMessage(codec.encodeMethodCall(clearClient));
+      await waitForTextStrategyStopPropagation();
+      domDocument.body!.removeEventListener('focusin', handleFocusIn);
 
-      final DomHTMLInputElement inputElement =
-          textEditing!.strategy.domElement! as DomHTMLInputElement;
-      expect(domDocument.activeElement, inputElement);
-    }, skip: !isSafari);
+      expect(focusinEvents, hasLength(3));
+      expect(focusinEvents[0].target, firstInput);
+      expect(focusinEvents[1].target, secondInput);
+      expect(focusinEvents[2].target, implicitViewRootElement);
+    });
 
     test('setClient, setEditingState, show, setClient', () async {
       final MethodCall setClient = MethodCall(
@@ -1232,8 +1260,11 @@ Future<void> testMain() async {
       });
       sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
 
-      // Editing shouldn't have started yet.
-      expect(domDocument.activeElement, domDocument.body);
+      expect(
+        domDocument.activeElement,
+        domDocument.body,
+        reason: 'Editing should not have started yet.',
+      );
 
       const MethodCall show = MethodCall('TextInput.show');
       sendFrameworkMessage(codec.encodeMethodCall(show));
@@ -1247,8 +1278,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
 
@@ -1256,9 +1285,13 @@ Future<void> testMain() async {
           'TextInput.setClient', <dynamic>[567, flutterSinglelineConfig]);
       sendFrameworkMessage(codec.encodeMethodCall(setClient2));
 
-      // Receiving another client via setClient should stop editing, hence
-      // should remove the previous active element.
-      expect(domDocument.activeElement, domDocument.body);
+      await waitForTextStrategyStopPropagation();
+
+      expect(
+        domDocument.activeElement,
+        implicitViewRootElement,
+        reason: 'Receiving another client via setClient should stop editing, hence should remove the previous active element.',
+      );
 
       // Confirm that [HybridTextEditing] didn't send any messages.
       expect(spy.messages, isEmpty);
@@ -1299,7 +1332,6 @@ Future<void> testMain() async {
       });
       sendFrameworkMessage(codec.encodeMethodCall(setEditingState2));
 
-      await waitForDesktopSafariFocus();
       // The second [setEditingState] should override the first one.
       checkInputEditingState(
           textEditing!.strategy.domElement, 'xyz', 0, 2);
@@ -1341,7 +1373,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
       // The second [setEditingState] should override the first one.
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
@@ -1412,7 +1443,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
       // Check the element still has focus. User can keep editing.
       expect(defaultTextEditingRoot.ownerDocument?.activeElement,
           textEditing!.strategy.domElement);
@@ -1468,8 +1498,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       // The second [setEditingState] should override the first one.
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
@@ -1780,8 +1808,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       // Check if the selection range is correct.
       checkInputEditingState(
           textEditing!.strategy.domElement, 'xyz', 1, 2);
@@ -1955,8 +1981,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       final DomHTMLInputElement input = textEditing!.strategy.domElement! as
           DomHTMLInputElement;
 
@@ -2030,8 +2054,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       final DomHTMLInputElement input = textEditing!.strategy.domElement! as
           DomHTMLInputElement;
 
@@ -2116,7 +2138,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
       // The second [setEditingState] should override the first one.
       checkInputEditingState(
           textEditing!.strategy.domElement, 'abcd', 2, 3);
@@ -2168,9 +2189,11 @@ Future<void> testMain() async {
           'TextInput.setClient', <dynamic>[123, flutterMultilineConfig]);
       sendFrameworkMessage(codec.encodeMethodCall(setClient));
 
-      // Editing shouldn't have started yet.
-      expect(defaultTextEditingRoot.ownerDocument?.activeElement,
-          domDocument.body);
+      expect(
+        defaultTextEditingRoot.ownerDocument?.activeElement,
+        domDocument.body,
+        reason: 'Editing should have not started yet',
+      );
 
       const MethodCall show = MethodCall('TextInput.show');
       sendFrameworkMessage(codec.encodeMethodCall(show));
@@ -2184,8 +2207,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       final DomHTMLTextAreaElement textarea = textEditing!.strategy.domElement!
           as DomHTMLTextAreaElement;
       checkTextAreaEditingState(textarea, '', 0, 0);
@@ -2253,8 +2274,13 @@ Future<void> testMain() async {
       const MethodCall hide = MethodCall('TextInput.hide');
       sendFrameworkMessage(codec.encodeMethodCall(hide));
 
-      // Text editing should've stopped.
-      expect(domDocument.activeElement, domDocument.body);
+      await waitForTextStrategyStopPropagation();
+
+      expect(
+        domDocument.activeElement,
+        implicitViewRootElement,
+        reason: 'Text editing should have stopped',
+      );
 
       // Confirm that [HybridTextEditing] didn't send any more messages.
       expect(spy.messages, isEmpty);
@@ -2277,8 +2303,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       expect(textEditing!.strategy.domElement!.tagName, 'INPUT');
       expect(getEditingInputMode(), 'none');
     });
@@ -2300,8 +2324,6 @@ Future<void> testMain() async {
               Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       expect(textEditing!.strategy.domElement!.tagName, 'TEXTAREA');
       expect(getEditingInputMode(), 'none');
     });
@@ -2538,11 +2560,8 @@ Future<void> testMain() async {
       final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       final DomElement input = textEditing!.strategy.domElement!;
 
-
       // Input is appended to the right view.
       expect(view.dom.textEditingHost.contains(input), isTrue);
 
@@ -2622,8 +2641,6 @@ Future<void> testMain() async {
       final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
       sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
 
-      await waitForDesktopSafariFocus();
-
       final DomElement input = textEditing!.strategy.domElement!;
       final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
 
@@ -2667,8 +2684,6 @@ Future<void> testMain() async {
       const MethodCall show = MethodCall('TextInput.show');
       sendFrameworkMessage(codec.encodeMethodCall(show));
 
-      await waitForDesktopSafariFocus();
-
       final DomElement input = textEditing!.strategy.domElement!;
       final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
 
@@ -2839,6 +2854,7 @@ Future<void> testMain() async {
       final DomHTMLInputElement inputElement = form.childNodes.toList()[0] as
           DomHTMLInputElement;
       expect(inputElement.type, 'submit');
+      expect(inputElement.tabIndex, -1, reason: 'The input should not be reachable by keyboard');
 
       // The submit button should have class `submitBtn`.
       expect(inputElement.className, 'submitBtn');
@@ -3494,6 +3510,8 @@ Future<void> testMain() async {
       );
 
       final DomHTMLElement input = editingStrategy!.activeDomElement;
+      expect(domDocument.activeElement, input, reason: 'the input element should be focused');
+
       expect(input.style.color, contains('transparent'));
       if (isSafari) {
         // macOS 13 returns different values than macOS 12.
@@ -3503,7 +3521,7 @@ Future<void> testMain() async {
       } else {
         expect(input.style.background, contains('transparent'));
         expect(input.style.outline, contains('none'));
-        expect(input.style.border, contains('none'));
+        expect(input.style.border, anyOf(contains('none'), contains('medium')));
       }
       expect(input.style.backgroundColor, contains('transparent'));
       expect(input.style.caretColor, contains('transparent'));
@@ -3707,13 +3725,9 @@ void clearForms() {
   formsOnTheDom.clear();
 }
 
-/// On Desktop Safari, the editing element is focused after a zero-duration timer
-/// to prevent autofill popup flickering. We must wait a tick for this placement
-/// before referencing these elements.
-Future<void> waitForDesktopSafariFocus() async {
-  if (textEditing.strategy is SafariDesktopTextEditingStrategy) {
-    await Future<void>.delayed(Duration.zero);
-  }
+/// Waits until the text strategy closes and moves the focus accordingly.
+Future<void> waitForTextStrategyStopPropagation() async {
+  await Future<void>.delayed(Duration.zero);
 }
 
 class GlobalTextEditingStrategySpy extends GloballyPositionedTextEditingStrategy {