From e9edb8e14f6d3bdc8d63b202f59fa35f8a7f7a32 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 2 Oct 2019 15:09:08 -0700 Subject: [PATCH] Support correct keymap for web (#12712) --- lib/web_ui/lib/src/engine/keyboard.dart | 57 ++++++--- lib/web_ui/test/keyboard_test.dart | 150 ++++++++++++++++++++---- 2 files changed, 169 insertions(+), 38 deletions(-) diff --git a/lib/web_ui/lib/src/engine/keyboard.dart b/lib/web_ui/lib/src/engine/keyboard.dart index 36983e1a1b7..881ef6ae75d 100644 --- a/lib/web_ui/lib/src/engine/keyboard.dart +++ b/lib/web_ui/lib/src/engine/keyboard.dart @@ -52,28 +52,51 @@ class Keyboard { void _handleHtmlEvent(html.KeyboardEvent event) { final Map eventData = { 'type': event.type, - // TODO(yjbanov): this emulates Android because that the only reasonable - // thing to map to right now (the other choice is fuchsia). - // However, eventually we need to have something that maps - // better to Web. - 'keymap': 'android', - 'keyCode': event.keyCode, + 'keymap': 'web', + 'code': event.code, + 'key': event.key, + 'metaState': _getMetaState(event), }; - // TODO(yjbanov): The browser does not report `charCode` for 'keydown' and - // 'keyup', only for 'keypress'. This restores the value - // from the 'key' field. However, we need to verify how - // many code units a single key can have. Right now it - // assumes exactly one unit (that's what Flutter framework - // expects). But we'll need a different strategy if other - // code unit counts are possible. - if (event.key.codeUnits.length == 1) { - eventData['codePoint'] = event.key.codeUnits.first; - } - ui.window.onPlatformMessage('flutter/keyevent', _messageCodec.encodeMessage(eventData), _noopCallback); } } +const int _modifierNone = 0x00; +const int _modifierShift = 0x01; +const int _modifierAlt = 0x02; +const int _modifierControl = 0x04; +const int _modifierMeta = 0x08; +const int _modifierNumLock = 0x10; +const int _modifierCapsLock = 0x20; +const int _modifierScrollLock = 0x40; + +/// Creates a bitmask representing the meta state of the [event]. +int _getMetaState(html.KeyboardEvent event) { + int metaState = _modifierNone; + if (event.getModifierState('Shift')) { + metaState |= _modifierShift; + } + if (event.getModifierState('Alt')) { + metaState |= _modifierAlt; + } + if (event.getModifierState('Control')) { + metaState |= _modifierControl; + } + if (event.getModifierState('Meta')) { + metaState |= _modifierMeta; + } + if (event.getModifierState('NumLock')) { + metaState |= _modifierNumLock; + } + if (event.getModifierState('CapsLock')) { + metaState |= _modifierCapsLock; + } + if (event.getModifierState('ScrollLock')) { + metaState |= _modifierScrollLock; + } + return metaState; +} + void _noopCallback(ByteData data) {} diff --git a/lib/web_ui/test/keyboard_test.dart b/lib/web_ui/test/keyboard_test.dart index 4e2ca3635ff..a0383f2f02f 100644 --- a/lib/web_ui/test/keyboard_test.dart +++ b/lib/web_ui/test/keyboard_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:html' as html; +import 'dart:js_util' as js_util; import 'dart:typed_data'; import 'package:ui/src/engine.dart'; @@ -15,7 +16,7 @@ void main() { test('initializes', () { expect(Keyboard.instance, isNull); Keyboard.initialize(); - expect(Keyboard.instance, isNotNull); + expect(Keyboard.instance, isA()); }); test('dispatches keyup to flutter/keyevent channel', () { @@ -27,17 +28,16 @@ void main() { dataReceived = const JSONMessageCodec().decodeMessage(data); }; - html.window.dispatchEvent(html.KeyboardEvent('keyup')); + dispatchKeyboardEvent('keyup', key: 'SomeKey', code: 'SomeCode'); expect(channelReceived, 'flutter/keyevent'); - expect(dataReceived['type'], 'keyup'); - expect(dataReceived['keymap'], 'android'); - - // Unfortunately there's no way to fake `keyCode`. - expect(dataReceived['keyCode'], 0); - - // Unfortunately there's no way to fake `key`. - expect(dataReceived, isNot(contains('codePoint'))); + expect(dataReceived, { + 'type': 'keyup', + 'keymap': 'web', + 'code': 'SomeCode', + 'key': 'SomeKey', + 'metaState': 0x0, + }); }); test('dispatches keydown to flutter/keyevent channel', () { @@ -49,17 +49,96 @@ void main() { dataReceived = const JSONMessageCodec().decodeMessage(data); }; - html.window.dispatchEvent(html.KeyboardEvent('keydown')); + dispatchKeyboardEvent('keydown', key: 'SomeKey', code: 'SomeCode'); expect(channelReceived, 'flutter/keyevent'); - expect(dataReceived['type'], 'keydown'); - expect(dataReceived['keymap'], 'android'); + expect(dataReceived, { + 'type': 'keydown', + 'keymap': 'web', + 'code': 'SomeCode', + 'key': 'SomeKey', + 'metaState': 0x0, + }); + }); - // Unfortunately there's no way to fake `keyCode`. - expect(dataReceived['keyCode'], 0); + test('dispatches correct meta state', () { + Map dataReceived; + ui.window.onPlatformMessage = (String channel, ByteData data, + ui.PlatformMessageResponseCallback callback) { + dataReceived = const JSONMessageCodec().decodeMessage(data); + }; - // Unfortunately there's no way to fake `key`. - expect(dataReceived, isNot(contains('codePoint'))); + dispatchKeyboardEvent( + 'keydown', + key: 'SomeKey', + code: 'SomeCode', + isControlPressed: true, + ); + expect(dataReceived, { + 'type': 'keydown', + 'keymap': 'web', + 'code': 'SomeCode', + 'key': 'SomeKey', + // ctrl + 'metaState': 0x4, + }); + + dispatchKeyboardEvent( + 'keydown', + key: 'SomeKey', + code: 'SomeCode', + isShiftPressed: true, + isAltPressed: true, + isMetaPressed: true, + ); + expect(dataReceived, { + 'type': 'keydown', + 'keymap': 'web', + 'code': 'SomeCode', + 'key': 'SomeKey', + // shift alt meta + 'metaState': 0x1 | 0x2 | 0x8, + }); + }); + + test('dispatches repeat events', () { + List> messages = >[]; + ui.window.onPlatformMessage = (String channel, ByteData data, + ui.PlatformMessageResponseCallback callback) { + messages.add(const JSONMessageCodec().decodeMessage(data)); + }; + + dispatchKeyboardEvent( + 'keydown', + key: 'SomeKey', + code: 'SomeCode', + repeat: true, + ); + dispatchKeyboardEvent( + 'keydown', + key: 'SomeKey', + code: 'SomeCode', + repeat: true, + ); + dispatchKeyboardEvent( + 'keydown', + key: 'SomeKey', + code: 'SomeCode', + repeat: true, + ); + + final Map expectedMessage = { + 'type': 'keydown', + 'keymap': 'web', + 'code': 'SomeCode', + 'key': 'SomeKey', + 'metaState': 0, + }; + expect(messages, >[ + expectedMessage, + expectedMessage, + expectedMessage, + ]); }); test('stops dispatching events after dispose', () { @@ -69,19 +148,48 @@ void main() { count += 1; }; - html.window.dispatchEvent(html.KeyboardEvent('keydown')); + dispatchKeyboardEvent('keydown'); expect(count, 1); - html.window.dispatchEvent(html.KeyboardEvent('keyup')); + dispatchKeyboardEvent('keyup'); expect(count, 2); Keyboard.instance.dispose(); expect(Keyboard.instance, isNull); // No more event dispatching. - html.window.dispatchEvent(html.KeyboardEvent('keydown')); + dispatchKeyboardEvent('keydown'); expect(count, 2); - html.window.dispatchEvent(html.KeyboardEvent('keyup')); + dispatchKeyboardEvent('keyup'); expect(count, 2); }); }); } + +void dispatchKeyboardEvent( + String type, { + String key, + String code, + bool repeat = false, + bool isShiftPressed = false, + bool isAltPressed = false, + bool isControlPressed = false, + bool isMetaPressed = false, +}) { + final Function jsKeyboardEvent = + js_util.getProperty(html.window, 'KeyboardEvent'); + final List eventArgs = [ + type, + { + 'key': key, + 'code': code, + 'repeat': repeat, + 'shiftKey': isShiftPressed, + 'altKey': isAltPressed, + 'ctrlKey': isControlPressed, + 'metaKey': isMetaPressed, + } + ]; + final event = + js_util.callConstructor(jsKeyboardEvent, js_util.jsify(eventArgs)); + html.window.dispatchEvent(event); +}