From b5fc79f9ec145c4ea800c057d3f69e4feefcc4b8 Mon Sep 17 00:00:00 2001 From: xubaolin Date: Fri, 11 Jun 2021 02:39:04 +0800 Subject: [PATCH] TextField terminal the enter and space raw key events by default (#82671) --- .../flutter/lib/src/cupertino/text_field.dart | 55 ++++++++++++++-- .../flutter/lib/src/material/text_field.dart | 46 +++++++++++++ .../test/cupertino/text_field_test.dart | 65 +++++++++++++++++++ .../test/material/text_field_test.dart | 63 ++++++++++++++++++ 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 7b7489bc74..ba5ed4035e 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -507,6 +507,30 @@ class CupertinoTextField extends StatefulWidget { final TextEditingController? controller; /// {@macro flutter.widgets.Focus.focusNode} + /// + /// ## Key handling + /// + /// By default, [CupertinoTextField] absorbs key events of the Space key and + /// Enter key, because they are commonly used as both shortcuts and text field + /// inputs. This means that, if these keys are pressed when + /// [CupertinoTextField] is the primary focus, they will not be sent to other + /// enclosing widgets. + /// + /// If [FocusNode.onKey] is not null, this filter is bypassed. In the likely + /// case that this filter is still desired, check these keys and return + /// [KeyEventResult.skipRemainingHandlers]. + /// + /// ```dart + /// final FocusNode focusNode = FocusNode( + /// onKey: (FocusNode node, RawKeyEvent event) { + /// if (event.logicalKey == LogicalKeyboardKey.space + /// || event.logicalKey == LogicalKeyboardKey.enter) { + /// return KeyEventResult.skipRemainingHandlers; + /// } + /// // Now process the event as desired. + /// }, + /// ); + /// ``` final FocusNode? focusNode; /// Controls the [BoxDecoration] of the box behind the text input. @@ -1067,11 +1091,25 @@ class _CupertinoTextFieldState extends State with Restoratio ); } + KeyEventResult _handleRawKeyEvent(FocusNode node, RawKeyEvent event) { + assert(node.hasFocus); + // TextField uses "enter" to finish the input or create a new line, and "space" as + // a normal input character, so we default to terminate the handling of these + // two keys to avoid ancestor behaving incorrectly for handling the two keys + // (such as `ListTile` or `Material`). + if (event.logicalKey == LogicalKeyboardKey.space + || event.logicalKey == LogicalKeyboardKey.enter) { + return KeyEventResult.skipRemainingHandlers; + } + return KeyEventResult.ignored; + } + @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. assert(debugCheckHasDirectionality(context)); final TextEditingController controller = _effectiveController; + final FocusNode focusNode = _effectiveFocusNode; TextSelectionControls? textSelectionControls = widget.selectionControls; VoidCallback? handleDidGainAccessibilityFocus; @@ -1089,8 +1127,8 @@ class _CupertinoTextFieldState extends State with Restoratio handleDidGainAccessibilityFocus = () { // macOS automatically activated the TextField when it receives // accessibility focus. - if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { - _effectiveFocusNode.requestFocus(); + if (!focusNode.hasFocus && focusNode.canRequestFocus) { + focusNode.requestFocus(); } }; break; @@ -1153,7 +1191,7 @@ class _CupertinoTextFieldState extends State with Restoratio final Color selectionColor = CupertinoTheme.of(context).primaryColor.withOpacity(0.2); - final Widget paddedEditable = Padding( + Widget paddedEditable = Padding( padding: widget.padding, child: RepaintBoundary( child: UnmanagedRestorationScope( @@ -1165,7 +1203,7 @@ class _CupertinoTextFieldState extends State with Restoratio toolbarOptions: widget.toolbarOptions, showCursor: widget.showCursor, showSelectionHandles: _showSelectionHandles, - focusNode: _effectiveFocusNode, + focusNode: focusNode, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, textCapitalization: widget.textCapitalization, @@ -1215,6 +1253,15 @@ class _CupertinoTextFieldState extends State with Restoratio ), ); + if (focusNode.onKey == null) { + paddedEditable = Focus( + onKey: _handleRawKeyEvent, + includeSemantics: false, + skipTraversal: true, + child: paddedEditable, + ); + } + return Semantics( enabled: enabled, onTap: !enabled || widget.readOnly ? null : () { diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index cdcad90f91..10d2b04ecc 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -486,6 +486,30 @@ class TextField extends StatefulWidget { /// /// This widget builds an [EditableText] and will ensure that the keyboard is /// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. + /// + /// ## Key handling + /// + /// By default, [TextField] absorbs key events of the Space key and Enter key, + /// because they are commonly used as both shortcuts and text field inputs. + /// This means that, if these keys are pressed when [TextField] is the + /// primary focus, they will not be sent to other widgets (such as triggering + /// an enclosing [ListTile]). + /// + /// If [FocusNode.onKey] is not null, this filter is bypassed. In the likely + /// case that this filter is still desired, check these keys and return + /// [KeyEventResult.skipRemainingHandlers]. + /// + /// ```dart + /// final FocusNode focusNode = FocusNode( + /// onKey: (FocusNode node, RawKeyEvent event) { + /// if (event.logicalKey == LogicalKeyboardKey.space + /// || event.logicalKey == LogicalKeyboardKey.enter) { + /// return KeyEventResult.skipRemainingHandlers; + /// } + /// // Now process the event as desired. + /// }, + /// ); + /// ``` final FocusNode? focusNode; /// The decoration to show around the text field. @@ -1111,6 +1135,19 @@ class _TextFieldState extends State with RestorationMixin implements } } + KeyEventResult _handleRawKeyEvent(FocusNode node, RawKeyEvent event) { + assert(node.hasFocus); + // TextField uses "enter" to finish the input or create a new line, and "space" as + // a normal input character, so we default to terminate the handling of these + // two keys to avoid ancestor behaving incorrectly for handling the two keys + // (such as `ListTile` or `Material`). + if (event.logicalKey == LogicalKeyboardKey.space + || event.logicalKey == LogicalKeyboardKey.enter) { + return KeyEventResult.skipRemainingHandlers; + } + return KeyEventResult.ignored; + } + @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -1263,6 +1300,15 @@ class _TextFieldState extends State with RestorationMixin implements ), ); + if (focusNode.onKey == null) { + child = Focus( + onKey: _handleRawKeyEvent, + includeSemantics: false, + skipTraversal: true, + child: child, + ); + } + if (widget.decoration != null) { child = AnimatedBuilder( animation: Listenable.merge([ focusNode, controller ]), diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index daf43f33ea..5404ddb873 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -4779,4 +4779,69 @@ void main() { expect(disabledColor, isSameColorAs(const Color(0xFFFAFAFA))); }, ); + + // Regression test for https://github.com/flutter/flutter/issues/81233 + testWidgets('CupertinoTextField should terminate the `space` and `enter` raw key events by default', (WidgetTester tester) async { + final Set outerReceivedAnEvent = {}; + final FocusNode outerFocusNode = FocusNode(debugLabel: 'outerFocusNode'); + KeyEventResult outerHandleEvent(FocusNode node, RawKeyEvent event) { + outerReceivedAnEvent.add(node); + return KeyEventResult.handled; + } + outerFocusNode.onKey = outerHandleEvent; + + final Set innerReceivedAnEvent = {}; + final FocusNode innerFocusNode = FocusNode(debugLabel: 'innerFocusNode'); + + Future sendEvent(LogicalKeyboardKey key) async { + await tester.sendKeyEvent(key, platform: 'windows'); + } + + Widget buildFrame() { + return CupertinoApp( + home: Center( + child: Focus( + onKey: outerFocusNode.onKey, + focusNode: outerFocusNode, + child: CupertinoTextField( + focusNode: innerFocusNode, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + innerFocusNode.requestFocus(); + await tester.pump(); + + // The inner TextField's focus node terminal the raw key event by default. + await sendEvent(LogicalKeyboardKey.space); + expect(outerReceivedAnEvent.length, 0); + + await sendEvent(LogicalKeyboardKey.enter); + expect(outerReceivedAnEvent.length, 0); + + // The `onKey` of the focus node of the TextField can be customized. + KeyEventResult innerHandleEvent(FocusNode node, RawKeyEvent event) { + innerReceivedAnEvent.add(node); + // The key event has not been handled, and the event should continue to be + // propagated to the outer key event handlers. + return KeyEventResult.ignored; + } + innerFocusNode.onKey = innerHandleEvent; + await tester.pumpWidget(buildFrame()); + + await sendEvent(LogicalKeyboardKey.space); + + expect(innerReceivedAnEvent.length, 1); + expect(outerReceivedAnEvent.length, 1); + + outerReceivedAnEvent.clear(); + innerReceivedAnEvent.clear(); + + await sendEvent(LogicalKeyboardKey.enter); + expect(outerReceivedAnEvent.length, 1); + expect(innerReceivedAnEvent.length, 1); + }, skip: kIsWeb); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7efe682751..178dade549 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -4537,6 +4537,69 @@ void main() { await tester.pump(); } + // Regression test for https://github.com/flutter/flutter/issues/81233 + testWidgets('TextField should terminate the `space` and `enter` raw key events by default', (WidgetTester tester) async { + final Set outerReceivedAnEvent = {}; + final FocusNode outerFocusNode = FocusNode(); + KeyEventResult outerHandleEvent(FocusNode node, RawKeyEvent event) { + outerReceivedAnEvent.add(node); + return KeyEventResult.handled; + } + outerFocusNode.onKey = outerHandleEvent; + + final Set innerReceivedAnEvent = {}; + final FocusNode innerFocusNode = FocusNode(); + + Future sendEvent(LogicalKeyboardKey key) async { + await tester.sendKeyEvent(key, platform: 'windows'); + } + + Widget buildFrame() { + return MaterialApp( + home: Material( + child: Focus( + onKey: outerFocusNode.onKey, + focusNode: outerFocusNode, + child: TextField( + focusNode: innerFocusNode, + ), + ), + ), + ); + } + await tester.pumpWidget(buildFrame()); + innerFocusNode.requestFocus(); + await tester.pump(); + + // The inner TextField's focus node terminal the raw key event by default. + await sendEvent(LogicalKeyboardKey.space); + expect(outerReceivedAnEvent.length, 0); + + await sendEvent(LogicalKeyboardKey.enter); + expect(outerReceivedAnEvent.length, 0); + + // The `onKey` of the focus node of the TextField can be customized. + KeyEventResult innerHandleEvent(FocusNode node, RawKeyEvent event) { + innerReceivedAnEvent.add(node); + // The key event has not been handled, and the event should continue to be + // propagated to the outer key event handlers. + return KeyEventResult.ignored; + } + innerFocusNode.onKey = innerHandleEvent; + await tester.pumpWidget(buildFrame()); + + await sendEvent(LogicalKeyboardKey.space); + expect(outerReceivedAnEvent.length, 1); + expect(innerReceivedAnEvent.length, 1); + + outerReceivedAnEvent.clear(); + innerReceivedAnEvent.clear(); + + await sendEvent(LogicalKeyboardKey.enter); + expect(outerReceivedAnEvent.length, 1); + expect(innerReceivedAnEvent.length, 1); + }, skip: areKeyEventsHandledByPlatform); + testWidgets('Shift test 1', (WidgetTester tester) async { await setupWidget(tester); const String testValue = 'a big house';