Unverified Commit e352e086 authored by YeungKC's avatar YeungKC Committed by GitHub

Add menuMaxHeight for DropdownButton (#76493)

parent 9300ad37
...@@ -325,7 +325,10 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate { ...@@ -325,7 +325,10 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
// the view height. This ensures a tappable area outside of the simple menu // the view height. This ensures a tappable area outside of the simple menu
// with which to dismiss the menu. // with which to dismiss the menu.
// -- https://material.io/design/components/menus.html#usage // -- https://material.io/design/components/menus.html#usage
final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight); double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
if (route.menuMaxHeight != null && route.menuMaxHeight! <= maxHeight) {
maxHeight = route.menuMaxHeight!;
}
// The width of a menu should be at most the view width. This ensures that // The width of a menu should be at most the view width. This ensures that
// the menu does not extend past the left and right edges of the screen. // the menu does not extend past the left and right edges of the screen.
final double width = math.min(constraints.maxWidth, buttonRect.width); final double width = math.min(constraints.maxWidth, buttonRect.width);
...@@ -411,6 +414,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -411,6 +414,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
this.barrierLabel, this.barrierLabel,
this.itemHeight, this.itemHeight,
this.dropdownColor, this.dropdownColor,
this.menuMaxHeight,
}) : assert(style != null), }) : assert(style != null),
itemHeights = List<double>.filled(items.length, itemHeight ?? kMinInteractiveDimension); itemHeights = List<double>.filled(items.length, itemHeight ?? kMinInteractiveDimension);
...@@ -423,6 +427,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -423,6 +427,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
final TextStyle style; final TextStyle style;
final double? itemHeight; final double? itemHeight;
final Color? dropdownColor; final Color? dropdownColor;
final double? menuMaxHeight;
final List<double> itemHeights; final List<double> itemHeights;
ScrollController? scrollController; ScrollController? scrollController;
...@@ -845,6 +851,7 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -845,6 +851,7 @@ class DropdownButton<T> extends StatefulWidget {
this.focusNode, this.focusNode,
this.autofocus = false, this.autofocus = false,
this.dropdownColor, this.dropdownColor,
this.menuMaxHeight,
// When adding new arguments, consider adding similar arguments to // When adding new arguments, consider adding similar arguments to
// DropdownButtonFormField. // DropdownButtonFormField.
}) : assert(items == null || items.isEmpty || value == null || }) : assert(items == null || items.isEmpty || value == null ||
...@@ -1094,6 +1101,17 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -1094,6 +1101,17 @@ class DropdownButton<T> extends StatefulWidget {
/// instead. /// instead.
final Color? dropdownColor; final Color? dropdownColor;
/// The maximum height of the menu.
///
/// The maximum height of the menu must be at least one row shorter than
/// the height of the app's view. This ensures that a tappable area
/// outside of the simple menu is present so the user can dismiss the menu.
///
/// If this property is set above the maximum allowable height threshold
/// mentioned above, then the menu defaults to being padded at the top
/// and bottom of the menu by at one menu item's height.
final double? menuMaxHeight;
@override @override
_DropdownButtonState<T> createState() => _DropdownButtonState<T>(); _DropdownButtonState<T> createState() => _DropdownButtonState<T>();
} }
...@@ -1240,6 +1258,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -1240,6 +1258,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
itemHeight: widget.itemHeight, itemHeight: widget.itemHeight,
dropdownColor: widget.dropdownColor, dropdownColor: widget.dropdownColor,
menuMaxHeight: widget.menuMaxHeight,
); );
navigator.push(_dropdownRoute!).then<void>((_DropdownRouteResult<T>? newValue) { navigator.push(_dropdownRoute!).then<void>((_DropdownRouteResult<T>? newValue) {
...@@ -1488,6 +1507,7 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1488,6 +1507,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
) )
bool autovalidate = false, bool autovalidate = false,
AutovalidateMode? autovalidateMode, AutovalidateMode? autovalidateMode,
double? menuMaxHeight,
}) : assert(items == null || items.isEmpty || value == null || }) : assert(items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItem<T> item) { items.where((DropdownMenuItem<T> item) {
return item.value == value; return item.value == value;
...@@ -1556,6 +1576,7 @@ class DropdownButtonFormField<T> extends FormField<T> { ...@@ -1556,6 +1576,7 @@ class DropdownButtonFormField<T> extends FormField<T> {
focusNode: focusNode, focusNode: focusNode,
autofocus: autofocus, autofocus: autofocus,
dropdownColor: dropdownColor, dropdownColor: dropdownColor,
menuMaxHeight: menuMaxHeight,
), ),
), ),
); );
......
...@@ -53,6 +53,7 @@ Widget buildDropdown({ ...@@ -53,6 +53,7 @@ Widget buildDropdown({
bool autofocus = false, bool autofocus = false,
Color? focusColor, Color? focusColor,
Color? dropdownColor, Color? dropdownColor,
double? menuMaxHeight,
}) { }) {
final List<DropdownMenuItem<String>>? listItems = items?.map<DropdownMenuItem<String>>((String item) { final List<DropdownMenuItem<String>>? listItems = items?.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
...@@ -85,6 +86,7 @@ Widget buildDropdown({ ...@@ -85,6 +86,7 @@ Widget buildDropdown({
items: listItems, items: listItems,
selectedItemBuilder: selectedItemBuilder, selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight, itemHeight: itemHeight,
menuMaxHeight: menuMaxHeight,
), ),
); );
} }
...@@ -109,6 +111,7 @@ Widget buildDropdown({ ...@@ -109,6 +111,7 @@ Widget buildDropdown({
items: listItems, items: listItems,
selectedItemBuilder: selectedItemBuilder, selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight, itemHeight: itemHeight,
menuMaxHeight: menuMaxHeight,
); );
} }
...@@ -137,6 +140,7 @@ Widget buildFrame({ ...@@ -137,6 +140,7 @@ Widget buildFrame({
Color? focusColor, Color? focusColor,
Color? dropdownColor, Color? dropdownColor,
bool isFormField = false, bool isFormField = false,
double? menuMaxHeight,
}) { }) {
return TestApp( return TestApp(
textDirection: textDirection, textDirection: textDirection,
...@@ -166,7 +170,9 @@ Widget buildFrame({ ...@@ -166,7 +170,9 @@ Widget buildFrame({
dropdownColor: dropdownColor, dropdownColor: dropdownColor,
items: items, items: items,
selectedItemBuilder: selectedItemBuilder, selectedItemBuilder: selectedItemBuilder,
itemHeight: itemHeight,), itemHeight: itemHeight,
menuMaxHeight: menuMaxHeight,
),
), ),
), ),
), ),
...@@ -2927,7 +2933,7 @@ void main() { ...@@ -2927,7 +2933,7 @@ void main() {
expect(menuItemTapCounters, <int>[0, 2, 1, 0]); expect(menuItemTapCounters, <int>[0, 2, 1, 0]);
}); });
testWidgets('does not crash when option is selected without waiting for opening animation to complete', (WidgetTester tester) async { testWidgets('Does not crash when option is selected without waiting for opening animation to complete', (WidgetTester tester) async {
// Regression test for b/171846624. // Regression test for b/171846624.
final List<String> options = <String>['first', 'second', 'third']; final List<String> options = <String>['first', 'second', 'third'];
...@@ -3007,6 +3013,59 @@ void main() { ...@@ -3007,6 +3013,59 @@ void main() {
expect(find.byType(Scrollbar), paints..rect()); expect(find.byType(Scrollbar), paints..rect());
}); });
testWidgets("Dropdown menu's maximum height should be influenced by DropdownButton.menuMaxHeight.", (WidgetTester tester) async {
await tester.pumpWidget(buildFrame(
value: '0',
items: List<String>.generate(/*length=*/64, (int index) => index.toString()),
onChanged: onChanged,
));
await tester.tap(find.text('0'));
await tester.pumpAndSettle();
final Element element = tester.element(find.byType(ListView));
double menuHeight = element.size!.height;
// The default maximum height should be one item height from the edge.
// https://material.io/design/components/menus.html#usage
final double mediaHeight = MediaQuery.of(element).size.height;
final double defaultMenuHeight = mediaHeight - (2 * kMinInteractiveDimension);
expect(menuHeight, defaultMenuHeight);
await tester.tap(find.text('0').last);
await tester.pumpAndSettle();
// Set menuMaxHeight which is less than defaultMenuHeight
await tester.pumpWidget(buildFrame(
value: '0',
items: List<String>.generate(/*length=*/64, (int index) => index.toString()),
onChanged: onChanged,
menuMaxHeight: 7 * kMinInteractiveDimension,
));
await tester.tap(find.text('0'));
await tester.pumpAndSettle();
menuHeight = tester.element(find.byType(ListView)).size!.height;
expect(menuHeight == defaultMenuHeight, isFalse);
expect(menuHeight, kMinInteractiveDimension * 7);
await tester.tap(find.text('0').last);
await tester.pumpAndSettle();
// Set menuMaxHeight which is greater than defaultMenuHeight
await tester.pumpWidget(buildFrame(
value: '0',
items: List<String>.generate(/*length=*/64, (int index) => index.toString()),
onChanged: onChanged,
menuMaxHeight: mediaHeight,
));
await tester.tap(find.text('0'));
await tester.pumpAndSettle();
menuHeight = tester.element(find.byType(ListView)).size!.height;
expect(menuHeight, defaultMenuHeight);
});
// Regression test for https://github.com/flutter/flutter/issues/76614 // Regression test for https://github.com/flutter/flutter/issues/76614
testWidgets('Do not crash if used in very short screen', (WidgetTester tester) async { testWidgets('Do not crash if used in very short screen', (WidgetTester tester) async {
// The default item height is 48.0 pixels and needs two items padding since // The default item height is 48.0 pixels and needs two items padding since
......
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