mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Hook up soft keyboard "next" and "previous" buttons so that they move the focus by default (#63592)
Focus will be moved automatically if onEditingComplete is not specified, but must by moved manually if onEditingComplete is specified.
This commit is contained in:
parent
7f122c7429
commit
d1eff0b413
@ -1629,18 +1629,25 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// action; The newline is already inserted. Otherwise, finalize
|
||||
// editing.
|
||||
if (!_isMultiline)
|
||||
_finalizeEditing(true);
|
||||
_finalizeEditing(action, shouldUnfocus: true);
|
||||
break;
|
||||
case TextInputAction.done:
|
||||
case TextInputAction.go:
|
||||
case TextInputAction.send:
|
||||
case TextInputAction.next:
|
||||
case TextInputAction.previous:
|
||||
case TextInputAction.search:
|
||||
_finalizeEditing(true);
|
||||
case TextInputAction.send:
|
||||
_finalizeEditing(action, shouldUnfocus: true);
|
||||
break;
|
||||
default:
|
||||
case TextInputAction.continueAction:
|
||||
case TextInputAction.emergencyCall:
|
||||
case TextInputAction.join:
|
||||
case TextInputAction.none:
|
||||
case TextInputAction.route:
|
||||
case TextInputAction.unspecified:
|
||||
// Finalize editing, but don't give up focus because this keyboard
|
||||
// action does not imply the user is done inputting information.
|
||||
_finalizeEditing(false);
|
||||
_finalizeEditing(action, shouldUnfocus: false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1725,16 +1732,38 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
}
|
||||
}
|
||||
|
||||
void _finalizeEditing(bool shouldUnfocus) {
|
||||
void _finalizeEditing(TextInputAction action, {@required bool shouldUnfocus}) {
|
||||
// Take any actions necessary now that the user has completed editing.
|
||||
if (widget.onEditingComplete != null) {
|
||||
widget.onEditingComplete();
|
||||
} else {
|
||||
// Default behavior if the developer did not provide an
|
||||
// onEditingComplete callback: Finalize editing and remove focus.
|
||||
// onEditingComplete callback: Finalize editing and remove focus, or move
|
||||
// it to the next/previous field, depending on the action.
|
||||
widget.controller.clearComposing();
|
||||
if (shouldUnfocus)
|
||||
widget.focusNode.unfocus();
|
||||
if (shouldUnfocus) {
|
||||
switch (action) {
|
||||
case TextInputAction.none:
|
||||
case TextInputAction.unspecified:
|
||||
case TextInputAction.done:
|
||||
case TextInputAction.go:
|
||||
case TextInputAction.search:
|
||||
case TextInputAction.send:
|
||||
case TextInputAction.continueAction:
|
||||
case TextInputAction.join:
|
||||
case TextInputAction.route:
|
||||
case TextInputAction.emergencyCall:
|
||||
case TextInputAction.newline:
|
||||
widget.focusNode.unfocus();
|
||||
break;
|
||||
case TextInputAction.next:
|
||||
widget.focusNode.nextFocus();
|
||||
break;
|
||||
case TextInputAction.previous:
|
||||
widget.focusNode.previousFocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke optional callback with the user's submitted content.
|
||||
@ -1883,7 +1912,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
_textInputConnection = null;
|
||||
_lastFormattedUnmodifiedTextEditingValue = null;
|
||||
_receivedRemoteTextEditingValue = null;
|
||||
_finalizeEditing(true);
|
||||
_finalizeEditing(TextInputAction.done, shouldUnfocus: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1336,35 +1336,88 @@ void main() {
|
||||
expect(changedValue, clipboardContent);
|
||||
});
|
||||
|
||||
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
// The variants to test in the focus handling test.
|
||||
final ValueVariant<TextInputAction> focusVariants = ValueVariant<
|
||||
TextInputAction>(
|
||||
TextInputAction.values.toSet(),
|
||||
);
|
||||
|
||||
final Widget widget = MaterialApp(
|
||||
home: EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: TextEditingController(),
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
|
||||
cursorColor: Colors.blue,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
keyboardType: TextInputType.text,
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(widget);
|
||||
testWidgets('Handles focus correctly when action is invoked', (WidgetTester tester) async {
|
||||
// The expectations for each of the types of TextInputAction.
|
||||
const Map<TextInputAction, bool> actionShouldLoseFocus = <TextInputAction, bool>{
|
||||
TextInputAction.none: false,
|
||||
TextInputAction.unspecified: false,
|
||||
TextInputAction.done: true,
|
||||
TextInputAction.go: true,
|
||||
TextInputAction.search: true,
|
||||
TextInputAction.send: true,
|
||||
TextInputAction.continueAction: false,
|
||||
TextInputAction.join: false,
|
||||
TextInputAction.route: false,
|
||||
TextInputAction.emergencyCall: false,
|
||||
TextInputAction.newline: true,
|
||||
TextInputAction.next: true,
|
||||
TextInputAction.previous: true,
|
||||
};
|
||||
|
||||
// Select EditableText to give it focus.
|
||||
final Finder textFinder = find.byType(EditableText);
|
||||
await tester.tap(textFinder);
|
||||
await tester.pump();
|
||||
final TextInputAction action = focusVariants.currentValue;
|
||||
expect(actionShouldLoseFocus.containsKey(action), isTrue);
|
||||
|
||||
assert(focusNode.hasFocus);
|
||||
Future<void> _ensureCorrectFocusHandlingForAction(
|
||||
TextInputAction action, {
|
||||
@required bool shouldLoseFocus,
|
||||
bool shouldFocusNext = false,
|
||||
bool shouldFocusPrevious = false,
|
||||
}) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final GlobalKey previousKey = GlobalKey();
|
||||
final GlobalKey nextKey = GlobalKey();
|
||||
|
||||
await tester.testTextInput.receiveAction(TextInputAction.next);
|
||||
await tester.pump();
|
||||
final Widget widget = MaterialApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
child: Text('Previous Widget', key: previousKey),
|
||||
onPressed: () {}),
|
||||
EditableText(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
controller: TextEditingController(),
|
||||
focusNode: focusNode,
|
||||
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1,
|
||||
cursorColor: Colors.blue,
|
||||
selectionControls: materialTextSelectionControls,
|
||||
keyboardType: TextInputType.text,
|
||||
autofocus: true,
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Next Widget', key: nextKey), onPressed: () {}),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(widget);
|
||||
|
||||
// Still has focus after pressing "next".
|
||||
expect(focusNode.hasFocus, true);
|
||||
});
|
||||
assert(focusNode.hasFocus);
|
||||
|
||||
await tester.testTextInput.receiveAction(action);
|
||||
await tester.pump();
|
||||
|
||||
expect(Focus.of(nextKey.currentContext).hasFocus, equals(shouldFocusNext));
|
||||
expect(Focus.of(previousKey.currentContext).hasFocus, equals(shouldFocusPrevious));
|
||||
expect(focusNode.hasFocus, equals(!shouldLoseFocus));
|
||||
}
|
||||
|
||||
try {
|
||||
await _ensureCorrectFocusHandlingForAction(
|
||||
action,
|
||||
shouldLoseFocus: actionShouldLoseFocus[action],
|
||||
shouldFocusNext: action == TextInputAction.next,
|
||||
shouldFocusPrevious: action == TextInputAction.previous,
|
||||
);
|
||||
} on PlatformException {
|
||||
// on Android, continueAction isn't supported.
|
||||
expect(action, equals(TextInputAction.continueAction));
|
||||
}
|
||||
}, variant: focusVariants);
|
||||
|
||||
testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided', (WidgetTester tester) async {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
|
@ -271,6 +271,61 @@ class TargetPlatformVariant extends TestVariant<TargetPlatform> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A [TestVariant] that runs separate tests with each of the given values.
|
||||
///
|
||||
/// To use this variant, define it before the test, and then access
|
||||
/// [currentValue] inside the test.
|
||||
///
|
||||
/// The values are typically enums, but they don't have to be. The `toString`
|
||||
/// for the given value will be used to describe the variant. Values will have
|
||||
/// their type name stripped from their `toString` output, so that enum values
|
||||
/// will only print the value, not the type.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// This example shows how to set up the test to access the [currentValue]. In
|
||||
/// this example, two tests will be run, one with `value1`, and one with
|
||||
/// `value2`. The test with `value2` will fail. The names of the tests will be:
|
||||
///
|
||||
/// - `Test handling of TestScenario (value1)`
|
||||
/// - `Test handling of TestScenario (value2)`
|
||||
///
|
||||
/// ```dart
|
||||
/// enum TestScenario {
|
||||
/// value1,
|
||||
/// value2,
|
||||
/// value3,
|
||||
/// }
|
||||
///
|
||||
/// final ValueVariant<TestScenario> variants = ValueVariant<TestScenario>(
|
||||
/// <TestScenario>{value1, value2},
|
||||
/// );
|
||||
///
|
||||
/// testWidgets('Test handling of TestScenario', (WidgetTester tester) {
|
||||
/// expect(variants.currentValue, equals(value1));
|
||||
/// }, variant: variants);
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
class ValueVariant<T> extends TestVariant<T> {
|
||||
/// Creates a [ValueVariant] that tests the given [values].
|
||||
ValueVariant(this.values);
|
||||
|
||||
/// Returns the value currently under test.
|
||||
T get currentValue => _currentValue;
|
||||
T _currentValue;
|
||||
|
||||
@override
|
||||
final Set<T> values;
|
||||
|
||||
@override
|
||||
String describeValue(T value) => value.toString().replaceFirst('$T.', '');
|
||||
|
||||
@override
|
||||
Future<T> setUp(T value) async => _currentValue = value;
|
||||
|
||||
@override
|
||||
Future<void> tearDown(T value, T memento) async {}
|
||||
}
|
||||
|
||||
/// The warning message to show when a benchmark is performed with assert on.
|
||||
const String kDebugWarning = '''
|
||||
┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
|
||||
|
Loading…
Reference in New Issue
Block a user