Impl and test (#37972)

diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart
index 92d5ec4..97b1a9d 100644
--- a/lib/web_ui/lib/src/engine/keyboard_binding.dart
+++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart
@@ -219,13 +219,13 @@
 // 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;
 
@@ -261,7 +261,7 @@
   // 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
@@ -272,10 +272,10 @@
   //
   // 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
diff --git a/lib/web_ui/test/keyboard_converter_test.dart b/lib/web_ui/test/keyboard_converter_test.dart
index 3bbbeb8..a5c81b3 100644
--- a/lib/web_ui/test/keyboard_converter_test.dart
+++ b/lib/web_ui/test/keyboard_converter_test.dart
@@ -541,84 +541,86 @@
     );
   });
 
-  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);
+  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();
+      // 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();
+      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();
+      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();
+      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();
+      // 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);
-  });
+      // 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));
@@ -663,69 +665,71 @@
     );
   });
 
-  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);
+  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('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();
+      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.
+      // 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();
+      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));
+      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));
+      // 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,
-    );
-  });
+      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>[];
@@ -795,7 +799,7 @@
     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));