diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index d3f422727885e..5fb335f11b728 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -339,6 +339,10 @@ extension DomHTMLDocumentExtension on DomHTMLDocument { @JS('visibilityState') external JSString get _visibilityState; String get visibilityState => _visibilityState.toDart; + + @JS('hasFocus') + external JSBoolean _hasFocus(); + bool hasFocus() => _hasFocus().toDart; } @JS('document') 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 54b71a9d07694..df176ed2453f2 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 @@ -64,6 +64,13 @@ final class ViewFocusBinding { }); late final DomEventListener _handleFocusout = createDomEventListener((DomEvent event) { + // Tipically when the browser handles focus events, blur happens first and then focusout. + // When blur is not prevented, by the time focusout is called, the document.activeElement is set to + // There are instances in the engine where in the middle of a blur event, focus is moved to another node. + if (domDocument.hasFocus() && domDocument.activeElement != domDocument.body) { + return; + } + event as DomFocusEvent; _handleFocusChange(event.relatedTarget as DomElement?); }); diff --git a/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart b/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart index 883176cef9235..e4f391a279bae 100644 --- a/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart +++ b/lib/web_ui/test/engine/platform_dispatcher/view_focus_binding_test.dart @@ -270,6 +270,31 @@ void testMain() { expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); }); + + test('works even if focus is changed within a focus change hook', () { + final DomElement input1 = createDomElement('input'); + final DomElement input2 = createDomElement('input'); + final EngineFlutterView view = createAndRegisterView(dispatcher); + final DomEventListener focusInput1Listener = createDomEventListener((DomEvent event) { + input1.focusWithoutScroll(); + }); + + view.dom.rootElement.append(input1); + view.dom.rootElement.append(input2); + + input1.addEventListener('blur', focusInput1Listener); + input1.focusWithoutScroll(); + // The event handler above should move the focus back to input1. + input2.focusWithoutScroll(); + input1.removeEventListener('blur', focusInput1Listener); + + expect(dispatchedViewFocusEvents, hasLength(1)); + + expect(dispatchedViewFocusEvents[0].viewId, view.viewId); + expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); + expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); + + }); }); }