From 1a2d2636f9432a3707f4a994ad0760638b8160ca Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 16:58:01 +0200 Subject: [PATCH 01/10] [web] Ensure handled key event is not propagated to IME --- .../lib/src/engine/keyboard_binding.dart | 8 ++++++-- lib/web_ui/lib/src/engine/raw_keyboard.dart | 19 ++----------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 7a851b16c2deb..5066f7ea62d9c 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -5,6 +5,7 @@ import 'dart:js_interop'; import 'package:meta/meta.dart'; +import 'package:ui/src/engine/raw_keyboard.dart'; import 'package:ui/ui.dart' as ui; import 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap; @@ -104,9 +105,11 @@ 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) { + _converter.handleEvent(FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent)); + RawKeyboard.instance?.handleHtmlEvent(domEvent); }); } @@ -209,6 +212,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..390bacad15039 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(); }); @@ -46,24 +37,17 @@ 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 +80,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 +142,7 @@ class RawKeyboard { if (jsonResponse['handled'] as bool) { // If the framework handled it, then don't propagate it any further. event.preventDefault(); + event.stopPropagation(); } }, ); From 96ba7a3a4848e621bfe4e8213add285188e277dd Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 17:20:33 +0200 Subject: [PATCH 02/10] Add missing implementation --- lib/web_ui/test/common/keyboard_test_common.dart | 7 +++++++ 1 file changed, 7 insertions(+) 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; } From 767e2c698f093517ca58de2c772b6f70b6b3a8d1 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 17:40:31 +0200 Subject: [PATCH 03/10] Use relative import --- lib/web_ui/lib/src/engine/keyboard_binding.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 5066f7ea62d9c..617dfbb9ccf7a 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -5,7 +5,6 @@ import 'dart:js_interop'; import 'package:meta/meta.dart'; -import 'package:ui/src/engine/raw_keyboard.dart'; import 'package:ui/ui.dart' as ui; import 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap; @@ -14,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(); From 203cfc27903969592dc018791e6a97e11fd9b7da Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 17:41:11 +0200 Subject: [PATCH 04/10] Init keyboard binding instance --- lib/web_ui/lib/src/engine/raw_keyboard.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/raw_keyboard.dart b/lib/web_ui/lib/src/engine/raw_keyboard.dart index 390bacad15039..5f6f8a22b40b6 100644 --- a/lib/web_ui/lib/src/engine/raw_keyboard.dart +++ b/lib/web_ui/lib/src/engine/raw_keyboard.dart @@ -25,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 instance is responsible for forwarding the keyboard + // events to the RawKeyboard handler. + KeyboardBinding.initInstance(); } /// The [RawKeyboard] singleton. @@ -37,7 +40,6 @@ class RawKeyboard { /// if no repeat events were received. final Map _keydownTimers = {}; - /// Uninitializes the [RawKeyboard] singleton. /// /// After calling this method this object becomes unusable and [instance] From 2afa26c3a63d142a4c586e56ebde50211c403d29 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 18:11:11 +0200 Subject: [PATCH 05/10] Check channel name in test --- lib/web_ui/test/engine/raw_keyboard_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/test/engine/raw_keyboard_test.dart b/lib/web_ui/test/engine/raw_keyboard_test.dart index d9f4cd0fe4036..297f57985ec0e 100644 --- a/lib/web_ui/test/engine/raw_keyboard_test.dart +++ b/lib/web_ui/test/engine/raw_keyboard_test.dart @@ -240,7 +240,9 @@ void testMain() { int count = 0; ui.window.onPlatformMessage = (String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback) { - count += 1; + if (channel == 'flutter/keyevent') { + count += 1; + } }; dispatchKeyboardEvent('keydown'); From 364c9c71d8e6ffac785615fac1eefa1b53ec43dd Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 19:19:28 +0200 Subject: [PATCH 06/10] Fix more tests --- lib/web_ui/lib/src/engine/keyboard_binding.dart | 3 ++- lib/web_ui/test/engine/raw_keyboard_test.dart | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 617dfbb9ccf7a..c12d1a36a9f8b 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -108,7 +108,8 @@ class KeyboardBinding { RawKeyboard.instance?.handleHtmlEvent(domEvent); }); _addEventListener('keyup', (DomEvent domEvent) { - _converter.handleEvent(FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent)); + final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent); + _converter.handleEvent(event); RawKeyboard.instance?.handleHtmlEvent(domEvent); }); } diff --git a/lib/web_ui/test/engine/raw_keyboard_test.dart b/lib/web_ui/test/engine/raw_keyboard_test.dart index 297f57985ec0e..ead0253911dcb 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); @@ -245,18 +249,18 @@ void testMain() { } }; - dispatchKeyboardEvent('keydown'); + dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode'); expect(count, 1); - dispatchKeyboardEvent('keyup'); + dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode'); expect(count, 2); RawKeyboard.instance!.dispose(); expect(RawKeyboard.instance, isNull); // No more event dispatching. - dispatchKeyboardEvent('keydown'); + dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode'); expect(count, 2); - dispatchKeyboardEvent('keyup'); + dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode'); expect(count, 2); }); @@ -766,8 +770,8 @@ void useTextEditingElement(ElementCallback callback) { DomKeyboardEvent dispatchKeyboardEvent( String type, { DomEventTarget? target, - String? key, - String? code, + required String? key, + required String? code, int location = 0, bool repeat = false, bool isShiftPressed = false, From 18b7702cb74a427bdfc940e8c9a1fc492dd596dd Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 19:25:45 +0200 Subject: [PATCH 07/10] typo --- lib/web_ui/lib/src/engine/raw_keyboard.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/raw_keyboard.dart b/lib/web_ui/lib/src/engine/raw_keyboard.dart index 5f6f8a22b40b6..d4aa87b6bbc90 100644 --- a/lib/web_ui/lib/src/engine/raw_keyboard.dart +++ b/lib/web_ui/lib/src/engine/raw_keyboard.dart @@ -25,7 +25,7 @@ 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 instance is responsible for forwarding the keyboard + // KeyboardBinding is responsible for forwarding the keyboard // events to the RawKeyboard handler. KeyboardBinding.initInstance(); } From e79ca6456fa5cd40b4a8117167b549578b59ded4 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Thu, 12 Oct 2023 22:05:13 +0200 Subject: [PATCH 08/10] Remove channel name check --- lib/web_ui/test/engine/raw_keyboard_test.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/web_ui/test/engine/raw_keyboard_test.dart b/lib/web_ui/test/engine/raw_keyboard_test.dart index ead0253911dcb..3b82789697702 100644 --- a/lib/web_ui/test/engine/raw_keyboard_test.dart +++ b/lib/web_ui/test/engine/raw_keyboard_test.dart @@ -244,9 +244,7 @@ void testMain() { int count = 0; ui.window.onPlatformMessage = (String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback) { - if (channel == 'flutter/keyevent') { - count += 1; - } + count += 1; }; dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode'); From 021da3bfe694c5a8e16bf6d137f449607e9f9a2f Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Fri, 13 Oct 2023 11:26:09 +0200 Subject: [PATCH 09/10] Fix exception on empty eventCode and eventKey --- lib/web_ui/test/engine/raw_keyboard_test.dart | 12 ++++++------ .../lib/web_locale_keymap/locale_keymap.dart | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/web_ui/test/engine/raw_keyboard_test.dart b/lib/web_ui/test/engine/raw_keyboard_test.dart index 3b82789697702..0b1681b04042b 100644 --- a/lib/web_ui/test/engine/raw_keyboard_test.dart +++ b/lib/web_ui/test/engine/raw_keyboard_test.dart @@ -247,18 +247,18 @@ void testMain() { count += 1; }; - dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode'); + dispatchKeyboardEvent('keydown'); expect(count, 1); - dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode'); + dispatchKeyboardEvent('keyup'); expect(count, 2); RawKeyboard.instance!.dispose(); expect(RawKeyboard.instance, isNull); // No more event dispatching. - dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode'); + dispatchKeyboardEvent('keydown'); expect(count, 2); - dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode'); + dispatchKeyboardEvent('keyup'); expect(count, 2); }); @@ -768,8 +768,8 @@ void useTextEditingElement(ElementCallback callback) { DomKeyboardEvent dispatchKeyboardEvent( String type, { DomEventTarget? target, - required String? key, - required String? code, + String? key, + String? code, int location = 0, bool repeat = false, bool isShiftPressed = false, 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; From 129cce80b7ec643a3172985f223043154bdea7b6 Mon Sep 17 00:00:00 2001 From: Matej Knopp Date: Fri, 13 Oct 2023 11:26:41 +0200 Subject: [PATCH 10/10] Add test --- lib/web_ui/test/engine/text_editing_test.dart | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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,