Unverified Commit cf12c314 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

DropdownButton Icon customizability (#29572)

parent 8da07214
......@@ -599,6 +599,9 @@ class DropdownButton<T> 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<T> 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<T> extends State<DropdownButton<T>> 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<T> extends State<DropdownButton<T>> with WidgetsBindi
);
}
const Icon defaultIcon = Icon(Icons.arrow_drop_down);
Widget result = DefaultTextStyle(
style: _textStyle,
child: Container(
......@@ -837,9 +876,12 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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,
),
],
),
......
......@@ -20,10 +20,21 @@ final Type dropdownButtonType = DropdownButton<String>(
items: const <DropdownMenuItem<String>>[],
).runtimeType;
Finder _iconRichText(Key iconKey) {
return find.descendant(
of: find.byKey(iconKey),
matching: find.byType(RichText),
);
}
Widget buildFrame({
Key buttonKey,
String value = 'two',
ValueChanged<String> 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<DropdownMenuItem<String>>((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<RichText>(_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<RichText>(_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<RichText>(_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<RichText>(_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<RichText>(_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<RichText>(_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';
......
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