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 d66264e2494ef..db0cd066da42b 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 @@ -14,12 +14,16 @@ final class ViewFocusBinding { static final ViewFocusBinding instance = ViewFocusBinding._(); final List _listeners = []; + int? _lastViewId; + ui.ViewFocusDirection _viewFocusDirection = ui.ViewFocusDirection.forward; /// Subscribes the [listener] to [ui.ViewFocusEvent] events. void addListener(ui.ViewFocusChangeCallback listener) { if (_listeners.isEmpty) { - domDocument.body?.addEventListener(_focusin, _handleFocusin, true); - domDocument.body?.addEventListener(_focusout, _handleFocusout, true); + domDocument.body?.addEventListener(_keyDown, _handleKeyDown); + domDocument.body?.addEventListener(_keyUp, _handleKeyUp); + domDocument.body?.addEventListener(_focusin, _handleFocusin); + domDocument.body?.addEventListener(_focusout, _handleFocusout); } _listeners.add(listener); } @@ -28,8 +32,10 @@ final class ViewFocusBinding { void removeListener(ui.ViewFocusChangeCallback listener) { _listeners.remove(listener); if (_listeners.isEmpty) { - domDocument.body?.removeEventListener(_focusin, _handleFocusin, true); - domDocument.body?.removeEventListener(_focusout, _handleFocusout, true); + domDocument.body?.removeEventListener(_keyDown, _handleKeyDown); + domDocument.body?.removeEventListener(_keyUp, _handleKeyUp); + domDocument.body?.removeEventListener(_focusin, _handleFocusin); + domDocument.body?.removeEventListener(_focusout, _handleFocusout); } } @@ -39,21 +45,32 @@ final class ViewFocusBinding { } } - late final DomEventListener _handleFocusin = createDomEventListener( - (DomEvent event) => _handleFocusChange(event.target as DomElement?), - ); + late final DomEventListener _handleFocusin = createDomEventListener((DomEvent event) { + event as DomFocusEvent; + _handleFocusChange(event.target as DomElement?); + }); - late final DomEventListener _handleFocusout = createDomEventListener( - (DomEvent event) => _handleFocusChange((event as DomFocusEvent).relatedTarget as DomElement?), - ); + late final DomEventListener _handleFocusout = createDomEventListener((DomEvent event) { + event as DomFocusEvent; + _handleFocusChange(event.relatedTarget as DomElement?); + }); + + late final DomEventListener _handleKeyDown = createDomEventListener((DomEvent event) { + event as DomKeyboardEvent; + if (event.shiftKey) { + _viewFocusDirection = ui.ViewFocusDirection.backward; + } + }); + + late final DomEventListener _handleKeyUp = createDomEventListener((DomEvent event) { + _viewFocusDirection = ui.ViewFocusDirection.forward; + }); - int? _lastViewId; void _handleFocusChange(DomElement? focusedElement) { final int? viewId = _viewId(focusedElement); if (viewId == _lastViewId) { return; } - final ui.ViewFocusEvent event; if (viewId == null) { event = ui.ViewFocusEvent( @@ -65,7 +82,7 @@ final class ViewFocusBinding { event = ui.ViewFocusEvent( viewId: viewId, state: ui.ViewFocusState.focused, - direction: ui.ViewFocusDirection.forward, + direction: _viewFocusDirection, ); } _lastViewId = viewId; @@ -84,4 +101,6 @@ final class ViewFocusBinding { static const String _focusin = 'focusin'; static const String _focusout = 'focusout'; + static const String _keyDown = 'keydown'; + static const String _keyUp = 'keyup'; } 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 9342595b0fca8..d220ed7eff19a 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 @@ -1,7 +1,6 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; @@ -85,8 +84,13 @@ void testMain() { focusableViewElement1.focus(); focusableViewElement2.focus(); + // The statements simulate the user pressing shift + tab in the keyboard. + // Synthetic keyboard events do not trigger focus changes. + domDocument.body!.pressTabKey(shift: true); + focusableViewElement1.focus(); + domDocument.body!.releaseTabKey(); - expect(viewFocusEvents, hasLength(2)); + expect(viewFocusEvents, hasLength(3)); expect(viewFocusEvents[0].viewId, view1.viewId); expect(viewFocusEvents[0].state, ui.ViewFocusState.focused); @@ -95,6 +99,10 @@ void testMain() { expect(viewFocusEvents[1].viewId, view2.viewId); expect(viewFocusEvents[1].state, ui.ViewFocusState.focused); expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.forward); + + expect(viewFocusEvents[2].viewId, view1.viewId); + expect(viewFocusEvents[2].state, ui.ViewFocusState.focused); + expect(viewFocusEvents[2].direction, ui.ViewFocusDirection.backward); }); test('fires a focus event - focus transitions on and off views', () async { @@ -137,3 +145,23 @@ void testMain() { }); }); } + +extension on DomElement { + void pressTabKey({bool shift = false}) { + dispatchKeyboardEvent(type: 'keydown', key: 'Tab', shiftKey: shift); + } + + void releaseTabKey({bool shift = false}) { + dispatchKeyboardEvent(type: 'keyup', key: 'Tab', shiftKey: shift); + } + + void dispatchKeyboardEvent({ + required String type, + required String key, + bool shiftKey = false, + }) { + dispatchEvent( + createDomKeyboardEvent(type, {'key': key, 'shiftKey': shiftKey}), + ); + } +}