Unverified Commit 751f119c authored by Hans Muller's avatar Hans Muller Committed by GitHub

Ensure that the DropdownButton menu respects its parents bounds (#28371)

parent fc6079a2
...@@ -327,19 +327,67 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -327,19 +327,67 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return _DropdownRoutePage<T>(
route: this,
constraints: constraints,
items: items,
padding: padding,
buttonRect: buttonRect,
selectedIndex: selectedIndex,
elevation: elevation,
theme: theme,
style: style,
);
}
);
}
void _dismiss() {
navigator?.removeRoute(this);
}
}
class _DropdownRoutePage<T> extends StatelessWidget {
const _DropdownRoutePage({
Key key,
this.route,
this.constraints,
this.items,
this.padding,
this.buttonRect,
this.selectedIndex,
this.elevation = 8,
this.theme,
this.style,
}) : super(key: key);
final _DropdownRoute<T> route;
final BoxConstraints constraints;
final List<DropdownMenuItem<T>> items;
final EdgeInsetsGeometry padding;
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final ThemeData theme;
final TextStyle style;
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context)); assert(debugCheckHasDirectionality(context));
final double screenHeight = MediaQuery.of(context).size.height; final double availableHeight = constraints.maxHeight;
final double maxMenuHeight = screenHeight - 2.0 * _kMenuItemHeight; final double maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight;
final double buttonTop = buttonRect.top; final double buttonTop = buttonRect.top;
final double buttonBottom = buttonRect.bottom; final double buttonBottom = math.min(buttonRect.bottom, availableHeight);
// If the button is placed on the bottom or top of the screen, its top or // If the button is placed on the bottom or top of the screen, its top or
// bottom may be less than [_kMenuItemHeight] from the edge of the screen. // bottom may be less than [_kMenuItemHeight] from the edge of the screen.
// In this case, we want to change the menu limits to align with the top // In this case, we want to change the menu limits to align with the top
// or bottom edge of the button. // or bottom edge of the button.
final double topLimit = math.min(_kMenuItemHeight, buttonTop); final double topLimit = math.min(_kMenuItemHeight, buttonTop);
final double bottomLimit = math.max(screenHeight - _kMenuItemHeight, buttonBottom); final double bottomLimit = math.max(availableHeight - _kMenuItemHeight, buttonBottom);
final double selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top; final double selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
...@@ -359,24 +407,25 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -359,24 +407,25 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
// respectively. // respectively.
if (menuTop < topLimit) if (menuTop < topLimit)
menuTop = math.min(buttonTop, topLimit); menuTop = math.min(buttonTop, topLimit);
if (menuBottom > bottomLimit) { if (menuBottom > bottomLimit) {
menuBottom = math.max(buttonBottom, bottomLimit); menuBottom = math.max(buttonBottom, bottomLimit);
menuTop = menuBottom - menuHeight; menuTop = menuBottom - menuHeight;
} }
if (scrollController == null) { if (route.scrollController == null) {
// The limit is asymmetrical because we do not care how far positive the // The limit is asymmetrical because we do not care how far positive the
// limit goes. We are only concerned about the case where the value of // limit goes. We are only concerned about the case where the value of
// [buttonTop - menuTop] is larger than selectedItemOffset, ie. when // [buttonTop - menuTop] is larger than selectedItemOffset, ie. when
// the button is close to the bottom of the screen and the selected item // the button is close to the bottom of the screen and the selected item
// is close to 0. // is close to 0.
final double scrollOffset = preferredMenuHeight > maxMenuHeight ? math.max(0.0, selectedItemOffset - (buttonTop - menuTop)) : 0.0; final double scrollOffset = preferredMenuHeight > maxMenuHeight ? math.max(0.0, selectedItemOffset - (buttonTop - menuTop)) : 0.0;
scrollController = ScrollController(initialScrollOffset: scrollOffset); route.scrollController = ScrollController(initialScrollOffset: scrollOffset);
} }
final TextDirection textDirection = Directionality.of(context); final TextDirection textDirection = Directionality.of(context);
Widget menu = _DropdownMenu<T>( Widget menu = _DropdownMenu<T>(
route: this, route: route,
padding: padding.resolve(textDirection), padding: padding.resolve(textDirection),
); );
...@@ -404,10 +453,6 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -404,10 +453,6 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
), ),
); );
} }
void _dismiss() {
navigator?.removeRoute(this);
}
} }
/// An item in a menu created by a [DropdownButton]. /// An item in a menu created by a [DropdownButton].
......
...@@ -1035,4 +1035,49 @@ void main() { ...@@ -1035,4 +1035,49 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(getMenuScroll(), 4312.0); expect(getMenuScroll(), 4312.0);
}); });
testWidgets('Dropdown menu respects parent size limits', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/24417
int selectedIndex;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
bottomNavigationBar: const SizedBox(height: 200),
body: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return SafeArea(
child: Container(
alignment: Alignment.topLeft,
// From material/dropdown.dart (menus are unaligned by default):
// _kUnalignedMenuMargin = EdgeInsetsDirectional.only(start: 16.0, end: 24.0)
// This padding ensures that the entire menu will be visible
padding: const EdgeInsetsDirectional.only(start: 16.0, end: 24.0),
child: DropdownButton<int>(
value: 12,
onChanged: (int i) { selectedIndex = i; },
items: List<DropdownMenuItem<int>>.generate(100, (int i) {
return DropdownMenuItem<int>(value: i, child: Text('$i'));
}),
),
),
);
},
);
},
),
),
),
);
await tester.tap(find.text('12'));
await tester.pumpAndSettle();
expect(selectedIndex, null);
await tester.tap(find.text('13').last);
await tester.pumpAndSettle();
expect(selectedIndex, 13);
});
} }
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