diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index 092125e9cd5..5afc7e3acb0 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -599,6 +599,9 @@ class DropdownButton extends StatefulWidget { @required this.onChanged, this.elevation = 8, this.style, + this.icon, + this.iconDisabledColor, + this.iconEnabledColor, this.iconSize = 24.0, this.isDense = false, this.isExpanded = false, @@ -653,6 +656,27 @@ class DropdownButton extends StatefulWidget { /// [ThemeData.textTheme] of the current [Theme]. final TextStyle style; + /// The widget to use for the drop-down button's icon. + /// + /// Defaults to an [Icon] with the [Icons.arrow_drop_down] glyph. + final Widget icon; + + /// The color of any [Icon] descendant of [icon] if this button is disabled, + /// i.e. if [onChanged] is null. + /// + /// Defaults to [Colors.grey.shade400] when the theme's + /// [ThemeData.brightness] is [Brightness.light] and to + /// [Colors.white10] when it is [Brightness.dark] + final Color iconDisabledColor; + + /// The color of any [Icon] descendant of [icon] if this button is enabled, + /// i.e. if [onChanged] is defined. + /// + /// Defaults to [Colors.grey.shade700] when the theme's + /// [ThemeData.brightness] is [Brightness.light] and to + /// [Colors.white70] when it is [Brightness.dark] + final Color iconEnabledColor; + /// The size to use for the drop-down button's down arrow icon button. /// /// Defaults to 24.0. @@ -768,21 +792,34 @@ class _DropdownButtonState extends State> with WidgetsBindi return math.max(_textStyle.fontSize, math.max(widget.iconSize, _kDenseButtonHeight)); } - Color get _downArrowColor { + Color get _iconColor { // These colors are not defined in the Material Design spec. if (_enabled) { - if (Theme.of(context).brightness == Brightness.light) { - return Colors.grey.shade700; - } else { - return Colors.white70; + if (widget.iconEnabledColor != null) { + return widget.iconEnabledColor; + } + + switch(Theme.of(context).brightness) { + case Brightness.light: + return Colors.grey.shade700; + case Brightness.dark: + return Colors.white70; } } else { - if (Theme.of(context).brightness == Brightness.light) { - return Colors.grey.shade400; - } else { - return Colors.white10; + if (widget.iconDisabledColor != null) { + return widget.iconDisabledColor; + } + + switch(Theme.of(context).brightness) { + case Brightness.light: + return Colors.grey.shade400; + case Brightness.dark: + return Colors.white10; } } + + assert(false); + return null; } bool get _enabled => widget.items != null && widget.items.isNotEmpty && widget.onChanged != null; @@ -827,6 +864,8 @@ class _DropdownButtonState extends State> with WidgetsBindi ); } + const Icon defaultIcon = Icon(Icons.arrow_drop_down); + Widget result = DefaultTextStyle( style: _textStyle, child: Container( @@ -837,9 +876,12 @@ class _DropdownButtonState extends State> with WidgetsBindi mainAxisSize: MainAxisSize.min, children: [ widget.isExpanded ? Expanded(child: innerItemsWidget) : innerItemsWidget, - Icon(Icons.arrow_drop_down, - size: widget.iconSize, - color: _downArrowColor, + IconTheme( + data: IconThemeData( + color: _iconColor, + size: widget.iconSize, + ), + child: widget.icon ?? defaultIcon, ), ], ), diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index 14c0075f790..b70fd241030 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -20,10 +20,21 @@ final Type dropdownButtonType = DropdownButton( items: const >[], ).runtimeType; +Finder _iconRichText(Key iconKey) { + return find.descendant( + of: find.byKey(iconKey), + matching: find.byType(RichText), + ); +} + Widget buildFrame({ Key buttonKey, String value = 'two', ValueChanged onChanged, + Widget icon, + Color iconDisabledColor, + Color iconEnabledColor, + double iconSize = 24.0, bool isDense = false, bool isExpanded = false, Widget hint, @@ -44,6 +55,10 @@ Widget buildFrame({ hint: hint, disabledHint: disabledHint, onChanged: onChanged, + icon: icon, + iconSize: iconSize, + iconDisabledColor: iconDisabledColor, + iconEnabledColor: iconEnabledColor, isDense: isDense, isExpanded: isExpanded, items: items == null ? null : items.map>((String item) { @@ -430,6 +445,134 @@ void main() { buttonBox.size.centerRight(Offset(-arrowIcon.size.width, 0.0)).dx); }); + testWidgets('Dropdown button icon will accept widgets as icons', (WidgetTester tester) async { + final Widget customWidget = Container( + decoration: ShapeDecoration( + shape: CircleBorder( + side: BorderSide( + width: 5.0, + color: Colors.grey.shade700, + ), + ), + ), + ); + + await tester.pumpWidget(buildFrame( + icon: customWidget, + onChanged: onChanged, + )); + + expect(find.byWidget(customWidget), findsOneWidget); + expect(find.byIcon(Icons.arrow_drop_down), findsNothing); + + await tester.pumpWidget(buildFrame( + icon: const Icon(Icons.assessment), + onChanged: onChanged, + )); + + expect(find.byIcon(Icons.assessment), findsOneWidget); + expect(find.byIcon(Icons.arrow_drop_down), findsNothing); + }); + + testWidgets('Dropdown button icon should have default size and colors when not defined', (WidgetTester tester) async { + final Key iconKey = UniqueKey(); + final Icon customIcon = Icon(Icons.assessment, key: iconKey); + + await tester.pumpWidget(buildFrame( + icon: customIcon, + onChanged: onChanged, + )); + + // test for size + final RenderBox icon = tester.renderObject(find.byKey(iconKey)); + expect(icon.size, const Size(24.0, 24.0)); + + // test for enabled color + final RichText enabledRichText = tester.widget(_iconRichText(iconKey)); + expect(enabledRichText.text.style.color, Colors.grey.shade700); + + // test for disabled color + await tester.pumpWidget(buildFrame( + icon: customIcon, + onChanged: null, + )); + + final RichText disabledRichText = tester.widget(_iconRichText(iconKey)); + expect(disabledRichText.text.style.color, Colors.grey.shade400); + }); + + testWidgets('Dropdown button icon should have the passed in size and color instead of defaults', (WidgetTester tester) async { + final Key iconKey = UniqueKey(); + final Icon customIcon = Icon(Icons.assessment, key: iconKey); + + await tester.pumpWidget(buildFrame( + icon: customIcon, + iconSize: 30.0, + iconEnabledColor: Colors.pink, + iconDisabledColor: Colors.orange, + onChanged: onChanged, + )); + + // test for size + final RenderBox icon = tester.renderObject(find.byKey(iconKey)); + expect(icon.size, const Size(30.0, 30.0)); + + // test for enabled color + final RichText enabledRichText = tester.widget(_iconRichText(iconKey)); + expect(enabledRichText.text.style.color, Colors.pink); + + // test for disabled color + await tester.pumpWidget(buildFrame( + icon: customIcon, + iconSize: 30.0, + iconEnabledColor: Colors.pink, + iconDisabledColor: Colors.orange, + onChanged: null, + )); + + final RichText disabledRichText = tester.widget(_iconRichText(iconKey)); + expect(disabledRichText.text.style.color, Colors.orange); + }); + + testWidgets('Dropdown button should use its own size and color properties over those defined by the theme', (WidgetTester tester) async { + final Key iconKey = UniqueKey(); + + final Icon customIcon = Icon( + Icons.assessment, + key: iconKey, + size: 40.0, + color: Colors.yellow, + ); + + await tester.pumpWidget(buildFrame( + icon: customIcon, + iconSize: 30.0, + iconEnabledColor: Colors.pink, + iconDisabledColor: Colors.orange, + onChanged: onChanged, + )); + + // test for size + final RenderBox icon = tester.renderObject(find.byKey(iconKey)); + expect(icon.size, const Size(40.0, 40.0)); + + // test for enabled color + final RichText enabledRichText = tester.widget(_iconRichText(iconKey)); + expect(enabledRichText.text.style.color, Colors.yellow); + + // test for disabled color + await tester.pumpWidget(buildFrame( + icon: customIcon, + iconSize: 30.0, + iconEnabledColor: Colors.pink, + iconDisabledColor: Colors.orange, + onChanged: null, + )); + + final RichText disabledRichText = tester.widget(_iconRichText(iconKey)); + expect(disabledRichText.text.style.color, Colors.yellow); + }); + testWidgets('Dropdown button with isDense:true aligns selected menu item', (WidgetTester tester) async { final Key buttonKey = UniqueKey(); const String value = 'two';