diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 7a851b16c2deb..c12d1a36a9f8b 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -13,6 +13,7 @@ import 'browser_detection.dart'; import 'dom.dart'; import 'key_map.g.dart'; import 'platform_dispatcher.dart'; +import 'raw_keyboard.dart'; import 'semantics.dart'; typedef _VoidCallback = void Function(); @@ -104,9 +105,12 @@ class KeyboardBinding { _addEventListener('keydown', (DomEvent domEvent) { final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent); _converter.handleEvent(event); + RawKeyboard.instance?.handleHtmlEvent(domEvent); }); - _addEventListener('keyup', (DomEvent event) { - _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); + _addEventListener('keyup', (DomEvent domEvent) { + final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent); + _converter.handleEvent(event); + RawKeyboard.instance?.handleHtmlEvent(domEvent); }); } @@ -209,6 +213,7 @@ class FlutterHtmlKeyboardEvent { bool getModifierState(String key) => _event.getModifierState(key); void preventDefault() => _event.preventDefault(); + void stopPropagation() => _event.stopPropagation(); bool get defaultPrevented => _event.defaultPrevented; } diff --git a/lib/web_ui/lib/src/engine/raw_keyboard.dart b/lib/web_ui/lib/src/engine/raw_keyboard.dart index b299437b21f0e..d4aa87b6bbc90 100644 --- a/lib/web_ui/lib/src/engine/raw_keyboard.dart +++ b/lib/web_ui/lib/src/engine/raw_keyboard.dart @@ -15,15 +15,6 @@ import 'services.dart'; /// Provides keyboard bindings, such as the `flutter/keyevent` channel. class RawKeyboard { RawKeyboard._(this._onMacOs) { - _keydownListener = createDomEventListener((DomEvent event) { - _handleHtmlEvent(event); - }); - domWindow.addEventListener('keydown', _keydownListener); - - _keyupListener = createDomEventListener((DomEvent event) { - _handleHtmlEvent(event); - }); - domWindow.addEventListener('keyup', _keyupListener); registerHotRestartListener(() { dispose(); }); @@ -34,6 +25,9 @@ class RawKeyboard { /// Use the [instance] getter to get the singleton after calling this method. static void initialize({bool onMacOs = false}) { _instance ??= RawKeyboard._(onMacOs); + // KeyboardBinding is responsible for forwarding the keyboard + // events to the RawKeyboard handler. + KeyboardBinding.initInstance(); } /// The [RawKeyboard] singleton. @@ -46,24 +40,16 @@ class RawKeyboard { /// if no repeat events were received. final Map _keydownTimers = {}; - DomEventListener? _keydownListener; - DomEventListener? _keyupListener; - /// Uninitializes the [RawKeyboard] singleton. /// /// After calling this method this object becomes unusable and [instance] /// becomes `null`. Call [initialize] again to initialize a new singleton. void dispose() { - domWindow.removeEventListener('keydown', _keydownListener); - domWindow.removeEventListener('keyup', _keyupListener); - for (final String key in _keydownTimers.keys) { _keydownTimers[key]!.cancel(); } _keydownTimers.clear(); - _keydownListener = null; - _keyupListener = null; _instance = null; } @@ -96,7 +82,7 @@ class RawKeyboard { return event.type == 'keydown' && event.key == 'Tab' && event.isComposing; } - void _handleHtmlEvent(DomEvent domEvent) { + void handleHtmlEvent(DomEvent domEvent) { if (!domInstanceOfString(domEvent, 'KeyboardEvent')) { return; } @@ -158,6 +144,7 @@ class RawKeyboard { if (jsonResponse['handled'] as bool) { // If the framework handled it, then don't propagate it any further. event.preventDefault(); + event.stopPropagation(); } }, ); diff --git a/lib/web_ui/test/common/keyboard_test_common.dart b/lib/web_ui/test/common/keyboard_test_common.dart index 0b447d8b3b5e7..0ec1e5d75acb9 100644 --- a/lib/web_ui/test/common/keyboard_test_common.dart +++ b/lib/web_ui/test/common/keyboard_test_common.dart @@ -22,6 +22,7 @@ class MockKeyboardEvent implements FlutterHtmlKeyboardEvent { bool altGrKey = false, this.location = 0, this.onPreventDefault, + this.onStopPropagation, }) : modifierState = { if (altKey) 'Alt', @@ -84,6 +85,12 @@ class MockKeyboardEvent implements FlutterHtmlKeyboardEvent { bool get defaultPrevented => _defaultPrevented; bool _defaultPrevented = false; + @override + void stopPropagation() { + onStopPropagation?.call(); + } + VoidCallback? onStopPropagation; + static bool get lastDefaultPrevented => _lastEvent?.defaultPrevented ?? false; static MockKeyboardEvent? _lastEvent; } diff --git a/lib/web_ui/test/engine/raw_keyboard_test.dart b/lib/web_ui/test/engine/raw_keyboard_test.dart index d9f4cd0fe4036..0b1681b04042b 100644 --- a/lib/web_ui/test/engine/raw_keyboard_test.dart +++ b/lib/web_ui/test/engine/raw_keyboard_test.dart @@ -52,6 +52,10 @@ void testMain() { DomKeyboardEvent event; + // Dispatch a keydown event first so that KeyboardBinding will recognize the keyup event. + // and will not set preventDefault on it. + event = dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode', keyCode: 1); + event = dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode', keyCode: 1); expect(event.defaultPrevented, isFalse); diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart index a6e5f02c0a33b..37d44b3f321ed 100644 --- a/lib/web_ui/test/engine/text_editing_test.dart +++ b/lib/web_ui/test/engine/text_editing_test.dart @@ -12,12 +12,14 @@ import 'package:test/test.dart'; import 'package:ui/src/engine.dart' show flutterViewEmbedder; import 'package:ui/src/engine/browser_detection.dart'; import 'package:ui/src/engine/dom.dart'; +import 'package:ui/src/engine/raw_keyboard.dart'; import 'package:ui/src/engine/services.dart'; import 'package:ui/src/engine/text_editing/autofill_hint.dart'; import 'package:ui/src/engine/text_editing/input_type.dart'; import 'package:ui/src/engine/text_editing/text_editing.dart'; import 'package:ui/src/engine/util.dart'; import 'package:ui/src/engine/vector_math.dart'; +import 'package:ui/ui.dart' as ui; import '../common/spy.dart'; import '../common/test_initialization.dart'; @@ -370,6 +372,52 @@ Future testMain() async { expect(lastInputAction, 'TextInputAction.done'); }); + test('handling keyboard event prevents triggering input action', () { + final ui.PlatformMessageCallback? savedCallback = ui.window.onPlatformMessage; + + bool markTextEventHandled = false; + ui.window.onPlatformMessage = (String channel, ByteData? data, + ui.PlatformMessageResponseCallback? callback) { + final ByteData response = const JSONMessageCodec() + .encodeMessage({'handled': markTextEventHandled})!; + callback!(response); + }; + RawKeyboard.initialize(); + + final InputConfiguration config = InputConfiguration(); + editingStrategy!.enable( + config, + onChange: trackEditingState, + onAction: trackInputAction, + ); + + // No input action so far. + expect(lastInputAction, isNull); + + markTextEventHandled = true; + dispatchKeyboardEvent( + editingStrategy!.domElement!, + 'keydown', + keyCode: _kReturnKeyCode, + ); + + // Input action prevented by platform message callback. + expect(lastInputAction, isNull); + + markTextEventHandled = false; + dispatchKeyboardEvent( + editingStrategy!.domElement!, + 'keydown', + keyCode: _kReturnKeyCode, + ); + + // Input action received. + expect(lastInputAction, 'TextInputAction.done'); + + ui.window.onPlatformMessage = savedCallback; + RawKeyboard.instance?.dispose(); + }); + test('Triggers input action in multi-line mode', () { final InputConfiguration config = InputConfiguration( inputType: EngineInputType.multiline, diff --git a/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart b/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart index dd2faa527fc2a..5c261f665571f 100644 --- a/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart +++ b/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart @@ -41,6 +41,9 @@ class LocaleKeymap { return eventKeyCode; } if (result == null) { + if ((eventCode ?? '').isEmpty && (eventKey ?? '').isEmpty) { + return null; + } final int? heuristicResult = heuristicMapper(eventCode ?? '', eventKey ?? ''); if (heuristicResult != null) { return heuristicResult;