[Property Editor] Can use "tab" key to submit text field inputs (#8841)

diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart
index c9dcc6a..6bc2a30 100644
--- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart
+++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_inputs.dart
@@ -2,7 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
 
+import 'dart:async';
+
 import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
@@ -152,15 +155,29 @@
 }
 
 class _TextInputState<T> extends State<_TextInput<T>>
-    with _PropertyInputMixin<_TextInput<T>, T> {
+    with _PropertyInputMixin<_TextInput<T>, T>, AutoDisposeMixin {
   String currentValue = '';
 
   double paddingDiffComparedToDropdown = 1.0;
+  late FocusNode _focusNode;
+
+  @override
+  void initState() {
+    super.initState();
+    _focusNode = FocusNode(debugLabel: 'text-input-${widget.property.name}');
+
+    addAutoDisposeListener(_focusNode, () async {
+      if (_focusNode.hasFocus) return;
+      // Edit property when clicking or tabbing away from input.
+      await _editProperty();
+    });
+  }
 
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
     return TextFormField(
+      focusNode: _focusNode,
       initialValue: widget.property.valueDisplay,
       enabled: widget.property.isEditable,
       autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -182,9 +199,6 @@
         });
       },
       onEditingComplete: _editProperty,
-      onTapOutside: (_) async {
-        await _editProperty();
-      },
     );
   }
 
diff --git a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor_test.dart b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor_test.dart
index 9bc2941..f4ada65 100644
--- a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor_test.dart
+++ b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor_test.dart
@@ -13,6 +13,7 @@
 import 'package:devtools_test/devtools_test.dart';
 import 'package:devtools_test/helpers.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/mockito.dart';
 
@@ -369,6 +370,31 @@
       });
     });
 
+    testWidgets('submitting a string input with TAB (title)', (tester) async {
+      return await tester.runAsync(() async {
+        // Load the property editor.
+        controller.initForTestsOnly(editableArgs: result1.args);
+        await tester.pumpWidget(wrap(propertyEditor));
+
+        // Edit the title.
+        final titleInput = _findTextFormField('title*');
+        expect(titleInput, findsOneWidget);
+        await _inputText(
+          titleInput,
+          text: 'Enter with TAB!',
+          tester: tester,
+          inputDoneKey: LogicalKeyboardKey.tab,
+        );
+
+        // Verify the edit is expected.
+        final nextEdit = await nextEditCompleter.future;
+        expect(
+          nextEdit,
+          equals('title: Enter with TAB! (TYPE: String, SUCCESS: true)'),
+        );
+      });
+    });
+
     testWidgets('editing a numeric input (height)', (tester) async {
       return await tester.runAsync(() async {
         // Load the property editor.
@@ -401,6 +427,27 @@
       });
     });
 
+    testWidgets('submitting a numeric input with TAB (height)', (tester) async {
+      return await tester.runAsync(() async {
+        // Load the property editor.
+        controller.initForTestsOnly(editableArgs: result1.args);
+        await tester.pumpWidget(wrap(propertyEditor));
+
+        // Edit the height.
+        final heightInput = _findTextFormField('height');
+        await _inputText(
+          heightInput,
+          text: '63.5',
+          tester: tester,
+          inputDoneKey: LogicalKeyboardKey.tab,
+        );
+
+        // Verify the edit is expected.
+        final nextEdit = await nextEditCompleter.future;
+        expect(nextEdit, equals('height: 63.5 (TYPE: double, SUCCESS: true)'));
+      });
+    });
+
     testWidgets('editing an enum input (align)', (tester) async {
       return await tester.runAsync(() async {
         // Load the property editor.
@@ -562,9 +609,14 @@
   Finder textFormField, {
   required String text,
   required WidgetTester tester,
+  LogicalKeyboardKey? inputDoneKey,
 }) async {
   await tester.enterText(textFormField, text);
-  await tester.testTextInput.receiveAction(TextInputAction.done);
+  if (inputDoneKey != null) {
+    await tester.sendKeyDownEvent(inputDoneKey);
+  } else {
+    await tester.testTextInput.receiveAction(TextInputAction.done);
+  }
   await tester.pump();
 }