mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
(Raw)Autocomplete: Add optional [optionsViewOpenDirection] param (#129802)
Allows positioning Autocomplete options above the field (previously hardcoded to under the field).
This commit is contained in:
parent
61ebf755d7
commit
8ab46bbce4
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user