Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit d3c97bb

Browse files
authored
[web] Ensure handled key event is not propagated to IME (#46829)
Fixes [136460](flutter/flutter#136460) Changes: - Raw keyboard event is handled during capture phase. This is to ensure that the framework processes the event before reaching to IME text area and raw keyboard can stop the propagation for handled events. - `RawKeyboard` event handler is invoked from `KeyboardBinding` event handler. This is to prevent race condition because both handlers now run in capture phase and `KeyboardBinding` needs to process the event first. *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I added new tests to check the change I am making or feature I am adding, or the PR is [test-exempt]. See [testing the engine] for instructions on writing and running engine tests. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I signed the [CLA]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style [testing the engine]: https://github.com/flutter/flutter/wiki/Testing-the-engine [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
1 parent 8d51b64 commit d3c97bb

File tree

6 files changed

+74
-20
lines changed

6 files changed

+74
-20
lines changed

lib/web_ui/lib/src/engine/keyboard_binding.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'browser_detection.dart';
1313
import 'dom.dart';
1414
import 'key_map.g.dart';
1515
import 'platform_dispatcher.dart';
16+
import 'raw_keyboard.dart';
1617
import 'semantics.dart';
1718

1819
typedef _VoidCallback = void Function();
@@ -104,9 +105,12 @@ class KeyboardBinding {
104105
_addEventListener('keydown', (DomEvent domEvent) {
105106
final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent);
106107
_converter.handleEvent(event);
108+
RawKeyboard.instance?.handleHtmlEvent(domEvent);
107109
});
108-
_addEventListener('keyup', (DomEvent event) {
109-
_converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent));
110+
_addEventListener('keyup', (DomEvent domEvent) {
111+
final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent);
112+
_converter.handleEvent(event);
113+
RawKeyboard.instance?.handleHtmlEvent(domEvent);
110114
});
111115
}
112116

@@ -209,6 +213,7 @@ class FlutterHtmlKeyboardEvent {
209213

210214
bool getModifierState(String key) => _event.getModifierState(key);
211215
void preventDefault() => _event.preventDefault();
216+
void stopPropagation() => _event.stopPropagation();
212217
bool get defaultPrevented => _event.defaultPrevented;
213218
}
214219

lib/web_ui/lib/src/engine/raw_keyboard.dart

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,6 @@ import 'services.dart';
1515
/// Provides keyboard bindings, such as the `flutter/keyevent` channel.
1616
class RawKeyboard {
1717
RawKeyboard._(this._onMacOs) {
18-
_keydownListener = createDomEventListener((DomEvent event) {
19-
_handleHtmlEvent(event);
20-
});
21-
domWindow.addEventListener('keydown', _keydownListener);
22-
23-
_keyupListener = createDomEventListener((DomEvent event) {
24-
_handleHtmlEvent(event);
25-
});
26-
domWindow.addEventListener('keyup', _keyupListener);
2718
registerHotRestartListener(() {
2819
dispose();
2920
});
@@ -34,6 +25,9 @@ class RawKeyboard {
3425
/// Use the [instance] getter to get the singleton after calling this method.
3526
static void initialize({bool onMacOs = false}) {
3627
_instance ??= RawKeyboard._(onMacOs);
28+
// KeyboardBinding is responsible for forwarding the keyboard
29+
// events to the RawKeyboard handler.
30+
KeyboardBinding.initInstance();
3731
}
3832

3933
/// The [RawKeyboard] singleton.
@@ -46,24 +40,16 @@ class RawKeyboard {
4640
/// if no repeat events were received.
4741
final Map<String, Timer> _keydownTimers = <String, Timer>{};
4842

49-
DomEventListener? _keydownListener;
50-
DomEventListener? _keyupListener;
51-
5243
/// Uninitializes the [RawKeyboard] singleton.
5344
///
5445
/// After calling this method this object becomes unusable and [instance]
5546
/// becomes `null`. Call [initialize] again to initialize a new singleton.
5647
void dispose() {
57-
domWindow.removeEventListener('keydown', _keydownListener);
58-
domWindow.removeEventListener('keyup', _keyupListener);
59-
6048
for (final String key in _keydownTimers.keys) {
6149
_keydownTimers[key]!.cancel();
6250
}
6351
_keydownTimers.clear();
6452

65-
_keydownListener = null;
66-
_keyupListener = null;
6753
_instance = null;
6854
}
6955

@@ -96,7 +82,7 @@ class RawKeyboard {
9682
return event.type == 'keydown' && event.key == 'Tab' && event.isComposing;
9783
}
9884

99-
void _handleHtmlEvent(DomEvent domEvent) {
85+
void handleHtmlEvent(DomEvent domEvent) {
10086
if (!domInstanceOfString(domEvent, 'KeyboardEvent')) {
10187
return;
10288
}
@@ -158,6 +144,7 @@ class RawKeyboard {
158144
if (jsonResponse['handled'] as bool) {
159145
// If the framework handled it, then don't propagate it any further.
160146
event.preventDefault();
147+
event.stopPropagation();
161148
}
162149
},
163150
);

lib/web_ui/test/common/keyboard_test_common.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class MockKeyboardEvent implements FlutterHtmlKeyboardEvent {
2222
bool altGrKey = false,
2323
this.location = 0,
2424
this.onPreventDefault,
25+
this.onStopPropagation,
2526
}) : modifierState =
2627
<String>{
2728
if (altKey) 'Alt',
@@ -84,6 +85,12 @@ class MockKeyboardEvent implements FlutterHtmlKeyboardEvent {
8485
bool get defaultPrevented => _defaultPrevented;
8586
bool _defaultPrevented = false;
8687

88+
@override
89+
void stopPropagation() {
90+
onStopPropagation?.call();
91+
}
92+
VoidCallback? onStopPropagation;
93+
8794
static bool get lastDefaultPrevented => _lastEvent?.defaultPrevented ?? false;
8895
static MockKeyboardEvent? _lastEvent;
8996
}

lib/web_ui/test/engine/raw_keyboard_test.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ void testMain() {
5252

5353
DomKeyboardEvent event;
5454

55+
// Dispatch a keydown event first so that KeyboardBinding will recognize the keyup event.
56+
// and will not set preventDefault on it.
57+
event = dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode', keyCode: 1);
58+
5559
event = dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode', keyCode: 1);
5660

5761
expect(event.defaultPrevented, isFalse);

lib/web_ui/test/engine/text_editing_test.dart

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import 'package:test/test.dart';
1212
import 'package:ui/src/engine.dart' show flutterViewEmbedder;
1313
import 'package:ui/src/engine/browser_detection.dart';
1414
import 'package:ui/src/engine/dom.dart';
15+
import 'package:ui/src/engine/raw_keyboard.dart';
1516
import 'package:ui/src/engine/services.dart';
1617
import 'package:ui/src/engine/text_editing/autofill_hint.dart';
1718
import 'package:ui/src/engine/text_editing/input_type.dart';
1819
import 'package:ui/src/engine/text_editing/text_editing.dart';
1920
import 'package:ui/src/engine/util.dart';
2021
import 'package:ui/src/engine/vector_math.dart';
22+
import 'package:ui/ui.dart' as ui;
2123

2224
import '../common/spy.dart';
2325
import '../common/test_initialization.dart';
@@ -370,6 +372,52 @@ Future<void> testMain() async {
370372
expect(lastInputAction, 'TextInputAction.done');
371373
});
372374

375+
test('handling keyboard event prevents triggering input action', () {
376+
final ui.PlatformMessageCallback? savedCallback = ui.window.onPlatformMessage;
377+
378+
bool markTextEventHandled = false;
379+
ui.window.onPlatformMessage = (String channel, ByteData? data,
380+
ui.PlatformMessageResponseCallback? callback) {
381+
final ByteData response = const JSONMessageCodec()
382+
.encodeMessage(<String, dynamic>{'handled': markTextEventHandled})!;
383+
callback!(response);
384+
};
385+
RawKeyboard.initialize();
386+
387+
final InputConfiguration config = InputConfiguration();
388+
editingStrategy!.enable(
389+
config,
390+
onChange: trackEditingState,
391+
onAction: trackInputAction,
392+
);
393+
394+
// No input action so far.
395+
expect(lastInputAction, isNull);
396+
397+
markTextEventHandled = true;
398+
dispatchKeyboardEvent(
399+
editingStrategy!.domElement!,
400+
'keydown',
401+
keyCode: _kReturnKeyCode,
402+
);
403+
404+
// Input action prevented by platform message callback.
405+
expect(lastInputAction, isNull);
406+
407+
markTextEventHandled = false;
408+
dispatchKeyboardEvent(
409+
editingStrategy!.domElement!,
410+
'keydown',
411+
keyCode: _kReturnKeyCode,
412+
);
413+
414+
// Input action received.
415+
expect(lastInputAction, 'TextInputAction.done');
416+
417+
ui.window.onPlatformMessage = savedCallback;
418+
RawKeyboard.instance?.dispose();
419+
});
420+
373421
test('Triggers input action in multi-line mode', () {
374422
final InputConfiguration config = InputConfiguration(
375423
inputType: EngineInputType.multiline,

third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class LocaleKeymap {
4141
return eventKeyCode;
4242
}
4343
if (result == null) {
44+
if ((eventCode ?? '').isEmpty && (eventKey ?? '').isEmpty) {
45+
return null;
46+
}
4447
final int? heuristicResult = heuristicMapper(eventCode ?? '', eventKey ?? '');
4548
if (heuristicResult != null) {
4649
return heuristicResult;

0 commit comments

Comments
 (0)