Unverified Commit 8ab46bbc authored by Chris Bobbe's avatar Chris Bobbe Committed by GitHub

(Raw)Autocomplete: Add optional [optionsViewOpenDirection] param (#129802)

Allows positioning Autocomplete options above the field (previously hardcoded to under the field).
parent 61ebf755
......@@ -66,6 +66,7 @@ class Autocomplete<T extends Object> extends StatelessWidget {
this.onSelected,
this.optionsMaxHeight = 200.0,
this.optionsViewBuilder,
this.optionsViewOpenDirection = OptionsViewOpenDirection.down,
this.initialValue,
});
......@@ -90,6 +91,9 @@ class Autocomplete<T extends Object> extends StatelessWidget {
/// default.
final AutocompleteOptionsViewBuilder<T>? optionsViewBuilder;
/// {@macro flutter.widgets.RawAutocomplete.optionsViewOpenDirection}
final OptionsViewOpenDirection optionsViewOpenDirection;
/// The maximum height used for the default Material options list widget.
///
/// When [optionsViewBuilder] is `null`, this property sets the maximum height
......@@ -116,6 +120,7 @@ class Autocomplete<T extends Object> extends StatelessWidget {
fieldViewBuilder: fieldViewBuilder,
initialValue: initialValue,
optionsBuilder: optionsBuilder,
optionsViewOpenDirection: optionsViewOpenDirection,
optionsViewBuilder: optionsViewBuilder ?? (BuildContext context, AutocompleteOnSelected<T> onSelected, Iterable<T> options) {
return _AutocompleteOptions<T>(
displayStringForOption: displayStringForOption,
......
......@@ -77,6 +77,29 @@ typedef AutocompleteFieldViewBuilder = Widget Function(
/// * [RawAutocomplete.displayStringForOption], which is of this type.
typedef AutocompleteOptionToString<T extends Object> = String Function(T option);
/// A direction in which to open the options-view overlay.
///
/// See also:
///
/// * [RawAutocomplete.optionsViewOpenDirection], which is of this type.
/// * [RawAutocomplete.optionsViewBuilder] to specify how to build the
/// selectable-options widget.
/// * [RawAutocomplete.fieldViewBuilder] to optionally specify how to build the
/// corresponding field widget.
enum OptionsViewOpenDirection {
/// Open upward.
///
/// The bottom edge of the options view will align with the top edge
/// of the text field built by [RawAutocomplete.fieldViewBuilder].
up,
/// Open downward.
///
/// The top edge of the options view will align with the bottom edge
/// of the text field built by [RawAutocomplete.fieldViewBuilder].
down,
}
// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
/// A widget for helping the user make a selection by entering some text and
......@@ -128,6 +151,7 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
super.key,
required this.optionsViewBuilder,
required this.optionsBuilder,
this.optionsViewOpenDirection = OptionsViewOpenDirection.down,
this.displayStringForOption = defaultStringForOption,
this.fieldViewBuilder,
this.focusNode,
......@@ -151,6 +175,9 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
/// Pass the provided [TextEditingController] to the field built here so that
/// RawAutocomplete can listen for changes.
/// {@endtemplate}
///
/// If this parameter is null, then a [SizedBox.shrink] is built instead.
/// For how that pattern can be useful, see [textEditingController].
final AutocompleteFieldViewBuilder? fieldViewBuilder;
/// The [FocusNode] that is used for the text field.
......@@ -161,9 +188,9 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
/// field built by [fieldViewBuilder]. For example, it may be desirable to
/// place the text field in the AppBar and the options below in the main body.
///
/// When following this pattern, [fieldViewBuilder] can return
/// `SizedBox.shrink()` so that nothing is drawn where the text field would
/// normally be. A separate text field can be created elsewhere, and a
/// When following this pattern, [fieldViewBuilder] can be omitted,
/// so that a text field is not drawn where it would normally be.
/// A separate text field can be created elsewhere, and a
/// FocusNode and TextEditingController can be passed both to that text field
/// and to RawAutocomplete.
///
......@@ -182,9 +209,10 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
/// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
/// Builds the selectable options widgets from a list of options objects.
///
/// The options are displayed floating below the field using a
/// The options are displayed floating below or above the field using a
/// [CompositedTransformFollower] inside of an [Overlay], not at the same
/// place in the widget tree as [RawAutocomplete].
/// place in the widget tree as [RawAutocomplete]. To control whether it opens
/// upward or downward, use [optionsViewOpenDirection].
///
/// In order to track which item is highlighted by keyboard navigation, the
/// resulting options will be wrapped in an inherited
......@@ -197,6 +225,13 @@ class RawAutocomplete<T extends Object> extends StatefulWidget {
/// {@endtemplate}
final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;
/// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection}
/// The direction in which to open the options-view overlay.
///
/// Defaults to [OptionsViewOpenDirection.down].
/// {@endtemplate}
final OptionsViewOpenDirection optionsViewOpenDirection;
/// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
/// Returns the string to display in the field when the option is selected.
///
......@@ -421,7 +456,14 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
return CompositedTransformFollower(
link: _optionsLayerLink,
showWhenUnlinked: false,
targetAnchor: Alignment.bottomLeft,
targetAnchor: switch (widget.optionsViewOpenDirection) {
OptionsViewOpenDirection.up => Alignment.topLeft,
OptionsViewOpenDirection.down => Alignment.bottomLeft,
},
followerAnchor: switch (widget.optionsViewOpenDirection) {
OptionsViewOpenDirection.up => Alignment.bottomLeft,
OptionsViewOpenDirection.down => Alignment.topLeft,
},
child: TextFieldTapRegion(
child: AutocompleteHighlightedOption(
highlightIndexNotifier: _highlightedOptionIndex,
......
......@@ -507,4 +507,53 @@ void main() {
checkOptionHighlight(tester, 'lemur', null);
checkOptionHighlight(tester, 'northern white rhinoceros', null);
});
group('optionsViewOpenDirection', () {
testWidgets('default (down)', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Autocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
),
),
),
);
final OptionsViewOpenDirection actual = tester.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
.optionsViewOpenDirection;
expect(actual, equals(OptionsViewOpenDirection.down));
});
testWidgets('down', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Autocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
),
),
),
);
final OptionsViewOpenDirection actual = tester.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
.optionsViewOpenDirection;
expect(actual, equals(OptionsViewOpenDirection.down));
});
testWidgets('up', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Autocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.up,
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
),
),
),
);
final OptionsViewOpenDirection actual = tester.widget<RawAutocomplete<String>>(find.byType(RawAutocomplete<String>))
.optionsViewOpenDirection;
expect(actual, equals(OptionsViewOpenDirection.up));
});
});
}
......@@ -421,6 +421,144 @@ void main() {
expect(textEditingController.text, lastOptions.elementAt(0));
});
group('optionsViewOpenDirection', () {
testWidgets('unset (default behavior): open downward', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) {
return TextField(controller: controller, focusNode: focusNode);
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return const Text('a');
},
),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
expect(tester.getBottomLeft(find.byType(TextField)),
offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))));
});
testWidgets('down: open downward', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawAutocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) {
return TextField(controller: controller, focusNode: focusNode);
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return const Text('a');
},
),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
expect(tester.getBottomLeft(find.byType(TextField)),
offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))));
});
testWidgets('up: open upward', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: RawAutocomplete<String>(
optionsViewOpenDirection: OptionsViewOpenDirection.up,
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
fieldViewBuilder: (BuildContext context, TextEditingController controller, FocusNode focusNode, VoidCallback onFieldSubmitted) {
return TextField(controller: controller, focusNode: focusNode);
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return const Text('a');
},
),
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
expect(tester.getTopLeft(find.byType(TextField)),
offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a'))));
});
group('fieldViewBuilder not passed', () {
testWidgets('down', (WidgetTester tester) async {
final GlobalKey autocompleteKey = GlobalKey();
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextField(controller: controller, focusNode: focusNode),
RawAutocomplete<String>(
key: autocompleteKey,
textEditingController: controller,
focusNode: focusNode,
optionsViewOpenDirection: OptionsViewOpenDirection.down, // ignore: avoid_redundant_argument_values
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return const Text('a');
},
),
],
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
expect(tester.getBottomLeft(find.byKey(autocompleteKey)),
offsetMoreOrLessEquals(tester.getTopLeft(find.text('a'))));
});
testWidgets('up', (WidgetTester tester) async {
final GlobalKey autocompleteKey = GlobalKey();
final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
RawAutocomplete<String>(
key: autocompleteKey,
textEditingController: controller,
focusNode: focusNode,
optionsViewOpenDirection: OptionsViewOpenDirection.up,
optionsBuilder: (TextEditingValue textEditingValue) => <String>['a'],
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
return const Text('a');
},
),
TextField(controller: controller, focusNode: focusNode),
],
),
),
),
);
await tester.showKeyboard(find.byType(TextField));
expect(tester.getTopLeft(find.byKey(autocompleteKey)),
offsetMoreOrLessEquals(tester.getBottomLeft(find.text('a'))));
});
});
});
testWidgets('options follow field when it moves', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
......
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