Unverified Commit d1eff0b4 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.
parent 7f122c74
......@@ -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(),
);
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,
};
final TextInputAction action = focusVariants.currentValue;
expect(actionShouldLoseFocus.containsKey(action), isTrue);
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();
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);
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);
// Select EditableText to give it focus.
final Finder textFinder = find.byType(EditableText);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(action);
await tester.pump();
await tester.testTextInput.receiveAction(TextInputAction.next);
await tester.pump();
expect(Focus.of(nextKey.currentContext).hasFocus, equals(shouldFocusNext));
expect(Focus.of(previousKey.currentContext).hasFocus, equals(shouldFocusPrevious));
expect(focusNode.hasFocus, equals(!shouldLoseFocus));
}
// Still has focus after pressing "next".
expect(focusNode.hasFocus, true);
});
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 = '''
┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓
......
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