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

Make DropdownButton's disabledHint and hint behavior consistent (#42479)

* Fix DropdownButton disabledHint behavior

* Fix hint behavior when selectedItemBuilder is null

* Improve variable names, some formatting updates

* Create _DropdownMenuItemContainer widget

* Improve API docs to be consistent with hint/disabledHint actual behavior
parent 109f2558
......@@ -537,17 +537,15 @@ class _RenderMenuItem extends RenderProxyBox {
}
}
/// An item in a menu created by a [DropdownButton].
///
/// The type `T` is the type of the value the entry represents. All the entries
/// in a given menu must represent values with consistent types.
class DropdownMenuItem<T> extends StatelessWidget {
// The container widget for a menu item created by a [DropdownButton]. It
// provides the default configuration for [DropdownMenuItem]s, as well as a
// [DropdownButton]'s hint and disabledHint widgets.
class _DropdownMenuItemContainer extends StatelessWidget {
/// Creates an item for a dropdown menu.
///
/// The [child] argument is required.
const DropdownMenuItem({
const _DropdownMenuItemContainer({
Key key,
this.value,
@required this.child,
}) : assert(child != null),
super(key: key);
......@@ -557,11 +555,6 @@ class DropdownMenuItem<T> extends StatelessWidget {
/// Typically a [Text] widget.
final Widget child;
/// The value to return if the user selects this menu item.
///
/// Eventually returned in a call to [DropdownButton.onChanged].
final T value;
@override
Widget build(BuildContext context) {
return Container(
......@@ -572,6 +565,27 @@ class DropdownMenuItem<T> extends StatelessWidget {
}
}
/// An item in a menu created by a [DropdownButton].
///
/// The type `T` is the type of the value the entry represents. All the entries
/// in a given menu must represent values with consistent types.
class DropdownMenuItem<T> extends _DropdownMenuItemContainer {
/// Creates an item for a dropdown menu.
///
/// The [child] argument is required.
const DropdownMenuItem({
Key key,
this.value,
@required Widget child,
}) : assert(child != null),
super(key: key, child: child);
/// The value to return if the user selects this menu item.
///
/// Eventually returned in a call to [DropdownButton.onChanged].
final T value;
}
/// An inherited widget that causes any descendant [DropdownButton]
/// widgets to not include their regular underline.
///
......@@ -658,7 +672,9 @@ class DropdownButtonHideUnderline extends InheritedWidget {
/// If the [onChanged] callback is null or the list of [items] is null
/// then the dropdown button will be disabled, i.e. its arrow will be
/// displayed in grey and it will not respond to input. A disabled button
/// will display the [disabledHint] widget if it is non-null.
/// will display the [disabledHint] widget if it is non-null. However, if
/// [disabledHint] is null and [hint] is non-null, the [hint] widget will
/// instead be displayed.
///
/// Requires one of its ancestors to be a [Material] widget.
///
......@@ -676,6 +692,8 @@ class DropdownButton<T> extends StatefulWidget {
/// must be equal to one of the [DropDownMenuItem] values. If [items] or
/// [onChanged] is null, the button will be disabled, the down arrow
/// will be greyed out, and the [disabledHint] will be shown (if provided).
/// If [disabledHint] is null and [hint] is non-null, [hint] will instead be
/// shown.
///
/// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified). The boolean [isDense] and
......@@ -715,20 +733,28 @@ class DropdownButton<T> extends StatefulWidget {
/// If the [onChanged] callback is null or the list of items is null
/// then the dropdown button will be disabled, i.e. its arrow will be
/// displayed in grey and it will not respond to input. A disabled button
/// will display the [disabledHint] widget if it is non-null.
/// will display the [disabledHint] widget if it is non-null. If
/// [disabledHint] is also null but [hint] is non-null, [hint] will instead
/// be displayed.
final List<DropdownMenuItem<T>> items;
/// The value of the currently selected [DropdownMenuItem], or null if no
/// item has been selected. If `value` is null then the menu is popped up as
/// if the first item were selected.
/// The value of the currently selected [DropdownMenuItem].
///
/// If [value] is null and [hint] is non-null, the [hint] widget is
/// displayed as a placeholder for the dropdown button's value.
final T value;
/// A placeholder widget that is displayed if no item is selected, i.e. if [value] is null.
/// A placeholder widget that is displayed by the dropdown button.
///
/// If [value] is null, this widget is displayed as a placeholder for
/// the dropdown button's value. This widget is also displayed if the button
/// is disabled ([items] or [onChanged] is null) and [disabledHint] is null.
final Widget hint;
/// A message to show when the dropdown is disabled.
///
/// Displayed if [items] or [onChanged] is null.
/// Displayed if [items] or [onChanged] is null. If [hint] is non-null and
/// [disabledHint] is null, the [hint] widget will be displayed instead.
final Widget disabledHint;
/// {@template flutter.material.dropdownButton.onChanged}
......@@ -737,7 +763,9 @@ class DropdownButton<T> extends StatefulWidget {
/// If the [onChanged] callback is null or the list of [items] is null
/// then the dropdown button will be disabled, i.e. its arrow will be
/// displayed in grey and it will not respond to input. A disabled button
/// will display the [disabledHint] widget if it is non-null.
/// will display the [disabledHint] widget if it is non-null. If
/// [disabledHint] is also null but [hint] is non-null, [hint] will instead
/// be displayed.
/// {@endtemplate}
final ValueChanged<T> onChanged;
......@@ -1113,28 +1141,25 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
if (_enabled) {
items = widget.selectedItemBuilder == null
? List<Widget>.from(widget.items)
: widget.selectedItemBuilder(context).map<Widget>((Widget item) {
return Container(
constraints: const BoxConstraints(minHeight: _kMenuItemHeight),
alignment: AlignmentDirectional.centerStart,
child: item,
);
}).toList();
: widget.selectedItemBuilder(context);
} else {
items = <Widget>[];
items = widget.selectedItemBuilder == null
? <Widget>[]
: widget.selectedItemBuilder(context);
}
int hintIndex;
if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
final Widget emplacedHint = _enabled
? widget.hint
: DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint);
Widget displayedHint = _enabled ? widget.hint : widget.disabledHint ?? widget.hint;
if (widget.selectedItemBuilder == null)
displayedHint = _DropdownMenuItemContainer(child: displayedHint);
hintIndex = items.length;
items.add(DefaultTextStyle(
style: _textStyle.copyWith(color: Theme.of(context).hintColor),
child: IgnorePointer(
child: emplacedHint,
ignoringSemantics: false,
child: displayedHint,
),
));
}
......
......@@ -42,6 +42,8 @@ Widget buildFrame({
Widget disabledHint,
Widget underline,
List<String> items = menuItems,
List<Widget> Function(BuildContext) selectedItemBuilder,
double itemHeight = kMinInteractiveDimension,
Alignment alignment = Alignment.center,
TextDirection textDirection = TextDirection.ltr,
Size mediaSize,
......@@ -79,6 +81,8 @@ Widget buildFrame({
child: Text(item, key: ValueKey<String>(item + 'Text')),
);
}).toList(),
selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight,
),
),
),
......@@ -1089,7 +1093,8 @@ void main() {
Widget build({ List<String> items, ValueChanged<String> onChanged }) => buildFrame(
items: items,
onChanged: onChanged,
buttonKey: buttonKey, value: null,
buttonKey: buttonKey,
value: null,
hint: const Text('enabled'),
disabledHint: const Text('disabled'));
......@@ -1119,6 +1124,311 @@ void main() {
expect(enabledHintBox.size, equals(disabledHintBox.size));
});
testWidgets('hint displays when the items list is empty, items is null, and disabledHint is null', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String> items }){
return buildFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
disabledHint: null,
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build(items: null));
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('disabledHint is null by default', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String> items }){
return buildFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build(items: null));
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('Size of largest widget is used DropdownButton when selectedItemBuilder is non-null', (WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
const String selectedItem = '25';
await tester.pumpWidget(buildFrame(
// To test the size constraints, the selected item should not be the
// largest item. This validates that the button sizes itself according
// to the largest item regardless of which one is selected.
value: selectedItem,
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: (String newValue) {},
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, '25'),
);
// DropdownButton should be the height of the largest item
expect(dropdownButtonRenderBox.size.height, 100);
// DropdownButton should be width of largest item added to the icon size
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
});
testWidgets(
'Enabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
'is non-null and hint is defined, but smaller than largest selected item widget',
(WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
await tester.pumpWidget(buildFrame(
value: null,
// [hint] widget is smaller than largest selected item widget
hint: Container(
height: 50,
width: 50,
child: const Text('hint')
),
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: (String newValue) {},
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, 'hint'),
);
// DropdownButton should be the height of the largest item
expect(dropdownButtonRenderBox.size.height, 100);
// DropdownButton should be width of largest item added to the icon size
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
},
);
testWidgets(
'Enabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
'is non-null and hint is defined, but larger than largest selected item widget',
(WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
const String selectedItem = '25';
await tester.pumpWidget(buildFrame(
// To test the size constraints, the selected item should not be the
// largest item. This validates that the button sizes itself according
// to the largest item regardless of which one is selected.
value: selectedItem,
// [hint] widget is larger than largest selected item widget
hint: Container(
height: 125,
width: 125,
child: const Text('hint')
),
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: (String newValue) {},
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, '25'),
);
// DropdownButton should be the height of the largest item (hint inclusive)
expect(dropdownButtonRenderBox.size.height, 125);
// DropdownButton should be width of largest item (hint inclusive) added to the icon size
expect(dropdownButtonRenderBox.size.width, 125 + 24.0);
},
);
testWidgets(
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
'is non-null, and hint is defined, but smaller than largest selected item widget',
(WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
await tester.pumpWidget(buildFrame(
value: null,
// [hint] widget is smaller than largest selected item widget
hint: Container(
height: 50,
width: 50,
child: const Text('hint')
),
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: null,
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, 'hint'),
);
// DropdownButton should be the height of the largest item
expect(dropdownButtonRenderBox.size.height, 100);
// DropdownButton should be width of largest item added to the icon size
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
},
);
testWidgets(
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
'is non-null and hint is defined, but larger than largest selected item widget',
(WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
await tester.pumpWidget(buildFrame(
value: null,
// [hint] widget is larger than largest selected item widget
hint: Container(
height: 125,
width: 125,
child: const Text('hint')
),
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: null,
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, '25'),
);
// DropdownButton should be the height of the largest item (hint inclusive)
expect(dropdownButtonRenderBox.size.height, 125);
// DropdownButton should be width of largest item (hint inclusive) added to the icon size
expect(dropdownButtonRenderBox.size.width, 125 + 24.0);
},
);
testWidgets(
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
'is non-null, and disabledHint is defined, but smaller than largest selected item widget',
(WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
await tester.pumpWidget(buildFrame(
value: null,
// [hint] widget is smaller than largest selected item widget
disabledHint: Container(
height: 50,
width: 50,
child: const Text('hint')
),
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: null,
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, 'hint'),
);
// DropdownButton should be the height of the largest item
expect(dropdownButtonRenderBox.size.height, 100);
// DropdownButton should be width of largest item added to the icon size
expect(dropdownButtonRenderBox.size.width, 100 + 24.0);
},
);
testWidgets(
'Disabled button - Size of largest widget is used DropdownButton when selectedItemBuilder '
'is non-null and disabledHint is defined, but larger than largest selected item widget',
(WidgetTester tester) async {
final List<String> items = <String>['25', '50', '100'];
await tester.pumpWidget(buildFrame(
value: null,
// [hint] widget is larger than largest selected item widget
disabledHint: Container(
height: 125,
width: 125,
child: const Text('hint')
),
items: items,
itemHeight: null,
selectedItemBuilder: (BuildContext context) {
return items.map<Widget>((String item) {
return Container(
height: double.parse(item),
width: double.parse(item),
child: Center(child: Text(item)),
);
}).toList();
},
onChanged: null,
));
final RenderBox dropdownButtonRenderBox = tester.renderObject<RenderBox>(
find.widgetWithText(Row, '25'),
);
// DropdownButton should be the height of the largest item (hint inclusive)
expect(dropdownButtonRenderBox.size.height, 125);
// DropdownButton should be width of largest item (hint inclusive) added to the icon size
expect(dropdownButtonRenderBox.size.width, 125 + 24.0);
},
);
testWidgets('Dropdown in middle showing middle item', (WidgetTester tester) async {
final List<DropdownMenuItem<int>> items =
List<DropdownMenuItem<int>>.generate(100, (int i) =>
......@@ -1397,7 +1707,7 @@ void main() {
selectedItem = string;
}),
selectedItemBuilder: (BuildContext context) {
return items.map((String item) {
return items.map<Widget>((String item) {
return Text('You have selected: $item');
}).toList();
},
......@@ -1613,6 +1923,67 @@ void main() {
expect(find.text('disabled'), findsOneWidget);
});
testWidgets('Dropdown form field - hint displays when the items list is empty, items is null, and disabledHint is null', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String> items }){
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
disabledHint: null,
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build(items: null));
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('Dropdown form field - disabledHint is null by default', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String> items }){
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build(items: null));
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('Dropdown form field - disabledHint is null by default', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({ List<String> items }){
return buildFormFrame(
items: items,
buttonKey: buttonKey,
value: null,
hint: const Text('hint used when disabled'),
);
}
// [hint] should display when [items] is null and [disabledHint] is not defined
await tester.pumpWidget(build(items: null));
expect(find.text('hint used when disabled'), findsOneWidget);
// [hint] should display when [items] is an empty list and [disabledHint] is not defined.
await tester.pumpWidget(build(items: <String>[]));
expect(find.text('hint used when disabled'), findsOneWidget);
});
testWidgets('Dropdown form field - disabledHint displays when onChanged is null', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
......
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