From 6bb412e35ed50be713732280109ebb98257e0202 Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Wed, 30 Nov 2022 16:58:21 -0800 Subject: [PATCH] Added `controller` and `onSelected` properties to DropdownMenu (#116259) --- .../dropdown_menu/dropdown_menu.0.dart | 120 ++++++--- .../lib/src/material/dropdown_menu.dart | 108 ++++++-- .../test/material/dropdown_menu_test.dart | 250 ++++++++++++++---- .../material/dropdown_menu_theme_test.dart | 40 +-- 4 files changed, 381 insertions(+), 137 deletions(-) diff --git a/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart b/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart index a9a59e65df94..39e82f6c436d 100644 --- a/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart +++ b/examples/api/lib/material/dropdown_menu/dropdown_menu.0.dart @@ -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 getEntryList() { - final List entries = []; + @override + State 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 { + final TextEditingController colorController = TextEditingController(); + final TextEditingController iconController = TextEditingController(); + ColorLabel? selectedColor; + IconLabel? selectedIcon; @override Widget build(BuildContext context) { - final List dropdownMenuEntries = getEntryList(); + final List> colorEntries = >[]; + for (final ColorLabel color in ColorLabel.values) { + colorEntries.add( + DropdownMenuEntry(value: color, label: color.label, enabled: color.label != 'Grey')); + } + + final List> iconEntries = >[]; + for (final IconLabel icon in IconLabel.values) { + iconEntries.add(DropdownMenuEntry(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: [ - DropdownMenu( - label: const Text('Label'), - dropdownMenuEntries: dropdownMenuEntries, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DropdownMenu( + initialSelection: ColorLabel.green, + controller: colorController, + label: const Text('Color'), + dropdownMenuEntries: colorEntries, + onSelected: (ColorLabel? color) { + setState(() { + selectedColor = color; + }); + }, + ), + const SizedBox(width: 20), + DropdownMenu( + 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: [ + 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 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 EntryLabel(this.label); + const IconLabel(this.label, this.icon); final String label; + final IconData icon; } diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 4bd6884f2f20..097fdef45fd2 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -36,11 +36,12 @@ const double _kDefaultHorizontalPadding = 12.0; /// See also: /// /// * [DropdownMenu] -class DropdownMenuEntry { +class DropdownMenuEntry { /// 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 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? 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 dropdownMenuEntries; + final List> dropdownMenuEntries; @override - State createState() => _DropdownMenuState(); + State> createState() => _DropdownMenuState(); } -class _DropdownMenuState extends State { - final MenuController _controller = MenuController(); +class _DropdownMenuState extends State> { 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 filteredEntries; + late List> filteredEntries; List? _initialMenu; int? currentHighlight; double? leadingPadding; @@ -231,22 +255,37 @@ class _DropdownMenuState extends State { @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 entry) => entry.enabled); + final int index = filteredEntries.indexWhere((DropdownMenuEntry 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 oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.dropdownMenuEntries != widget.dropdownMenuEntries) { - _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); + _menuHasEnabledItem = filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); } if (oldWidget.leadingIcon != widget.leadingIcon) { refreshLeadingPadding(); } + if (oldWidget.initialSelection != widget.initialSelection) { + final int index = filteredEntries.indexWhere((DropdownMenuEntry 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 { return null; } - List filter(List entries, TextEditingController textEditingController) { + List> filter(List> entries, TextEditingController textEditingController) { final String filterText = textEditingController.text.toLowerCase(); return entries - .where((DropdownMenuEntry entry) => entry.label.toLowerCase().contains(filterText)) + .where((DropdownMenuEntry entry) => entry.label.toLowerCase().contains(filterText)) .toList(); } - int? search(List entries, TextEditingController textEditingController) { + int? search(List> 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 entry) => entry.label.toLowerCase().contains(searchText)); return index != -1 ? index : null; } List _buildButtons( - List filteredEntries, + List> filteredEntries, TextEditingController textEditingController, TextDirection textDirection, { int? focusedIndex } @@ -306,7 +345,7 @@ class _DropdownMenuState extends State { } for (int i = 0; i < filteredEntries.length; i++) { - final DropdownMenuEntry entry = filteredEntries[i]; + final DropdownMenuEntry entry = filteredEntries[i]; ButtonStyle effectiveStyle = entry.style ?? defaultStyle; final Color focusedBackgroundColor = effectiveStyle.foregroundColor?.resolve({MaterialState.focused}) ?? Theme.of(context).colorScheme.onSurface; @@ -328,8 +367,9 @@ class _DropdownMenuState extends State { ? () { 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 { 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 { 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 { 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 { controller: _textEditingController, onEditingComplete: () { if (currentHighlight != null) { - _textEditingController.text = filteredEntries[currentHighlight!].label; - _textEditingController.selection = - TextSelection.collapsed(offset: _textEditingController.text.length); + final DropdownMenuEntry 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; diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 3e831df15812..bb314a43f967 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -8,19 +8,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - final List menuChildren = []; + final List> menuChildren = >[]; for (final TestMenu value in TestMenu.values) { - final DropdownMenuEntry entry = DropdownMenuEntry(label: value.label); + final DropdownMenuEntry entry = DropdownMenuEntry(value: value, label: value.label); menuChildren.add(entry); } - Widget buildTest(ThemeData themeData, List entries, + Widget buildTest(ThemeData themeData, List> entries, {double? width, double? menuHeight, Widget? leadingIcon, Widget? label}) { return MaterialApp( theme: themeData, home: Scaffold( - body: DropdownMenu( + body: DropdownMenu( label: label, leadingIcon: leadingIcon, width: width, @@ -80,7 +80,7 @@ void main() { theme: themeData, home: Scaffold( body: SafeArea( - child: DropdownMenu( + child: DropdownMenu( enabled: false, dropdownMenuEntries: menuChildren, ), @@ -115,7 +115,7 @@ void main() { theme: themeData, home: Scaffold( body: SafeArea( - child: DropdownMenu( + child: DropdownMenu( 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)); 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 shortMenuItems = []; + final List> shortMenuItems = >[]; for (final ShortMenu value in ShortMenu.values) { - final DropdownMenuEntry entry = DropdownMenuEntry(label: value.label); + final DropdownMenuEntry entry = DropdownMenuEntry(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)); expect(box.size.width, customBigWidth); - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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)); expect(box.size.width, customSmallWidth); - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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)); 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)); 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)); 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)); 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)); 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( 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)); 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( 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)); final Finder updatedLabel = find.text('label'); final Offset updatedLabelTopRight = tester.getTopRight(updatedLabel); - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( 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)); final Finder updatedLabel1 = find.text('label'); final Offset updatedLabelTopRight1 = tester.getTopRight(updatedLabel1); - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( 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( trailingIcon: const Icon(Icons.ac_unit), dropdownMenuEntries: menuChildren, ), ), )); - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( dropdownMenuEntries: menuChildren, ), ), )); - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( dropdownMenuEntries: menuChildren, ), ), )); // Open the menu - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( dropdownMenuEntries: menuChildren, ), ), )); // Open the menu - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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 menuWithDisabledItems = [ - 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> menuWithDisabledItems = >[ + const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), + const DropdownMenuEntry(value: TestMenu.mainMenu1, label: 'Item 1', enabled: false), + const DropdownMenuEntry(value: TestMenu.mainMenu2, label: 'Item 2', enabled: false), + const DropdownMenuEntry(value: TestMenu.mainMenu3, label: 'Item 3'), + const DropdownMenuEntry(value: TestMenu.mainMenu4, label: 'Item 4'), + const DropdownMenuEntry(value: TestMenu.mainMenu5, label: 'Item 5', enabled: false), ]; await tester.pumpWidget(MaterialApp( theme: themeData, home: Scaffold( - body: DropdownMenu( + body: DropdownMenu( 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)); 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( dropdownMenuEntries: menuChildren, ), ), )); // Open the menu - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( dropdownMenuEntries: menuChildren, ), ), )); // Open the menu - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( dropdownMenuEntries: menuChildren, ), ), )); // Open the menu - await tester.tap(find.byType(DropdownMenu)); + await tester.tap(find.byType(DropdownMenu)); 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( 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)); 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( + enableFilter: true, + dropdownMenuEntries: menuChildren, + controller: controller, + ), + ); + } + ), + )); + + // Open the menu + await tester.tap(find.byType(DropdownMenu)); + 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> menuWithDisabledItems = >[ + const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 0'), + const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 1', enabled: false), + const DropdownMenuEntry(value: TestMenu.mainMenu0, label: 'Item 2'), + const DropdownMenuEntry(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( + dropdownMenuEntries: menuWithDisabledItems, + controller: controller, + onSelected: (_) { + setState(() { + selectionCount++; + }); + }, + ), + ); + } + ), + )); + + // Open the menu + await tester.tap(find.byType(DropdownMenu)); + 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)); + 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)); + 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( + initialSelection: TestMenu.mainMenu3, + dropdownMenuEntries: menuChildren, + controller: controller, + ), + ); + } + ), + )); + + expect(find.widgetWithText(TextField, 'Item 3'), findsOneWidget); + + // Open the menu + await tester.tap(find.byType(DropdownMenu)); + 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(buttonMaterial); + expect(itemMaterial.color, themeData.colorScheme.onSurface.withOpacity(0.12)); + }); } enum TestMenu { diff --git a/packages/flutter/test/material/dropdown_menu_theme_test.dart b/packages/flutter/test/material/dropdown_menu_theme_test.dart index e733d51553eb..d8ee0e88c9e1 100644 --- a/packages/flutter/test/material/dropdown_menu_theme_test.dart +++ b/packages/flutter/test/material/dropdown_menu_theme_test.dart @@ -43,11 +43,11 @@ void main() { theme: themeData, home: const Scaffold( body: Center( - child: DropdownMenu( - dropdownMenuEntries: [ - DropdownMenuEntry(label: 'Item 0'), - DropdownMenuEntry(label: 'Item 1'), - DropdownMenuEntry(label: 'Item 2'), + child: DropdownMenu( + dropdownMenuEntries: >[ + DropdownMenuEntry(value: 0, label: 'Item 0'), + DropdownMenuEntry(value: 1, label: 'Item 1'), + DropdownMenuEntry(value: 2, label: 'Item 2'), ], ), ), @@ -122,11 +122,11 @@ void main() { theme: theme, home: const Scaffold( body: Center( - child: DropdownMenu( - dropdownMenuEntries: [ - DropdownMenuEntry(label: 'Item 0'), - DropdownMenuEntry(label: 'Item 1'), - DropdownMenuEntry(label: 'Item 2'), + child: DropdownMenu( + dropdownMenuEntries: >[ + DropdownMenuEntry(value: 0, label: 'Item 0'), + DropdownMenuEntry(value: 1, label: 'Item 1'), + DropdownMenuEntry(value: 2, label: 'Item 2'), ], ), ), @@ -223,11 +223,11 @@ void main() { data: dropdownMenuTheme, child: const Scaffold( body: Center( - child: DropdownMenu( - dropdownMenuEntries: [ - DropdownMenuEntry(label: 'Item 0'), - DropdownMenuEntry(label: 'Item 1'), - DropdownMenuEntry(label: 'Item 2'), + child: DropdownMenu( + dropdownMenuEntries: >[ + DropdownMenuEntry(value: 0, label: 'Item 0'), + DropdownMenuEntry(value: 1, label: 'Item 1'), + DropdownMenuEntry(value: 2, label: 'Item 2'), ], ), ), @@ -326,7 +326,7 @@ void main() { data: dropdownMenuTheme, child: Scaffold( body: Center( - child: DropdownMenu( + child: DropdownMenu( 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(label: 'Item 0'), - DropdownMenuEntry(label: 'Item 1'), - DropdownMenuEntry(label: 'Item 2'), + dropdownMenuEntries: const >[ + DropdownMenuEntry(value: 0, label: 'Item 0'), + DropdownMenuEntry(value: 1, label: 'Item 1'), + DropdownMenuEntry(value: 2, label: 'Item 2'), ], ), ),