Unverified Commit b5fc79f9 authored by xubaolin's avatar xubaolin Committed by GitHub

TextField terminal the enter and space raw key events by default (#82671)

parent a0d801f7
...@@ -507,6 +507,30 @@ class CupertinoTextField extends StatefulWidget { ...@@ -507,6 +507,30 @@ class CupertinoTextField extends StatefulWidget {
final TextEditingController? controller; final TextEditingController? controller;
/// {@macro flutter.widgets.Focus.focusNode} /// {@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; final FocusNode? focusNode;
/// Controls the [BoxDecoration] of the box behind the text input. /// Controls the [BoxDecoration] of the box behind the text input.
...@@ -1067,11 +1091,25 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1067,11 +1091,25 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
assert(debugCheckHasDirectionality(context)); assert(debugCheckHasDirectionality(context));
final TextEditingController controller = _effectiveController; final TextEditingController controller = _effectiveController;
final FocusNode focusNode = _effectiveFocusNode;
TextSelectionControls? textSelectionControls = widget.selectionControls; TextSelectionControls? textSelectionControls = widget.selectionControls;
VoidCallback? handleDidGainAccessibilityFocus; VoidCallback? handleDidGainAccessibilityFocus;
...@@ -1089,8 +1127,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1089,8 +1127,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
handleDidGainAccessibilityFocus = () { handleDidGainAccessibilityFocus = () {
// macOS automatically activated the TextField when it receives // macOS automatically activated the TextField when it receives
// accessibility focus. // accessibility focus.
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { if (!focusNode.hasFocus && focusNode.canRequestFocus) {
_effectiveFocusNode.requestFocus(); focusNode.requestFocus();
} }
}; };
break; break;
...@@ -1153,7 +1191,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1153,7 +1191,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
final Color selectionColor = CupertinoTheme.of(context).primaryColor.withOpacity(0.2); final Color selectionColor = CupertinoTheme.of(context).primaryColor.withOpacity(0.2);
final Widget paddedEditable = Padding( Widget paddedEditable = Padding(
padding: widget.padding, padding: widget.padding,
child: RepaintBoundary( child: RepaintBoundary(
child: UnmanagedRestorationScope( child: UnmanagedRestorationScope(
...@@ -1165,7 +1203,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1165,7 +1203,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
toolbarOptions: widget.toolbarOptions, toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor, showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles, showSelectionHandles: _showSelectionHandles,
focusNode: _effectiveFocusNode, focusNode: focusNode,
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction, textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
...@@ -1215,6 +1253,15 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1215,6 +1253,15 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
), ),
); );
if (focusNode.onKey == null) {
paddedEditable = Focus(
onKey: _handleRawKeyEvent,
includeSemantics: false,
skipTraversal: true,
child: paddedEditable,
);
}
return Semantics( return Semantics(
enabled: enabled, enabled: enabled,
onTap: !enabled || widget.readOnly ? null : () { onTap: !enabled || widget.readOnly ? null : () {
......
...@@ -486,6 +486,30 @@ class TextField extends StatefulWidget { ...@@ -486,6 +486,30 @@ class TextField extends StatefulWidget {
/// ///
/// This widget builds an [EditableText] and will ensure that the keyboard is /// This widget builds an [EditableText] and will ensure that the keyboard is
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. /// 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; final FocusNode? focusNode;
/// The decoration to show around the text field. /// The decoration to show around the text field.
...@@ -1111,6 +1135,19 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1111,6 +1135,19 @@ class _TextFieldState extends State<TextField> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -1263,6 +1300,15 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1263,6 +1300,15 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
), ),
); );
if (focusNode.onKey == null) {
child = Focus(
onKey: _handleRawKeyEvent,
includeSemantics: false,
skipTraversal: true,
child: child,
);
}
if (widget.decoration != null) { if (widget.decoration != null) {
child = AnimatedBuilder( child = AnimatedBuilder(
animation: Listenable.merge(<Listenable>[ focusNode, controller ]), animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
......
...@@ -4779,4 +4779,69 @@ void main() { ...@@ -4779,4 +4779,69 @@ void main() {
expect(disabledColor, isSameColorAs(const Color(0xFFFAFAFA))); 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<FocusNode> outerReceivedAnEvent = <FocusNode>{};
final FocusNode outerFocusNode = FocusNode(debugLabel: 'outerFocusNode');
KeyEventResult outerHandleEvent(FocusNode node, RawKeyEvent event) {
outerReceivedAnEvent.add(node);
return KeyEventResult.handled;
}
outerFocusNode.onKey = outerHandleEvent;
final Set<FocusNode> innerReceivedAnEvent = <FocusNode>{};
final FocusNode innerFocusNode = FocusNode(debugLabel: 'innerFocusNode');
Future<void> 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);
} }
...@@ -4537,6 +4537,69 @@ void main() { ...@@ -4537,6 +4537,69 @@ void main() {
await tester.pump(); 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<FocusNode> outerReceivedAnEvent = <FocusNode>{};
final FocusNode outerFocusNode = FocusNode();
KeyEventResult outerHandleEvent(FocusNode node, RawKeyEvent event) {
outerReceivedAnEvent.add(node);
return KeyEventResult.handled;
}
outerFocusNode.onKey = outerHandleEvent;
final Set<FocusNode> innerReceivedAnEvent = <FocusNode>{};
final FocusNode innerFocusNode = FocusNode();
Future<void> 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 { testWidgets('Shift test 1', (WidgetTester tester) async {
await setupWidget(tester); await setupWidget(tester);
const String testValue = 'a big house'; const String testValue = 'a big house';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment