diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 6005f1f3cf8bd..1cb4a75d565f6 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -932,6 +932,7 @@ extension DomKeyboardEventExtension on DomKeyboardEvent { external bool get metaKey; external bool? get repeat; external bool get shiftKey; + external bool get isComposing; external bool getModifierState(String keyArg); } diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index 0e8226a15aa01..e7f67bcb49ce2 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -198,6 +198,7 @@ class FlutterHtmlKeyboardEvent { bool get ctrlKey => _event.ctrlKey; bool get shiftKey => _event.shiftKey; bool get metaKey => _event.metaKey; + bool get isComposing => _event.isComposing; bool getModifierState(String key) => _event.getModifierState(key); void preventDefault() => _event.preventDefault(); diff --git a/lib/web_ui/lib/src/engine/raw_keyboard.dart b/lib/web_ui/lib/src/engine/raw_keyboard.dart index 57d16116e7c2d..6157bd7477c85 100644 --- a/lib/web_ui/lib/src/engine/raw_keyboard.dart +++ b/lib/web_ui/lib/src/engine/raw_keyboard.dart @@ -88,6 +88,14 @@ class RawKeyboard { return _onMacOs; } + bool _shouldIgnore(FlutterHtmlKeyboardEvent event) { + // During IME composition, Tab fires twice (once for composition and once + // for regular tabbing behavior), which causes issues. Intercepting the + // tab keydown event during composition prevents these issues from occurring. + // https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event#ignoring_keydown_during_ime_composition + return event.type == 'keydown' && event.key == 'Tab' && event.isComposing; + } + void _handleHtmlEvent(DomEvent domEvent) { if (!domInstanceOfString(domEvent, 'KeyboardEvent')) { return; @@ -96,6 +104,10 @@ class RawKeyboard { final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent); final String timerKey = event.code!; + if (_shouldIgnore(event)) { + return; + } + // Don't handle synthesizing a keyup event for modifier keys if (!_isModifierKey(event) && _shouldDoKeyGuard()) { _keydownTimers[timerKey]?.cancel(); diff --git a/lib/web_ui/test/keyboard_test_common.dart b/lib/web_ui/test/keyboard_test_common.dart index b4b2113731ea8..0b447d8b3b5e7 100644 --- a/lib/web_ui/test/keyboard_test_common.dart +++ b/lib/web_ui/test/keyboard_test_common.dart @@ -14,6 +14,7 @@ class MockKeyboardEvent implements FlutterHtmlKeyboardEvent { this.timeStamp = 0, this.repeat = false, this.keyCode = 0, + this.isComposing = false, bool altKey = false, bool ctrlKey = false, bool shiftKey = false, @@ -50,6 +51,9 @@ class MockKeyboardEvent implements FlutterHtmlKeyboardEvent { @override num? timeStamp; + @override + bool isComposing; + @override bool get altKey => modifierState.contains('Alt'); diff --git a/lib/web_ui/test/raw_keyboard_test.dart b/lib/web_ui/test/raw_keyboard_test.dart index bb490b60fb8f7..0f137ff42df95 100644 --- a/lib/web_ui/test/raw_keyboard_test.dart +++ b/lib/web_ui/test/raw_keyboard_test.dart @@ -299,7 +299,9 @@ void testMain() { RawKeyboard.instance!.dispose(); }); - test('the "Tab" key should never be ignored', () { + test( + 'the "Tab" key should never be ignored when it is not a part of IME composition', + () { RawKeyboard.initialize(); int count = 0; @@ -325,6 +327,28 @@ void testMain() { RawKeyboard.instance!.dispose(); }); + test('Ignores event when Tab key is hit during IME composition', () { + RawKeyboard.initialize(); + + int count = 0; + ui.window.onPlatformMessage = (String channel, ByteData? data, + ui.PlatformMessageResponseCallback? callback) { + count += 1; + final ByteData response = const JSONMessageCodec() + .encodeMessage({'handled': true})!; + callback!(response); + }; + + useTextEditingElement((DomElement element) { + dispatchKeyboardEvent('keydown', + key: 'Tab', code: 'Tab', target: element, isComposing: true); + + expect(count, 0); // no message sent to framework + }); + + RawKeyboard.instance!.dispose(); + }); + testFakeAsync( 'On macOS, synthesize keyup when shortcut is handled by the system', (FakeAsync async) { @@ -719,6 +743,7 @@ DomKeyboardEvent dispatchKeyboardEvent( bool isAltPressed = false, bool isControlPressed = false, bool isMetaPressed = false, + bool isComposing = false, int keyCode = 0, }) { target ??= domWindow; @@ -736,6 +761,7 @@ DomKeyboardEvent dispatchKeyboardEvent( 'altKey': isAltPressed, 'ctrlKey': isControlPressed, 'metaKey': isMetaPressed, + 'isComposing': isComposing, 'keyCode': keyCode, 'bubbles': true, 'cancelable': true,