[Keyboard, Web] Map from "Esc" to the Escape key (#106133)

* Impl

* Fix build

* Add test
diff --git a/dev/tools/gen_keycodes/data/logical_key_data.json b/dev/tools/gen_keycodes/data/logical_key_data.json
index 49c5896..289a3b7 100644
--- a/dev/tools/gen_keycodes/data/logical_key_data.json
+++ b/dev/tools/gen_keycodes/data/logical_key_data.json
@@ -1552,7 +1552,8 @@
     "value": 4294967323,
     "names": {
       "web": [
-        "Escape"
+        "Escape",
+        "Esc"
       ],
       "macos": [
         "Escape"
diff --git a/dev/tools/gen_keycodes/data/physical_key_data.json b/dev/tools/gen_keycodes/data/physical_key_data.json
index 1f0c526..270c054 100644
--- a/dev/tools/gen_keycodes/data/physical_key_data.json
+++ b/dev/tools/gen_keycodes/data/physical_key_data.json
@@ -1192,6 +1192,9 @@
       "name": "Escape",
       "chromium": "Escape"
     },
+    "otherWebCodes": [
+      "Esc"
+    ],
     "scanCodes": {
       "android": [
         1
diff --git a/dev/tools/gen_keycodes/data/supplemental_hid_codes.inc b/dev/tools/gen_keycodes/data/supplemental_hid_codes.inc
index 8917dcb..8b66cc6 100644
--- a/dev/tools/gen_keycodes/data/supplemental_hid_codes.inc
+++ b/dev/tools/gen_keycodes/data/supplemental_hid_codes.inc
@@ -50,6 +50,11 @@
   DOM_CODE(0x05ff1e, 0x0000, 0x0000, 0x0000, 0xffff, "GameButtonY", BUTTON_Y),
   DOM_CODE(0x05ff1f, 0x0000, 0x0000, 0x0000, 0xffff, "GameButtonZ", BUTTON_Z),
 
+  // Sometimes the Escape key produces "Esc" instead of "Escape". This includes
+  // older IE and Firefox browsers, and the current Cobalt browser.
+  // See: https://github.com/flutter/flutter/issues/106062
+  DOM_CODE(0x070029, 0x0000, 0x0000, 0x0000, 0xffff, "Esc", ESCAPE),
+
   // ============================================================
   // Fn key for Mac
   // ============================================================
@@ -58,4 +63,4 @@
   // defined on other platforms. Chromium does define an "Fn" row, but doesn't
   // give it a Mac keycode. This overrides their definition.
   //        USB HID    evdev    XKB     Win    Mac  DOMKey         Code
-   DOM_CODE(0x000012, 0x0000, 0x0000, 0x0000, 0x003f, "Fn", FN),
+  DOM_CODE(0x000012, 0x0000, 0x0000, 0x0000, 0x003f, "Fn", FN),
diff --git a/dev/tools/gen_keycodes/data/supplemental_key_data.inc b/dev/tools/gen_keycodes/data/supplemental_key_data.inc
index c7c38af..c928dc2 100644
--- a/dev/tools/gen_keycodes/data/supplemental_key_data.inc
+++ b/dev/tools/gen_keycodes/data/supplemental_key_data.inc
@@ -76,6 +76,16 @@
   DOM_KEY_UNI("Tilde",                  TILDE,                  '~'),
   DOM_KEY_UNI("Bar",                    BAR,                    '|'),
 
+  // ============================================================
+  // Unprintable keys (Unicode plane)
+  // ============================================================
+
+  //          Key                       Enum                    Value
+  // Sometimes the Escape key produces "Esc" instead of "Escape". This includes
+  // older IE and Firefox browsers, and the current Cobalt browser.
+  // See: https://github.com/flutter/flutter/issues/106062
+  DOM_KEY_MAP("Esc",                    ESC,                    0x1B),
+
   // The following keys reside in the Flutter plane (0x0100000000).
 
   // ============================================================
diff --git a/dev/tools/gen_keycodes/lib/keyboard_keys_code_gen.dart b/dev/tools/gen_keycodes/lib/keyboard_keys_code_gen.dart
index dbc7761..1e35429 100644
--- a/dev/tools/gen_keycodes/lib/keyboard_keys_code_gen.dart
+++ b/dev/tools/gen_keycodes/lib/keyboard_keys_code_gen.dart
@@ -78,7 +78,7 @@
 
   /// Gets the generated definitions of LogicalKeyboardKeys.
   String get _logicalDefinitions {
-    final OutputLines<int> lines = OutputLines<int>('Logical debug names');
+    final OutputLines<int> lines = OutputLines<int>('Logical debug names', behavior: DeduplicateBehavior.kSkip);
     void printKey(int flutterId, String constantName, String commentName, {String? otherComments}) {
       final String firstComment = _wrapString('Represents the logical "$commentName" key on the keyboard.');
       otherComments ??= _wrapString('See the function [RawKeyEvent.logicalKey] for more information.');
@@ -122,7 +122,7 @@
   }
 
   String get _logicalKeyLabels {
-    final OutputLines<int> lines = OutputLines<int>('Logical key labels');
+    final OutputLines<int> lines = OutputLines<int>('Logical key labels', behavior: DeduplicateBehavior.kSkip);
     for (final LogicalKeyEntry entry in logicalData.entries) {
       lines.add(entry.value, '''
     ${toHex(entry.value, digits: 11)}: '${entry.commentName}',''');
@@ -141,7 +141,7 @@
 
   /// This generates the map of Flutter key codes to logical keys.
   String get _predefinedKeyCodeMap {
-    final OutputLines<int> lines = OutputLines<int>('Logical key map');
+    final OutputLines<int> lines = OutputLines<int>('Logical key map', behavior: DeduplicateBehavior.kSkip);
     for (final LogicalKeyEntry entry in logicalData.entries) {
       lines.add(entry.value, '    ${toHex(entry.value, digits: 11)}: ${entry.constantName},');
     }
@@ -149,7 +149,7 @@
   }
 
   String get _maskConstantVariables {
-    final OutputLines<int> lines = OutputLines<int>('Mask constants', checkDuplicate: false);
+    final OutputLines<int> lines = OutputLines<int>('Mask constants', behavior: DeduplicateBehavior.kKeep);
     for (final MaskConstant constant in _maskConstants) {
       lines.add(constant.value, '''
 ${_wrapString(constant.description)}  ///
diff --git a/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart b/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
index 39d4dc2..28e78b7 100644
--- a/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
+++ b/dev/tools/gen_keycodes/lib/keyboard_maps_code_gen.dart
@@ -303,7 +303,7 @@
 
   /// This generates the map of Web KeyboardEvent codes to physical keys.
   String get _webPhysicalKeyMap {
-    final OutputLines<String> lines = OutputLines<String>('Web physical key map');
+    final OutputLines<String> lines = OutputLines<String>('Web physical key map', behavior: DeduplicateBehavior.kKeep);
     for (final PhysicalKeyEntry entry in keyData.entries) {
       for (final String webCodes in entry.webCodes()) {
         lines.add(entry.name, "  '$webCodes': PhysicalKeyboardKey.${entry.constantName},");
diff --git a/dev/tools/gen_keycodes/lib/logical_key_data.dart b/dev/tools/gen_keycodes/lib/logical_key_data.dart
index 5439e92..d943192 100644
--- a/dev/tools/gen_keycodes/lib/logical_key_data.dart
+++ b/dev/tools/gen_keycodes/lib/logical_key_data.dart
@@ -53,8 +53,7 @@
     String glfwNameMap,
     PhysicalKeyData physicalKeyData,
   ) {
-    final Map<String, LogicalKeyEntry> data = <String, LogicalKeyEntry>{};
-    _readKeyEntries(data, chromiumKeys);
+    final Map<String, LogicalKeyEntry> data = _readKeyEntries(chromiumKeys);
     _readWindowsKeyCodes(data, windowsKeyCodeHeader, parseMapOfListOfString(windowsNameMap));
     _readGtkKeyCodes(data, gtkKeyCodeHeader, parseMapOfListOfString(gtkNameMap));
     _readAndroidKeyCodes(data, androidKeyCodeHeader, parseMapOfListOfString(androidNameMap));
@@ -130,7 +129,8 @@
   /// The following format should be mapped to the Flutter plane.
   ///                 Key       Enum       Character
   /// FLUTTER_KEY_MAP("Lang4",  LANG4,     0x00013),
-  static void _readKeyEntries(Map<String, LogicalKeyEntry> data, String input) {
+  static Map<String, LogicalKeyEntry> _readKeyEntries(String input) {
+    final Map<int, LogicalKeyEntry> dataByValue = <int, LogicalKeyEntry>{};
     final RegExp domKeyRegExp = RegExp(
       r'(?<source>DOM|FLUTTER)_KEY_(?<kind>UNI|MAP)\s*\(\s*'
       r'"(?<name>[^\s]+?)",\s*'
@@ -162,17 +162,23 @@
       }
 
       final bool isPrintable = keyLabel != null;
-      data.putIfAbsent(name, () {
-        final LogicalKeyEntry entry = LogicalKeyEntry.fromName(
-          value: toPlane(value, _sourceToPlane(source, isPrintable)),
+      final int entryValue = toPlane(value, _sourceToPlane(source, isPrintable));
+      final LogicalKeyEntry entry = dataByValue.putIfAbsent(entryValue, () =>
+        LogicalKeyEntry.fromName(
+          value: entryValue,
           name: name,
           keyLabel: keyLabel,
-        );
-        if (source == 'DOM' && !isPrintable)
-          entry.webNames.add(webName);
-        return entry;
-      });
+        ),
+      );
+      if (source == 'DOM' && !isPrintable) {
+        entry.webNames.add(webName);
+      }
     }
+    return Map<String, LogicalKeyEntry>.fromEntries(
+      dataByValue.values.map((LogicalKeyEntry entry) =>
+        MapEntry<String, LogicalKeyEntry>(entry.name, entry),
+      ),
+    );
   }
 
   static void _readMacOsKeyCodes(
diff --git a/dev/tools/gen_keycodes/lib/physical_key_data.dart b/dev/tools/gen_keycodes/lib/physical_key_data.dart
index 4982993..5bbe5ab 100644
--- a/dev/tools/gen_keycodes/lib/physical_key_data.dart
+++ b/dev/tools/gen_keycodes/lib/physical_key_data.dart
@@ -171,6 +171,22 @@
         // Skip key that is not actually generated by any keyboard.
         continue;
       }
+      final PhysicalKeyEntry? existing = entries[usbHidCode];
+      // Allow duplicate entries for Fn, which overwrites.
+      if (existing != null && existing.name != 'Fn') {
+        // If it's an existing entry, the only thing we currently support is
+        // to insert an extra DOMKey. The other entries must be empty.
+        assert(evdevCode == 0
+            && xKbScanCode == 0
+            && windowsScanCode == 0
+            && macScanCode == 0xffff
+            && chromiumCode != null
+            && chromiumCode.isNotEmpty,
+            'Duplicate usbHidCode ${existing.usbHidCode} of key ${existing.name} '
+            'conflicts with existing ${entries[existing.usbHidCode]!.name}.');
+        existing.otherWebCodes.add(chromiumCode!);
+        continue;
+      }
       final PhysicalKeyEntry newEntry = PhysicalKeyEntry(
         usbHidCode: usbHidCode,
         androidScanCodes: nameToAndroidScanCodes[name] ?? <int>[],
@@ -182,15 +198,6 @@
         name: name,
         chromiumCode: chromiumCode,
       );
-      // Remove duplicates: last one wins, so that supplemental codes
-      // override.
-      if (entries.containsKey(newEntry.usbHidCode)) {
-        // This is expected for Fn. Warn for other keys.
-        if (newEntry.name != 'Fn') {
-          print('Duplicate usbHidCode ${newEntry.usbHidCode} of key ${newEntry.name} '
-            'conflicts with existing ${entries[newEntry.usbHidCode]!.name}. Keeping the new one.');
-        }
-      }
       entries[newEntry.usbHidCode] = newEntry;
     }
     return entries.map((int code, PhysicalKeyEntry entry) =>
@@ -216,7 +223,8 @@
     required this.macOSScanCode,
     required this.iOSScanCode,
     required this.chromiumCode,
-  });
+    List<String>? otherWebCodes,
+  }) : otherWebCodes = otherWebCodes ?? <String>[];
 
   /// Populates the key from a JSON map.
   factory PhysicalKeyEntry.fromJsonMapEntry(Map<String, dynamic> map) {
@@ -232,6 +240,7 @@
       windowsScanCode: scanCodes['windows'] as int?,
       macOSScanCode: scanCodes['macos'] as int?,
       iOSScanCode: scanCodes['ios'] as int?,
+      otherWebCodes: (map['otherWebCodes'] as List<dynamic>?)?.cast<String>(),
     );
   }
 
@@ -258,11 +267,14 @@
   final String name;
   /// The Chromium event code for the key.
   final String? chromiumCode;
+  /// Other codes used by Web besides chromiumCode.
+  final List<String> otherWebCodes;
 
   Iterable<String> webCodes() sync* {
     if (chromiumCode != null) {
       yield chromiumCode!;
     }
+    yield* otherWebCodes;
   }
 
   /// Creates a JSON map from the key data.
@@ -272,6 +284,7 @@
         'name': name,
         'chromium': chromiumCode,
       },
+      'otherWebCodes': otherWebCodes,
       'scanCodes': <String, dynamic>{
         'android': androidScanCodes,
         'usb': usbHidCode,
@@ -323,11 +336,14 @@
 
   @override
   String toString() {
+    final String otherWebStr = otherWebCodes.isEmpty
+        ? ''
+        : ', otherWebCodes: [${otherWebCodes.join(', ')}]';
     return """'$constantName': (name: "$name", usbHidCode: ${toHex(usbHidCode)}, """
         'linuxScanCode: ${toHex(evdevCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
         'windowsKeyCode: ${toHex(windowsScanCode)}, macOSScanCode: ${toHex(macOSScanCode)}, '
         'windowsScanCode: ${toHex(windowsScanCode)}, chromiumSymbolName: $chromiumCode '
-        'iOSScanCode: ${toHex(iOSScanCode)})';
+        'iOSScanCode: ${toHex(iOSScanCode)})$otherWebStr';
   }
 
   static int compareByUsbHidCode(PhysicalKeyEntry a, PhysicalKeyEntry b) =>
diff --git a/dev/tools/gen_keycodes/lib/testing_key_codes_cc_gen.dart b/dev/tools/gen_keycodes/lib/testing_key_codes_cc_gen.dart
index 15d9388..69cb48f 100644
--- a/dev/tools/gen_keycodes/lib/testing_key_codes_cc_gen.dart
+++ b/dev/tools/gen_keycodes/lib/testing_key_codes_cc_gen.dart
@@ -30,7 +30,7 @@
 
   /// Gets the generated definitions of PhysicalKeyboardKeys.
   String get _logicalDefinitions {
-    final OutputLines<int> lines = OutputLines<int>('Logical Key list');
+    final OutputLines<int> lines = OutputLines<int>('Logical Key list', behavior: DeduplicateBehavior.kSkip);
     for (final LogicalKeyEntry entry in logicalData.entries) {
       lines.add(entry.value, '''
 constexpr uint64_t kLogical${_toUpperCammel(entry.constantName)} = ${toHex(entry.value, digits: 11)};''');
diff --git a/dev/tools/gen_keycodes/lib/testing_key_codes_java_gen.dart b/dev/tools/gen_keycodes/lib/testing_key_codes_java_gen.dart
index 281f6c2..bd1bd80 100644
--- a/dev/tools/gen_keycodes/lib/testing_key_codes_java_gen.dart
+++ b/dev/tools/gen_keycodes/lib/testing_key_codes_java_gen.dart
@@ -42,7 +42,7 @@
 
   /// Gets the generated definitions of PhysicalKeyboardKeys.
   String get _logicalDefinitions {
-    final OutputLines<int> lines = OutputLines<int>('Logical Key list');
+    final OutputLines<int> lines = OutputLines<int>('Logical Key list', behavior: DeduplicateBehavior.kSkip);
     for (final LogicalKeyEntry entry in logicalData.entries) {
       lines.add(entry.value, '''
   public static final long LOGICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.value, digits: 11)}L;''');
diff --git a/dev/tools/gen_keycodes/lib/utils.dart b/dev/tools/gen_keycodes/lib/utils.dart
index 138fcdf..9e9b0b8 100644
--- a/dev/tools/gen_keycodes/lib/utils.dart
+++ b/dev/tools/gen_keycodes/lib/utils.dart
@@ -233,12 +233,24 @@
   }
 }
 
+enum DeduplicateBehavior {
+  // Warn the duplicate entry.
+  kWarn,
+
+  // Skip the latter duplicate entry.
+  kSkip,
+
+  // Keep all duplicate entries.
+  kKeep,
+}
+
 /// The information for a line used by [OutputLines].
 class OutputLine<T extends Comparable<Object>> {
-  const OutputLine(this.key, this.value);
+  OutputLine(this.key, String value)
+    : values = <String>[value];
 
   final T key;
-  final String value;
+  final List<String> values;
 }
 
 /// A utility class to build join a number of lines in a sorted order.
@@ -247,41 +259,43 @@
 /// get the joined string of these lines joined sorting them in the order of the
 /// index.
 class OutputLines<T extends Comparable<Object>> {
-  OutputLines(this.mapName, {this.checkDuplicate = true});
+  OutputLines(this.mapName, {this.behavior = DeduplicateBehavior.kWarn});
 
-  /// If true, then lines with duplicate keys will be warned and discarded.
-  ///
-  /// Default to true.
-  final bool checkDuplicate;
+  /// What to do if there are entries with the same key.
+  final DeduplicateBehavior behavior;
 
   /// The name for this map.
   ///
   /// Used in warning messages.
   final String mapName;
 
-  final Set<T> keys = <T>{};
-  final List<OutputLine<T>> lines = <OutputLine<T>>[];
+  final Map<T, OutputLine<T>> lines = <T, OutputLine<T>>{};
 
-  void add(T code, String line) {
-    if (checkDuplicate) {
-      if (keys.contains(code)) {
-        final OutputLine<T> existing = lines.firstWhere((OutputLine<T> line) => line.key == code);
-        print('Warn: $mapName is requested to add line $code as:\n    $line\n  but it already exists as:\n    ${existing.value}');
-        return;
+  void add(T key, String line) {
+    final OutputLine<T>? existing = lines[key];
+    if (existing != null) {
+      switch (behavior) {
+        case DeduplicateBehavior.kWarn:
+          print('Warn: Request to add $key to map "$mapName" as:\n    $line\n  but it already exists as:\n    ${existing.values[0]}');
+          return;
+        case DeduplicateBehavior.kSkip:
+          return;
+        case DeduplicateBehavior.kKeep:
+          existing.values.add(line);
+          return;
       }
-      keys.add(code);
     }
-    lines.add(OutputLine<T>(code, line));
+    lines[key] = OutputLine<T>(key, line);
   }
 
   String join() {
-    return lines.map((OutputLine<T> line) => line.value).join('\n');
+    return lines.values.map((OutputLine<T> line) => line.values.join('\n')).join('\n');
   }
 
   String sortedJoin() {
-    return (lines.sublist(0)
+    return (lines.values.toList()
       ..sort((OutputLine<T> a, OutputLine<T> b) => a.key.compareTo(b.key)))
-      .map((OutputLine<T> line) => line.value)
+      .map((OutputLine<T> line) => line.values.join('\n'))
       .join('\n');
   }
 }
diff --git a/packages/flutter/lib/src/services/keyboard_maps.dart b/packages/flutter/lib/src/services/keyboard_maps.dart
index 0c26814..6d34631 100644
--- a/packages/flutter/lib/src/services/keyboard_maps.dart
+++ b/packages/flutter/lib/src/services/keyboard_maps.dart
@@ -2211,6 +2211,7 @@
   'EndCall': LogicalKeyboardKey.endCall,
   'Enter': LogicalKeyboardKey.enter,
   'EraseEof': LogicalKeyboardKey.eraseEof,
+  'Esc': LogicalKeyboardKey.escape,
   'Escape': LogicalKeyboardKey.escape,
   'ExSel': LogicalKeyboardKey.exSel,
   'Execute': LogicalKeyboardKey.execute,
@@ -2495,6 +2496,7 @@
   'Enter': PhysicalKeyboardKey.enter,
   'Equal': PhysicalKeyboardKey.equal,
   'Escape': PhysicalKeyboardKey.escape,
+  'Esc': PhysicalKeyboardKey.escape,
   'F1': PhysicalKeyboardKey.f1,
   'F10': PhysicalKeyboardKey.f10,
   'F11': PhysicalKeyboardKey.f11,
diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart
index 69b4b0f..5cc53d8 100644
--- a/packages/flutter/test/services/raw_keyboard_test.dart
+++ b/packages/flutter/test/services/raw_keyboard_test.dart
@@ -2680,6 +2680,23 @@
       expect(data.keyCode, equals(0x10));
     });
 
+    test('Esc keys generated by older browsers are correctly translated', () {
+      final RawKeyEvent escapeKeyEvent = RawKeyEvent.fromMessage(const <String, Object?>{
+        'type': 'keydown',
+        'keymap': 'web',
+        'code': 'Esc',
+        'key': 'Esc',
+        'location': 0,
+        'metaState': 0x0,
+        'keyCode': 0x1B,
+      });
+      final RawKeyEventDataWeb data = escapeKeyEvent.data as RawKeyEventDataWeb;
+      expect(data.physicalKey, equals(PhysicalKeyboardKey.escape));
+      expect(data.logicalKey, equals(LogicalKeyboardKey.escape));
+      expect(data.keyLabel, isEmpty);
+      expect(data.keyCode, equals(0x1B));
+    });
+
     test('Arrow keys from a keyboard give correct physical key mappings', () {
       final RawKeyEvent arrowKeyDown = RawKeyEvent.fromMessage(const <String, Object?>{
         'type': 'keydown',