Added controller and onSelected properties to DropdownMenu (#116259)

This commit is contained in:
Qun Cheng 2022-11-30 16:58:21 -08:00 committed by GitHub
parent e5e21c9837
commit 6bb412e35e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 381 additions and 137 deletions

View file

@ -9,23 +9,31 @@ import 'package:flutter/material.dart';
void main() => runApp(const DropdownMenuExample());
class DropdownMenuExample extends StatelessWidget {
class DropdownMenuExample extends StatefulWidget {
const DropdownMenuExample({super.key});
List<DropdownMenuEntry> getEntryList() {
final List<DropdownMenuEntry> entries = <DropdownMenuEntry>[];
@override
State<DropdownMenuExample> createState() => _DropdownMenuExampleState();
}
for (int index = 0; index < EntryLabel.values.length; index++) {
// Disabled item 1, 2 and 6.
final bool enabled = index != 1 && index != 2 && index != 6;
entries.add(DropdownMenuEntry(label: EntryLabel.values[index].label, enabled: enabled));
}
return entries;
}
class _DropdownMenuExampleState extends State<DropdownMenuExample> {
final TextEditingController colorController = TextEditingController();
final TextEditingController iconController = TextEditingController();
ColorLabel? selectedColor;
IconLabel? selectedIcon;
@override
Widget build(BuildContext context) {
final List<DropdownMenuEntry> dropdownMenuEntries = getEntryList();
final List<DropdownMenuEntry<ColorLabel>> colorEntries = <DropdownMenuEntry<ColorLabel>>[];
for (final ColorLabel color in ColorLabel.values) {
colorEntries.add(
DropdownMenuEntry<ColorLabel>(value: color, label: color.label, enabled: color.label != 'Grey'));
}
final List<DropdownMenuEntry<IconLabel>> iconEntries = <DropdownMenuEntry<IconLabel>>[];
for (final IconLabel icon in IconLabel.values) {
iconEntries.add(DropdownMenuEntry<IconLabel>(value: icon, label: icon.label));
}
return MaterialApp(
theme: ThemeData(
@ -34,25 +42,53 @@ class DropdownMenuExample extends StatelessWidget {
),
home: Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DropdownMenu(
label: const Text('Label'),
dropdownMenuEntries: dropdownMenuEntries,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
DropdownMenu<ColorLabel>(
initialSelection: ColorLabel.green,
controller: colorController,
label: const Text('Color'),
dropdownMenuEntries: colorEntries,
onSelected: (ColorLabel? color) {
setState(() {
selectedColor = color;
});
},
),
const SizedBox(width: 20),
DropdownMenu<IconLabel>(
controller: iconController,
enableFilter: true,
leadingIcon: const Icon(Icons.search),
label: const Text('Icon'),
dropdownMenuEntries: iconEntries,
inputDecorationTheme: const InputDecorationTheme(filled: true),
onSelected: (IconLabel? icon) {
setState(() {
selectedIcon = icon;
});
},
)
],
),
const SizedBox(width: 20),
DropdownMenu(
enableFilter: true,
leadingIcon: const Icon(Icons.search),
label: const Text('Label'),
dropdownMenuEntries: dropdownMenuEntries,
inputDecorationTheme: const InputDecorationTheme(filled: true),
),
if (selectedColor != null && selectedIcon != null)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You selected a ${selectedColor?.label} ${selectedIcon?.label}'),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Icon(selectedIcon?.icon, color: selectedColor?.color,))
],
)
],
),
else const Text('Please select a color and an icon.')
],
)
),
),
@ -60,15 +96,25 @@ class DropdownMenuExample extends StatelessWidget {
}
}
enum EntryLabel {
item0('Item 0'),
item1('Item 1'),
item2('Item 2'),
item3('Item 3'),
item4('Item 4'),
item5('Item 5'),
item6('Item 6');
enum ColorLabel {
blue('Blue', Colors.blue),
pink('Pink', Colors.pink),
green('Green', Colors.green),
yellow('Yellow', Colors.yellow),
grey('Grey', Colors.grey);
const EntryLabel(this.label);
const ColorLabel(this.label, this.color);
final String label;
final Color color;
}
enum IconLabel {
smile('Smile', Icons.sentiment_satisfied_outlined),
cloud('Cloud', Icons.cloud_outlined,),
brush('Brush', Icons.brush_outlined),
heart('Heart', Icons.favorite);
const IconLabel(this.label, this.icon);
final String label;
final IconData icon;
}

View file

@ -36,11 +36,12 @@ const double _kDefaultHorizontalPadding = 12.0;
/// See also:
///
/// * [DropdownMenu]
class DropdownMenuEntry {
class DropdownMenuEntry<T> {
/// Creates an entry that is used with [DropdownMenu.dropdownMenuEntries].
///
/// [label] must be non-null.
const DropdownMenuEntry({
required this.value,
required this.label,
this.leadingIcon,
this.trailingIcon,
@ -48,6 +49,11 @@ class DropdownMenuEntry {
this.style,
});
/// the value used to identify the entry.
///
/// This value must be unique across all entries in a [DropdownMenu].
final T value;
/// The label displayed in the center of the menu item.
final String label;
@ -101,7 +107,7 @@ class DropdownMenuEntry {
/// The [DropdownMenu] uses a [TextField] as the "anchor".
/// * [TextField], which is a text input widget that uses an [InputDecoration].
/// * [DropdownMenuEntry], which is used to build the [MenuItemButton] in the [DropdownMenu] list.
class DropdownMenu extends StatefulWidget {
class DropdownMenu<T> extends StatefulWidget {
/// Creates a const [DropdownMenu].
///
/// The leading and trailing icons in the text field can be customized by using
@ -126,6 +132,9 @@ class DropdownMenu extends StatefulWidget {
this.textStyle,
this.inputDecorationTheme,
this.menuStyle,
this.controller,
this.initialSelection,
this.onSelected,
required this.dropdownMenuEntries,
});
@ -204,25 +213,40 @@ class DropdownMenu extends StatefulWidget {
/// The default width of the menu is set to the width of the text field.
final MenuStyle? menuStyle;
/// Controls the text being edited or selected in the menu.
///
/// If null, this widget will create its own [TextEditingController].
final TextEditingController? controller;
/// The value used to for an initial selection.
///
/// Defaults to null.
final T? initialSelection;
/// The callback is called when a selection is made.
///
/// Defaults to null. If null, only the text field is updated.
final ValueChanged<T?>? onSelected;
/// Descriptions of the menu items in the [DropdownMenu].
///
/// This is a required parameter. It is recommended that at least one [DropdownMenuEntry]
/// is provided. If this is an empty list, the menu will be empty and only
/// contain space for padding.
final List<DropdownMenuEntry> dropdownMenuEntries;
final List<DropdownMenuEntry<T>> dropdownMenuEntries;
@override
State<DropdownMenu> createState() => _DropdownMenuState();
State<DropdownMenu<T>> createState() => _DropdownMenuState<T>();
}
class _DropdownMenuState extends State<DropdownMenu> {
final MenuController _controller = MenuController();
class _DropdownMenuState<T> extends State<DropdownMenu<T>> {
final GlobalKey _anchorKey = GlobalKey();
final GlobalKey _leadingKey = GlobalKey();
final FocusNode _textFocusNode = FocusNode();
final TextEditingController _textEditingController = TextEditingController();
final MenuController _controller = MenuController();
late final TextEditingController _textEditingController;
late bool _enableFilter;
late List<DropdownMenuEntry> filteredEntries;
late List<DropdownMenuEntry<T>> filteredEntries;
List<Widget>? _initialMenu;
int? currentHighlight;
double? leadingPadding;
@ -231,22 +255,37 @@ class _DropdownMenuState extends State<DropdownMenu> {
@override
void initState() {
super.initState();
_textEditingController = widget.controller ?? TextEditingController();
_enableFilter = widget.enableFilter;
filteredEntries = widget.dropdownMenuEntries;
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled);
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
if (index != -1) {
_textEditingController.text = filteredEntries[index].label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
}
refreshLeadingPadding();
}
@override
void didUpdateWidget(DropdownMenu oldWidget) {
void didUpdateWidget(DropdownMenu<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) {
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled);
_menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry<T> entry) => entry.enabled);
}
if (oldWidget.leadingIcon != widget.leadingIcon) {
refreshLeadingPadding();
}
if (oldWidget.initialSelection != widget.initialSelection) {
final int index = filteredEntries.indexWhere((DropdownMenuEntry<T> entry) => entry.value == widget.initialSelection);
if (index != -1) {
_textEditingController.text = filteredEntries[index].label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
}
}
}
void refreshLeadingPadding() {
@ -266,25 +305,25 @@ class _DropdownMenuState extends State<DropdownMenu> {
return null;
}
List<DropdownMenuEntry> filter(List<DropdownMenuEntry> entries, TextEditingController textEditingController) {
List<DropdownMenuEntry<T>> filter(List<DropdownMenuEntry<T>> entries, TextEditingController textEditingController) {
final String filterText = textEditingController.text.toLowerCase();
return entries
.where((DropdownMenuEntry entry) => entry.label.toLowerCase().contains(filterText))
.where((DropdownMenuEntry<T> entry) => entry.label.toLowerCase().contains(filterText))
.toList();
}
int? search(List<DropdownMenuEntry> entries, TextEditingController textEditingController) {
int? search(List<DropdownMenuEntry<T>> entries, TextEditingController textEditingController) {
final String searchText = textEditingController.value.text.toLowerCase();
if (searchText.isEmpty) {
return null;
}
final int index = entries.indexWhere((DropdownMenuEntry entry) => entry.label.toLowerCase().contains(searchText));
final int index = entries.indexWhere((DropdownMenuEntry<T> entry) => entry.label.toLowerCase().contains(searchText));
return index != -1 ? index : null;
}
List<Widget> _buildButtons(
List<DropdownMenuEntry> filteredEntries,
List<DropdownMenuEntry<T>> filteredEntries,
TextEditingController textEditingController,
TextDirection textDirection,
{ int? focusedIndex }
@ -306,7 +345,7 @@ class _DropdownMenuState extends State<DropdownMenu> {
}
for (int i = 0; i < filteredEntries.length; i++) {
final DropdownMenuEntry entry = filteredEntries[i];
final DropdownMenuEntry<T> entry = filteredEntries[i];
ButtonStyle effectiveStyle = entry.style ?? defaultStyle;
final Color focusedBackgroundColor = effectiveStyle.foregroundColor?.resolve(<MaterialState>{MaterialState.focused})
?? Theme.of(context).colorScheme.onSurface;
@ -328,8 +367,9 @@ class _DropdownMenuState extends State<DropdownMenu> {
? () {
textEditingController.text = entry.label;
textEditingController.selection =
TextSelection.collapsed(offset: textEditingController.text.length);
currentHighlight = widget.enableSearch ? i : -1;
TextSelection.collapsed(offset: textEditingController.text.length);
currentHighlight = widget.enableSearch ? i : null;
widget.onSelected?.call(entry.value);
}
: null,
requestFocusOnHover: false,
@ -351,7 +391,8 @@ class _DropdownMenuState extends State<DropdownMenu> {
while (!filteredEntries[currentHighlight!].enabled) {
currentHighlight = (currentHighlight! - 1) % filteredEntries.length;
}
_textEditingController.text = filteredEntries[currentHighlight!].label;
final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.text = currentLabel;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
});
@ -366,14 +407,15 @@ class _DropdownMenuState extends State<DropdownMenu> {
while (!filteredEntries[currentHighlight!].enabled) {
currentHighlight = (currentHighlight! + 1) % filteredEntries.length;
}
_textEditingController.text = filteredEntries[currentHighlight!].label;
final String currentLabel = filteredEntries[currentHighlight!].label;
_textEditingController.text = currentLabel;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
});
void handlePressed(MenuController controller) {
if (controller.isOpen) {
currentHighlight = -1;
currentHighlight = null;
controller.close();
} else { // close to open
if (_textEditingController.text.isNotEmpty) {
@ -443,6 +485,7 @@ class _DropdownMenuState extends State<DropdownMenu> {
controller: _controller,
menuChildren: menu,
crossAxisUnconstrained: false,
onClose: () { setState(() {}); }, // To update the status of the IconButton
builder: (BuildContext context, MenuController controller, Widget? child) {
assert(_initialMenu != null);
final Widget trailingButton = Padding(
@ -473,16 +516,27 @@ class _DropdownMenuState extends State<DropdownMenu> {
controller: _textEditingController,
onEditingComplete: () {
if (currentHighlight != null) {
_textEditingController.text = filteredEntries[currentHighlight!].label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
final DropdownMenuEntry<T> entry = filteredEntries[currentHighlight!];
if (entry.enabled) {
_textEditingController.text = entry.label;
_textEditingController.selection =
TextSelection.collapsed(offset: _textEditingController.text.length);
widget.onSelected?.call(entry.value);
}
} else {
widget.onSelected?.call(null);
}
if (!widget.enableSearch) {
currentHighlight = null;
}
if (_textEditingController.text.isNotEmpty) {
controller.close();
}
controller.close();
},
onTap: () {
handlePressed(controller);
},
onChanged: (_) {
onChanged: (String text) {
controller.open();
setState(() {
filteredEntries = widget.dropdownMenuEntries;

View file

@ -8,19 +8,19 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final List<DropdownMenuEntry> menuChildren = <DropdownMenuEntry>[];
final List<DropdownMenuEntry<TestMenu>> menuChildren = <DropdownMenuEntry<TestMenu>>[];
for (final TestMenu value in TestMenu.values) {
final DropdownMenuEntry entry = DropdownMenuEntry(label: value.label);
final DropdownMenuEntry<TestMenu> entry = DropdownMenuEntry<TestMenu>(value: value, label: value.label);
menuChildren.add(entry);
}
Widget buildTest(ThemeData themeData, List<DropdownMenuEntry> entries,
Widget buildTest<T extends Enum>(ThemeData themeData, List<DropdownMenuEntry<T>> entries,
{double? width, double? menuHeight, Widget? leadingIcon, Widget? label}) {
return MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<T>(
label: label,
leadingIcon: leadingIcon,
width: width,
@ -80,7 +80,7 @@ void main() {
theme: themeData,
home: Scaffold(
body: SafeArea(
child: DropdownMenu(
child: DropdownMenu<TestMenu>(
enabled: false,
dropdownMenuEntries: menuChildren,
),
@ -115,7 +115,7 @@ void main() {
theme: themeData,
home: Scaffold(
body: SafeArea(
child: DropdownMenu(
child: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
@ -127,7 +127,7 @@ void main() {
final Size anchorSize = tester.getSize(textField);
expect(anchorSize, const Size(180.0, 54.0));
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder menuMaterial = find.ancestor(
@ -145,7 +145,7 @@ void main() {
final Size size = tester.getSize(anchor);
expect(size, const Size(200.0, 54.0));
await tester.tap(find.byType(DropdownMenu));
await tester.tap(anchor);
await tester.pumpAndSettle();
final Finder updatedMenu = find.ancestor(
@ -158,19 +158,19 @@ void main() {
testWidgets('The width property can customize the width of the dropdown menu', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry> shortMenuItems = <DropdownMenuEntry>[];
final List<DropdownMenuEntry<ShortMenu>> shortMenuItems = <DropdownMenuEntry<ShortMenu>>[];
for (final ShortMenu value in ShortMenu.values) {
final DropdownMenuEntry entry = DropdownMenuEntry(label: value.label);
final DropdownMenuEntry<ShortMenu> entry = DropdownMenuEntry<ShortMenu>(value: value, label: value.label);
shortMenuItems.add(entry);
}
const double customBigWidth = 250.0;
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customBigWidth));
RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu));
RenderBox box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
expect(box.size.width, customBigWidth);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<ShortMenu>));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
Size buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').last);
@ -180,10 +180,10 @@ void main() {
await tester.pumpWidget(Container());
const double customSmallWidth = 100.0;
await tester.pumpWidget(buildTest(themeData, shortMenuItems, width: customSmallWidth));
box = tester.firstRenderObject(find.byType(DropdownMenu));
box = tester.firstRenderObject(find.byType(DropdownMenu<ShortMenu>));
expect(box.size.width, customSmallWidth);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<ShortMenu>));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
buttonSize = tester.getSize(find.widgetWithText(MenuItemButton, 'I0').last);
@ -195,7 +195,7 @@ void main() {
final ThemeData themeData = ThemeData();
await tester.pumpWidget(buildTest(themeData, menuChildren));
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Element firstItem = tester.element(find.widgetWithText(MenuItemButton, 'Item 0').last);
@ -219,7 +219,7 @@ void main() {
await tester.pumpWidget(buildTest(themeData, menuChildren, menuHeight: 100));
await tester.pumpAndSettle();
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedMenu = find.ancestor(
@ -240,7 +240,7 @@ void main() {
final Finder label = find.text('label');
final Offset labelTopLeft = tester.getTopLeft(label);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder itemText = find.text('Item 0').last;
final Offset itemTextTopLeft = tester.getTopLeft(itemText);
@ -259,7 +259,7 @@ void main() {
final Finder updatedLabel = find.text('label');
final Offset updatedLabelTopLeft = tester.getTopLeft(updatedLabel);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText = find.text('Item 0').last;
final Offset updatedItemTextTopLeft = tester.getTopLeft(updatedItemText);
@ -282,7 +282,7 @@ void main() {
final Finder updatedLabel1 = find.text('label');
final Offset updatedLabelTopLeft1 = tester.getTopLeft(updatedLabel1);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText1 = find.text('Item 0').last;
final Offset updatedItemTextTopLeft1 = tester.getTopLeft(updatedItemText1);
@ -301,7 +301,7 @@ void main() {
home: Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: DropdownMenu(
child: DropdownMenu<TestMenu>(
label: const Text('label'),
dropdownMenuEntries: menuChildren,
),
@ -312,7 +312,7 @@ void main() {
final Finder label = find.text('label');
final Offset labelTopRight = tester.getTopRight(label);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder itemText = find.text('Item 0').last;
final Offset itemTextTopRight = tester.getTopRight(itemText);
@ -326,7 +326,7 @@ void main() {
home: Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: DropdownMenu(
child: DropdownMenu<TestMenu>(
leadingIcon: const Icon(Icons.search),
label: const Text('label'),
dropdownMenuEntries: menuChildren,
@ -338,11 +338,11 @@ void main() {
final Finder leadingIcon = find.widgetWithIcon(Container, Icons.search);
final double iconWidth = tester.getSize(leadingIcon).width;
final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu));
final Offset dropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>));
final Finder updatedLabel = find.text('label');
final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText = find.text('Item 0').last;
final Offset updatedItemTextTopRight = tester.getTopRight(updatedItemText);
@ -358,7 +358,7 @@ void main() {
home: Scaffold(
body: Directionality(
textDirection: TextDirection.rtl,
child: DropdownMenu(
child: DropdownMenu<TestMenu>(
leadingIcon: const SizedBox(width: 75.0, child: Icon(Icons.search)),
label: const Text('label'),
dropdownMenuEntries: menuChildren,
@ -370,11 +370,11 @@ void main() {
final Finder largeLeadingIcon = find.widgetWithIcon(Container, Icons.search);
final double largeIconWidth = tester.getSize(largeLeadingIcon).width;
final Offset updatedDropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu));
final Offset updatedDropdownMenuTopRight = tester.getTopRight(find.byType(DropdownMenu<TestMenu>));
final Finder updatedLabel1 = find.text('label');
final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1);
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
final Finder updatedItemText1 = find.text('Item 0').last;
final Offset updatedItemTextTopRight1 = tester.getTopRight(updatedItemText1);
@ -407,7 +407,7 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
trailingIcon: const Icon(Icons.ac_unit),
dropdownMenuEntries: menuChildren,
),
@ -433,14 +433,14 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
trailingIcon: const Icon(Icons.ac_unit),
dropdownMenuEntries: menuChildren,
),
),
));
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
@ -475,13 +475,13 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
@ -517,14 +517,14 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
@ -547,14 +547,14 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowUp);
@ -574,18 +574,18 @@ void main() {
testWidgets('Disabled button will be skipped while pressing up/down key', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry> menuWithDisabledItems = <DropdownMenuEntry>[
const DropdownMenuEntry(label: 'Item 0'),
const DropdownMenuEntry(label: 'Item 1', enabled: false),
const DropdownMenuEntry(label: 'Item 2', enabled: false),
const DropdownMenuEntry(label: 'Item 3'),
const DropdownMenuEntry(label: 'Item 4'),
const DropdownMenuEntry(label: 'Item 5', enabled: false),
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu2, label: 'Item 2', enabled: false),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu3, label: 'Item 3'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu4, label: 'Item 4'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu5, label: 'Item 5', enabled: false),
];
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
),
),
@ -593,7 +593,7 @@ void main() {
await tester.pump();
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pumpAndSettle();
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
@ -621,14 +621,14 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'Menu 1');
await tester.pumpAndSettle();
@ -645,14 +645,14 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'Menu 1');
await tester.pumpAndSettle();
@ -691,14 +691,14 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuChildren,
),
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find.byType(TextField).first, 'Menu 1');
@ -714,7 +714,7 @@ void main() {
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: Scaffold(
body: DropdownMenu(
body: DropdownMenu<TestMenu>(
enableFilter: true,
dropdownMenuEntries: menuChildren,
),
@ -722,7 +722,7 @@ void main() {
));
// Open the menu
await tester.tap(find.byType(DropdownMenu));
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
await tester.enterText(find
@ -738,6 +738,150 @@ void main() {
}
}
});
testWidgets('The controller can access the value in the input field', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: DropdownMenu<TestMenu>(
enableFilter: true,
dropdownMenuEntries: menuChildren,
controller: controller,
),
);
}
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item3 = find.widgetWithText(MenuItemButton, 'Item 3').last;
await tester.tap(item3);
await tester.pumpAndSettle();
expect(controller.text, 'Item 3');
await tester.enterText(find.byType(TextField).first, 'New Item');
expect(controller.text, 'New Item');
});
testWidgets('The onSelected gets called only when a selection is made', (WidgetTester tester) async {
int selectionCount = 0;
final ThemeData themeData = ThemeData();
final List<DropdownMenuEntry<TestMenu>> menuWithDisabledItems = <DropdownMenuEntry<TestMenu>>[
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 0'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 1', enabled: false),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 2'),
const DropdownMenuEntry<TestMenu>(value: TestMenu.mainMenu0, label: 'Item 3'),
];
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: DropdownMenu<TestMenu>(
dropdownMenuEntries: menuWithDisabledItems,
controller: controller,
onSelected: (_) {
setState(() {
selectionCount++;
});
},
),
);
}
),
));
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
// Test onSelected on key press
await simulateKeyDownEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(selectionCount, 1);
// Disabled item doesn't trigger onSelected callback.
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item1 = find.widgetWithText(MenuItemButton, 'Item 1').last;
await tester.tap(item1);
await tester.pumpAndSettle();
expect(controller.text, 'Item 0');
expect(selectionCount, 1);
final Finder item2 = find.widgetWithText(MenuItemButton, 'Item 2').last;
await tester.tap(item2);
await tester.pumpAndSettle();
expect(controller.text, 'Item 2');
expect(selectionCount, 2);
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder item3 = find.widgetWithText(MenuItemButton, 'Item 3').last;
await tester.tap(item3);
await tester.pumpAndSettle();
expect(controller.text, 'Item 3');
expect(selectionCount, 3);
// When typing something in the text field without selecting any of the options,
// the onSelected should not be called.
await tester.enterText(find.byType(TextField).first, 'New Item');
expect(controller.text, 'New Item');
expect(selectionCount, 3);
expect(find.widgetWithText(TextField, 'New Item'), findsOneWidget);
await tester.enterText(find.byType(TextField).first, '');
expect(selectionCount, 3);
expect(controller.text.isEmpty, true);
});
testWidgets('The selectedValue gives an initial text and highlights the according item', (WidgetTester tester) async {
final ThemeData themeData = ThemeData();
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(MaterialApp(
theme: themeData,
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: DropdownMenu<TestMenu>(
initialSelection: TestMenu.mainMenu3,
dropdownMenuEntries: menuChildren,
controller: controller,
),
);
}
),
));
expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget);
// Open the menu
await tester.tap(find.byType(DropdownMenu<TestMenu>));
await tester.pump();
final Finder buttonMaterial = find.descendant(
of: find.widgetWithText(MenuItemButton, 'Item 3'),
matching: find.byType(Material),
).last;
// Validate the item 3 is highlighted.
final Material itemMaterial = tester.widget<Material>(buttonMaterial);
expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12));
});
}
enum TestMenu {

View file

@ -43,11 +43,11 @@ void main() {
theme: themeData,
home: const Scaffold(
body: Center(
child: DropdownMenu(
dropdownMenuEntries: <DropdownMenuEntry>[
DropdownMenuEntry(label: 'Item 0'),
DropdownMenuEntry(label: 'Item 1'),
DropdownMenuEntry(label: 'Item 2'),
child: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'Item 0'),
DropdownMenuEntry<int>(value: 1, label: 'Item 1'),
DropdownMenuEntry<int>(value: 2, label: 'Item 2'),
],
),
),
@ -122,11 +122,11 @@ void main() {
theme: theme,
home: const Scaffold(
body: Center(
child: DropdownMenu(
dropdownMenuEntries: <DropdownMenuEntry>[
DropdownMenuEntry(label: 'Item 0'),
DropdownMenuEntry(label: 'Item 1'),
DropdownMenuEntry(label: 'Item 2'),
child: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'Item 0'),
DropdownMenuEntry<int>(value: 1, label: 'Item 1'),
DropdownMenuEntry<int>(value: 2, label: 'Item 2'),
],
),
),
@ -223,11 +223,11 @@ void main() {
data: dropdownMenuTheme,
child: const Scaffold(
body: Center(
child: DropdownMenu(
dropdownMenuEntries: <DropdownMenuEntry>[
DropdownMenuEntry(label: 'Item 0'),
DropdownMenuEntry(label: 'Item 1'),
DropdownMenuEntry(label: 'Item 2'),
child: DropdownMenu<int>(
dropdownMenuEntries: <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'Item 0'),
DropdownMenuEntry<int>(value: 1, label: 'Item 1'),
DropdownMenuEntry<int>(value: 2, label: 'Item 2'),
],
),
),
@ -326,7 +326,7 @@ void main() {
data: dropdownMenuTheme,
child: Scaffold(
body: Center(
child: DropdownMenu(
child: DropdownMenu<int>(
textStyle: TextStyle(
color: Colors.pink,
backgroundColor: Colors.cyan,
@ -345,10 +345,10 @@ void main() {
),
),
inputDecorationTheme: const InputDecorationTheme(filled: true, fillColor: Colors.deepPurple),
dropdownMenuEntries: const <DropdownMenuEntry>[
DropdownMenuEntry(label: 'Item 0'),
DropdownMenuEntry(label: 'Item 1'),
DropdownMenuEntry(label: 'Item 2'),
dropdownMenuEntries: const <DropdownMenuEntry<int>>[
DropdownMenuEntry<int>(value: 0, label: 'Item 0'),
DropdownMenuEntry<int>(value: 1, label: 'Item 1'),
DropdownMenuEntry<int>(value: 2, label: 'Item 2'),
],
),
),