TextField terminal the enter and space raw key events by default (#82671)
This commit is contained in:
parent
a0d801f7f7
commit
b5fc79f9ec
@ -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<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
|
||||
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<CupertinoTextField> 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<CupertinoTextField> 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<CupertinoTextField> 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<CupertinoTextField> 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 : () {
|
||||
|
@ -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<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
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
@ -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) {
|
||||
child = AnimatedBuilder(
|
||||
animation: Listenable.merge(<Listenable>[ focusNode, controller ]),
|
||||
|
@ -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<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() {
|
||||
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 {
|
||||
await setupWidget(tester);
|
||||
const String testValue = 'a big house';
|
||||
|
Loading…
x
Reference in New Issue
Block a user