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

[Keyboard] Enable special CapsLock handling and key guarding for Web on iOS #37972

Merged
merged 1 commit into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions lib/web_ui/lib/src/engine/keyboard_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,13 @@ class FlutterHtmlKeyboardEvent {
// dispatched asynchronously.
class KeyboardConverter {
KeyboardConverter(this.performDispatchKeyData, OperatingSystem platform)
: onMacOs = platform == OperatingSystem.macOs,
: onDarwin = platform == OperatingSystem.macOs || platform == OperatingSystem.iOs,
_mapping = _mappingFromPlatform(platform);

final DispatchKeyData performDispatchKeyData;
/// Whether the current platform is macOS, which affects how certain key events
/// are comprehended.
final bool onMacOs;
/// Whether the current platform is macOS or iOS, which affects how certain key
/// events are comprehended, including CapsLock and key guarding.
final bool onDarwin;
/// Maps logical keys from key event properties.
final locale_keymap.LocaleKeymap _mapping;

Expand Down Expand Up @@ -261,7 +261,7 @@ class KeyboardConverter {
// key down, and synthesizes immediate cancel events following them. The state
// of "whether CapsLock is on" should be accessed by "activeLocks".
bool _shouldSynthesizeCapsLockUp() {
return onMacOs;
return onDarwin;
}

// ## About Key guards
Expand All @@ -272,10 +272,10 @@ class KeyboardConverter {
//
// To avoid this, we rely on the fact that browsers send repeat events
// while the key is held down by the user. If we don't receive a repeat
// event within a specific duration ([_keydownCancelDurationMac]) we assume
// event within a specific duration (_kKeydownCancelDurationMac) we assume
// the user has released the key and we synthesize a keyup event.
bool _shouldDoKeyGuard() {
return onMacOs;
return onDarwin;
}

/// After a keydown is received, this is the duration we wait for a repeat event
Expand Down
276 changes: 140 additions & 136 deletions lib/web_ui/test/keyboard_converter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -541,84 +541,86 @@ void testMain() {
);
});

testFakeAsync('CapsLock down synthesizes an immediate cancel on macOS', (FakeAsync async) {
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
keyDataList.add(key);
return true;
}, OperatingSystem.macOs);

// A KeyDown of ShiftRight is missed due to loss of focus.
converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.down,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

async.elapse(const Duration(microseconds: 1));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.up,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
synthesized: true,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

converter.handleEvent(keyUpEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.down,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

async.elapse(const Duration(microseconds: 1));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.up,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
synthesized: true,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

// Another key down works
converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.down,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
);
keyDataList.clear();


// Schedules are canceled after disposal
converter.dispose();
async.elapse(const Duration(seconds: 10));
expect(keyDataList, isEmpty);
});
for (final OperatingSystem system in <OperatingSystem>[OperatingSystem.macOs, OperatingSystem.iOs]) {
testFakeAsync('CapsLock down synthesizes an immediate cancel on $system', (FakeAsync async) {
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
keyDataList.add(key);
return true;
}, system);

// A KeyDown of ShiftRight is missed due to loss of focus.
converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.down,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

async.elapse(const Duration(microseconds: 1));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.up,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
synthesized: true,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

converter.handleEvent(keyUpEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.down,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

async.elapse(const Duration(microseconds: 1));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.up,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
synthesized: true,
);
expect(MockKeyboardEvent.lastDefaultPrevented, isTrue);
keyDataList.clear();

// Another key down works
converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
expectKeyData(keyDataList.last,
type: ui.KeyEventType.down,
physical: kPhysicalCapsLock,
logical: kLogicalCapsLock,
character: null,
);
keyDataList.clear();


// Schedules are canceled after disposal
converter.dispose();
async.elapse(const Duration(seconds: 10));
expect(keyDataList, isEmpty);
});
}

testFakeAsync('CapsLock behaves normally on non-macOS', (FakeAsync async) {
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
keyDataList.add(key);
return true;
}, OperatingSystem.linux); // onMacOs: false
}, OperatingSystem.linux);

converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock'));
expect(keyDataList, hasLength(1));
Expand Down Expand Up @@ -663,69 +665,71 @@ void testMain() {
);
});

testFakeAsync('Key guards: key down events are guarded on macOS', (FakeAsync async) {
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
keyDataList.add(key);
return true;
}, OperatingSystem.macOs);

converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100);
async.elapse(const Duration(milliseconds: 100));

converter.handleEvent(keyDownEvent('KeyA', 'a', kMeta)..timeStamp = 200);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 200),
type: ui.KeyEventType.down,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: 'a',
);
keyDataList.clear();

// Keyup of KeyA is omitted due to being a shortcut.

async.elapse(const Duration(milliseconds: 2500));
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2200),
type: ui.KeyEventType.up,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: null,
synthesized: true,
);
keyDataList.clear();

converter.handleEvent(keyUpEvent('MetaLeft', 'Meta', 0, kLocationLeft)..timeStamp = 2700);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2700),
type: ui.KeyEventType.up,
physical: kPhysicalMetaLeft,
logical: kLogicalMetaLeft,
character: null,
);
async.elapse(const Duration(milliseconds: 100));

// Key A states are cleared
converter.handleEvent(keyDownEvent('KeyA', 'a')..timeStamp = 2800);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2800),
type: ui.KeyEventType.down,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: 'a',
);
async.elapse(const Duration(milliseconds: 100));

converter.handleEvent(keyUpEvent('KeyA', 'a')..timeStamp = 2900);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2900),
type: ui.KeyEventType.up,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: null,
);
});
for (final OperatingSystem system in <OperatingSystem>[OperatingSystem.macOs, OperatingSystem.iOs]) {
testFakeAsync('Key guards: key down events are guarded on $system', (FakeAsync async) {
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
keyDataList.add(key);
return true;
}, system);

converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100);
async.elapse(const Duration(milliseconds: 100));

converter.handleEvent(keyDownEvent('KeyA', 'a', kMeta)..timeStamp = 200);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 200),
type: ui.KeyEventType.down,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: 'a',
);
keyDataList.clear();

// Keyup of KeyA is omitted due to being a shortcut.

async.elapse(const Duration(milliseconds: 2500));
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2200),
type: ui.KeyEventType.up,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: null,
synthesized: true,
);
keyDataList.clear();

converter.handleEvent(keyUpEvent('MetaLeft', 'Meta', 0, kLocationLeft)..timeStamp = 2700);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2700),
type: ui.KeyEventType.up,
physical: kPhysicalMetaLeft,
logical: kLogicalMetaLeft,
character: null,
);
async.elapse(const Duration(milliseconds: 100));

// Key A states are cleared
converter.handleEvent(keyDownEvent('KeyA', 'a')..timeStamp = 2800);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2800),
type: ui.KeyEventType.down,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: 'a',
);
async.elapse(const Duration(milliseconds: 100));

converter.handleEvent(keyUpEvent('KeyA', 'a')..timeStamp = 2900);
expectKeyData(keyDataList.last,
timeStamp: const Duration(milliseconds: 2900),
type: ui.KeyEventType.up,
physical: kPhysicalKeyA,
logical: kLogicalKeyA,
character: null,
);
});
}

testFakeAsync('Key guards: key repeated down events refreshes guards', (FakeAsync async) {
final List<ui.KeyData> keyDataList = <ui.KeyData>[];
Expand Down Expand Up @@ -795,7 +799,7 @@ void testMain() {
final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) {
keyDataList.add(key);
return true;
}, OperatingSystem.linux);
}, OperatingSystem.macOs);

converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100);
async.elapse(const Duration(milliseconds: 100));
Expand Down