diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 20588af1054f7..868ceab4c8dcd 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -257,7 +257,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { required ui.ViewFocusState state, required ui.ViewFocusDirection direction, }) { - // TODO(tugorez): implement this method. At the moment will be a no op call. + _viewFocusBinding.changeViewFocus(viewId, state); } /// A set of views which have rendered in the current `onBeginFrame` or 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 9f9852bd48ed8..1dc10b9fef846 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 @@ -34,6 +34,19 @@ final class ViewFocusBinding { _onViewCreatedListener?.cancel(); } + void changeViewFocus(int viewId, ui.ViewFocusState state) { + final DomElement? viewElement = _viewManager[viewId]?.dom.rootElement; + + 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(); + } + } else { + viewElement?.blur(); + } + } + late final DomEventListener _handleFocusin = createDomEventListener((DomEvent event) { event as DomFocusEvent; _handleFocusChange(event.target 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 8610d394fff90..419de687ac66e 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 @@ -164,6 +164,108 @@ void testMain() { expect(dispatchedViewFocusEvents[2].state, ui.ViewFocusState.unfocused); expect(dispatchedViewFocusEvents[2].direction, ui.ViewFocusDirection.undefined); }); + + test('requestViewFocusChange focuses the view', () { + final EngineFlutterView view = createAndRegisterView(dispatcher); + + dispatcher.requestViewFocusChange( + viewId: view.viewId, + state: ui.ViewFocusState.focused, + direction: ui.ViewFocusDirection.forward, + ); + + expect(domDocument.activeElement, view.dom.rootElement); + + expect(dispatchedViewFocusEvents, hasLength(1)); + + expect(dispatchedViewFocusEvents[0].viewId, view.viewId); + expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); + expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); + }); + + test('requestViewFocusChange blurs the view', () { + final EngineFlutterView view = createAndRegisterView(dispatcher); + + dispatcher.requestViewFocusChange( + viewId: view.viewId, + state: ui.ViewFocusState.focused, + direction: ui.ViewFocusDirection.forward, + ); + + dispatcher.requestViewFocusChange( + viewId: view.viewId, + state: ui.ViewFocusState.unfocused, + direction: ui.ViewFocusDirection.undefined, + ); + + expect(domDocument.activeElement, isNot(view.dom.rootElement)); + + expect(dispatchedViewFocusEvents, hasLength(2)); + + expect(dispatchedViewFocusEvents[0].viewId, view.viewId); + expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); + expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); + + expect(dispatchedViewFocusEvents[1].viewId, view.viewId); + expect(dispatchedViewFocusEvents[1].state, ui.ViewFocusState.unfocused); + expect(dispatchedViewFocusEvents[1].direction, ui.ViewFocusDirection.undefined); + }); + + test('requestViewFocusChange does nothing if the view does not exist', () { + final EngineFlutterView view = createAndRegisterView(dispatcher); + + dispatcher.requestViewFocusChange( + viewId: 5094555, + state: ui.ViewFocusState.focused, + direction: ui.ViewFocusDirection.forward, + ); + + expect(domDocument.activeElement, isNot(view.dom.rootElement)); + expect(dispatchedViewFocusEvents, isEmpty); + }); + + test('requestViewFocusChange does nothing if the view is already focused', () { + final EngineFlutterView view = createAndRegisterView(dispatcher); + + dispatcher.requestViewFocusChange( + viewId: view.viewId, + state: ui.ViewFocusState.focused, + direction: ui.ViewFocusDirection.forward, + ); + dispatcher.requestViewFocusChange( + viewId: view.viewId, + state: ui.ViewFocusState.focused, + direction: ui.ViewFocusDirection.forward, + ); + + expect(dispatchedViewFocusEvents, hasLength(1)); + + expect(dispatchedViewFocusEvents[0].viewId, view.viewId); + expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); + expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); + }); + + test('requestViewFocusChange does not move the focus to the view', () { + final DomElement input = createDomElement('input'); + final EngineFlutterView view = createAndRegisterView(dispatcher); + + view.dom.rootElement.append(input); + input.focus(); + + dispatcher.requestViewFocusChange( + viewId: view.viewId, + state: ui.ViewFocusState.focused, + direction: ui.ViewFocusDirection.forward, + ); + + expect(domDocument.activeElement, input); + + expect(dispatchedViewFocusEvents, hasLength(1)); + + expect(dispatchedViewFocusEvents[0].viewId, view.viewId); + expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused); + expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward); + }); }); }