diff --git a/packages/flutter/lib/src/material/autocomplete.dart b/packages/flutter/lib/src/material/autocomplete.dart index 32d8bae65a8..8fcdedab0c9 100644 --- a/packages/flutter/lib/src/material/autocomplete.dart +++ b/packages/flutter/lib/src/material/autocomplete.dart @@ -63,10 +63,12 @@ class Autocomplete extends StatelessWidget { required this.optionsBuilder, this.displayStringForOption = RawAutocomplete.defaultStringForOption, this.fieldViewBuilder = _defaultFieldViewBuilder, + this.focusNode, this.onSelected, this.optionsMaxHeight = 200.0, this.optionsViewBuilder, this.optionsViewOpenDirection = OptionsViewOpenDirection.down, + this.textEditingController, this.initialValue, }); @@ -79,6 +81,14 @@ class Autocomplete extends StatelessWidget { /// default. final AutocompleteFieldViewBuilder fieldViewBuilder; + /// The [FocusNode] that is used for the text field. + /// + /// {@macro flutter.widgets.RawAutocomplete.split} + /// + /// If this parameter is not null, then [textEditingController] must also be + /// non-null. + final FocusNode? focusNode; + /// {@macro flutter.widgets.RawAutocomplete.onSelected} final AutocompleteOnSelected? onSelected; @@ -102,6 +112,13 @@ class Autocomplete extends StatelessWidget { /// The default value is set to 200. final double optionsMaxHeight; + /// The [TextEditingController] that is used for the text field. + /// + /// {@macro flutter.widgets.RawAutocomplete.split} + /// + /// If this parameter is not null, then [focusNode] must also be non-null. + final TextEditingController? textEditingController; + /// {@macro flutter.widgets.RawAutocomplete.initialValue} final TextEditingValue? initialValue; @@ -123,6 +140,8 @@ class Autocomplete extends StatelessWidget { return RawAutocomplete( displayStringForOption: displayStringForOption, fieldViewBuilder: fieldViewBuilder, + focusNode: focusNode, + textEditingController: textEditingController, initialValue: initialValue, optionsBuilder: optionsBuilder, optionsViewOpenDirection: optionsViewOpenDirection, diff --git a/packages/flutter/lib/src/widgets/autocomplete.dart b/packages/flutter/lib/src/widgets/autocomplete.dart index 7bdf9b38d40..01da7c4d2ab 100644 --- a/packages/flutter/lib/src/widgets/autocomplete.dart +++ b/packages/flutter/lib/src/widgets/autocomplete.dart @@ -211,7 +211,7 @@ class RawAutocomplete extends StatefulWidget { /// {@endtemplate} /// /// If this parameter is not null, then [textEditingController] must also be - /// not null. + /// non-null. final FocusNode? focusNode; /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder} @@ -265,7 +265,7 @@ class RawAutocomplete extends StatefulWidget { /// /// {@macro flutter.widgets.RawAutocomplete.split} /// - /// If this parameter is not null, then [focusNode] must also be not null. + /// If this parameter is not null, then [focusNode] must also be non-null. final TextEditingController? textEditingController; /// {@template flutter.widgets.RawAutocomplete.initialValue} diff --git a/packages/flutter/test/material/autocomplete_test.dart b/packages/flutter/test/material/autocomplete_test.dart index 71aa6d28d2f..e93d0f860f0 100644 --- a/packages/flutter/test/material/autocomplete_test.dart +++ b/packages/flutter/test/material/autocomplete_test.dart @@ -648,4 +648,87 @@ void main() { expect(optionFinder(kOptions.length - 1), findsNothing); checkOptionHighlight(tester, kOptions.first, highlightColor); }); + + testWidgets( + 'passes textEditingController, focusNode to textEditingController, focusNode RawAutocomplete', + (WidgetTester tester) async { + final TextEditingController textEditingController = TextEditingController(); + final FocusNode focusNode = FocusNode(); + addTearDown(textEditingController.dispose); + addTearDown(focusNode.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Autocomplete( + focusNode: focusNode, + textEditingController: textEditingController, + optionsBuilder: (TextEditingValue textEditingValue) => ['a'], + ), + ), + ), + ), + ); + + final RawAutocomplete rawAutocomplete = tester.widget( + find.byType(RawAutocomplete), + ); + expect(rawAutocomplete.textEditingController, textEditingController); + expect(rawAutocomplete.focusNode, focusNode); + }, + ); + + testWidgets('when field scrolled offscreen, reshown selected value when scrolled back', ( + WidgetTester tester, + ) async { + final ScrollController scrollController = ScrollController(); + final TextEditingController textEditingController = TextEditingController(); + final FocusNode focusNode = FocusNode(); + addTearDown(textEditingController.dispose); + addTearDown(focusNode.dispose); + addTearDown(scrollController.dispose); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: ListView( + controller: scrollController, + children: [ + Autocomplete( + focusNode: focusNode, + textEditingController: textEditingController, + optionsBuilder: (TextEditingValue textEditingValue) { + return kOptions.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + ), + const SizedBox(height: 1000.0), + ], + ), + ), + ), + ); + + /// Select an option. + await tester.tap(find.byType(TextField)); + await tester.pump(); + const String textSelection = 'chameleon'; + await tester.tap(find.text(textSelection)); + + // Unfocus and scroll to deconstruct the widge + final TextField field = find.byType(TextField).evaluate().first.widget as TextField; + field.focusNode?.unfocus(); + scrollController.jumpTo(2000.0); + await tester.pumpAndSettle(); + + /// Scroll to go back to the widget. + scrollController.jumpTo(0.0); + await tester.pumpAndSettle(); + + /// Checks that the option selected is still present. + final TextField field2 = find.byType(TextField).evaluate().first.widget as TextField; + expect(field2.controller!.text, textSelection); + }); }