Commit 6a1ac731 authored by Hans Muller's avatar Hans Muller Committed by GitHub

PopupMenuButton menus inherit the button theme (#6132)

parent d3d9e1ca
...@@ -246,6 +246,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -246,6 +246,7 @@ class _MaterialAppState extends State<MaterialApp> {
ThemeData theme = config.theme ?? new ThemeData.fallback(); ThemeData theme = config.theme ?? new ThemeData.fallback();
Widget result = new AnimatedTheme( Widget result = new AnimatedTheme(
data: theme, data: theme,
isMaterialAppTheme: true,
child: new WidgetsApp( child: new WidgetsApp(
key: new GlobalObjectKey(this), key: new GlobalObjectKey(this),
title: config.title, title: config.title,
......
...@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'material.dart'; import 'material.dart';
import 'theme.dart';
const Duration _kBottomSheetDuration = const Duration(milliseconds: 200); const Duration _kBottomSheetDuration = const Duration(milliseconds: 200);
const double _kMinFlingVelocity = 700.0; const double _kMinFlingVelocity = 700.0;
...@@ -204,10 +205,12 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> { ...@@ -204,10 +205,12 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
class _ModalBottomSheetRoute<T> extends PopupRoute<T> { class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
_ModalBottomSheetRoute({ _ModalBottomSheetRoute({
Completer<T> completer, Completer<T> completer,
this.builder this.builder,
this.theme,
}) : super(completer: completer); }) : super(completer: completer);
final WidgetBuilder builder; final WidgetBuilder builder;
final ThemeData theme;
@override @override
Duration get transitionDuration => _kBottomSheetDuration; Duration get transitionDuration => _kBottomSheetDuration;
...@@ -229,7 +232,10 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> { ...@@ -229,7 +232,10 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return new _ModalBottomSheet<T>(route: this); Widget bottomSheet = new _ModalBottomSheet<T>(route: this);
if (theme != null)
bottomSheet = new Theme(data: theme, child: bottomSheet);
return bottomSheet;
} }
} }
...@@ -257,7 +263,8 @@ Future<dynamic/*=T*/> showModalBottomSheet/*<T>*/({ BuildContext context, Widget ...@@ -257,7 +263,8 @@ Future<dynamic/*=T*/> showModalBottomSheet/*<T>*/({ BuildContext context, Widget
final Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>(); final Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>();
Navigator.push(context, new _ModalBottomSheetRoute<dynamic/*=T*/>( Navigator.push(context, new _ModalBottomSheetRoute<dynamic/*=T*/>(
completer: completer, completer: completer,
builder: builder builder: builder,
theme: Theme.of(context, shadowThemeOnly: true),
)); ));
return completer.future; return completer.future;
} }
...@@ -280,10 +280,12 @@ class SimpleDialog extends StatelessWidget { ...@@ -280,10 +280,12 @@ class SimpleDialog extends StatelessWidget {
class _DialogRoute<T> extends PopupRoute<T> { class _DialogRoute<T> extends PopupRoute<T> {
_DialogRoute({ _DialogRoute({
Completer<T> completer, Completer<T> completer,
this.child this.child,
this.theme,
}) : super(completer: completer); }) : super(completer: completer);
final Widget child; final Widget child;
final ThemeData theme;
@override @override
Duration get transitionDuration => const Duration(milliseconds: 150); Duration get transitionDuration => const Duration(milliseconds: 150);
...@@ -296,7 +298,7 @@ class _DialogRoute<T> extends PopupRoute<T> { ...@@ -296,7 +298,7 @@ class _DialogRoute<T> extends PopupRoute<T> {
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return child; return theme != null ? new Theme(data: theme, child: child) : child;
} }
@override @override
...@@ -324,6 +326,10 @@ class _DialogRoute<T> extends PopupRoute<T> { ...@@ -324,6 +326,10 @@ class _DialogRoute<T> extends PopupRoute<T> {
/// * <https://www.google.com/design/spec/components/dialogs.html> /// * <https://www.google.com/design/spec/components/dialogs.html>
Future<dynamic/*=T*/> showDialog/*<T>*/({ BuildContext context, Widget child }) { Future<dynamic/*=T*/> showDialog/*<T>*/({ BuildContext context, Widget child }) {
Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>(); Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>();
Navigator.push(context, new _DialogRoute<dynamic/*=T*/>(completer: completer, child: child)); Navigator.push(context, new _DialogRoute<dynamic/*=T*/>(
completer: completer,
child: child,
theme: Theme.of(context, shadowThemeOnly: true),
));
return completer.future; return completer.future;
} }
...@@ -293,7 +293,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -293,7 +293,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
this.buttonRect, this.buttonRect,
this.selectedIndex, this.selectedIndex,
this.elevation: 8, this.elevation: 8,
TextStyle style this.theme,
TextStyle style,
}) : _style = style, super(completer: completer) { }) : _style = style, super(completer: completer) {
assert(style != null); assert(style != null);
} }
...@@ -303,6 +304,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -303,6 +304,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
final Rect buttonRect; final Rect buttonRect;
final int selectedIndex; final int selectedIndex;
final int elevation; final int elevation;
final ThemeData theme;
// The layout gets this route's scrollableKey so that it can scroll the // The layout gets this route's scrollableKey so that it can scroll the
/// selected item into position, but only on the initial layout. /// selected item into position, but only on the initial layout.
bool initialLayout = true; bool initialLayout = true;
...@@ -329,9 +331,13 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> { ...@@ -329,9 +331,13 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
Widget menu = new _DropdownMenu<T>(route: this);
if (theme != null)
menu = new Theme(data: theme, child: menu);
return new CustomSingleChildLayout( return new CustomSingleChildLayout(
delegate: new _DropdownMenuRouteLayout<T>(route: this), delegate: new _DropdownMenuRouteLayout<T>(route: this),
child: new _DropdownMenu<T>(route: this) child: menu,
); );
} }
} }
...@@ -501,7 +507,8 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> { ...@@ -501,7 +507,8 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect), buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex, selectedIndex: _selectedIndex,
elevation: config.elevation, elevation: config.elevation,
style: _textStyle theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle,
); );
Navigator.push(context, _currentRoute); Navigator.push(context, _currentRoute);
completer.future.then((_DropdownRouteResult<T> newValue) { completer.future.then((_DropdownRouteResult<T> newValue) {
......
...@@ -374,13 +374,15 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -374,13 +374,15 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
this.position, this.position,
this.items, this.items,
this.initialValue, this.initialValue,
this.elevation this.elevation,
this.theme
}) : super(completer: completer); }) : super(completer: completer);
final RelativeRect position; final RelativeRect position;
final List<PopupMenuEntry<T>> items; final List<PopupMenuEntry<T>> items;
final dynamic initialValue; final dynamic initialValue;
final int elevation; final int elevation;
final ThemeData theme;
@override @override
Animation<double> createAnimation() { Animation<double> createAnimation() {
...@@ -411,9 +413,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -411,9 +413,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
selectedItemOffset += items[i].height; selectedItemOffset += items[i].height;
} }
} }
Widget menu = new _PopupMenu<T>(route: this);
if (theme != null)
menu = new Theme(data: theme, child: menu);
return new CustomSingleChildLayout( return new CustomSingleChildLayout(
delegate: new _PopupMenuRouteLayout(position, selectedItemOffset), delegate: new _PopupMenuRouteLayout(position, selectedItemOffset),
child: new _PopupMenu<T>(route: this) child: menu
); );
} }
} }
...@@ -438,7 +445,8 @@ Future<dynamic/*=T*/> showMenu/*<T>*/({ ...@@ -438,7 +445,8 @@ Future<dynamic/*=T*/> showMenu/*<T>*/({
position: position, position: position,
items: items, items: items,
initialValue: initialValue, initialValue: initialValue,
elevation: elevation elevation: elevation,
theme: Theme.of(context, shadowThemeOnly: true),
)); ));
return completer.future; return completer.future;
} }
......
...@@ -18,6 +18,7 @@ const Duration kThemeAnimationDuration = const Duration(milliseconds: 200); ...@@ -18,6 +18,7 @@ const Duration kThemeAnimationDuration = const Duration(milliseconds: 200);
/// ///
/// * [AnimatedTheme] /// * [AnimatedTheme]
/// * [ThemeData] /// * [ThemeData]
/// * [MaterialApp]
class Theme extends InheritedWidget { class Theme extends InheritedWidget {
/// Applies the given theme [data] to [child]. /// Applies the given theme [data] to [child].
/// ///
...@@ -25,6 +26,7 @@ class Theme extends InheritedWidget { ...@@ -25,6 +26,7 @@ class Theme extends InheritedWidget {
Theme({ Theme({
Key key, Key key,
@required this.data, @required this.data,
this.isMaterialAppTheme: false,
Widget child Widget child
}) : super(key: key, child: child) { }) : super(key: key, child: child) {
assert(child != null); assert(child != null);
...@@ -34,14 +36,33 @@ class Theme extends InheritedWidget { ...@@ -34,14 +36,33 @@ class Theme extends InheritedWidget {
/// Specifies the color and typography values for descendant widgets. /// Specifies the color and typography values for descendant widgets.
final ThemeData data; final ThemeData data;
/// True if this theme was installed by the [MaterialApp].
///
/// When an app uses the [Navigator] to push a route, the route's widgets
/// will only inherit from the app's theme, even though the widget that
/// triggered the push may inherit from a theme that "shadows" the app's
/// theme because it's deeper in the widget tree. Apps can find the shadowing
/// theme with `Theme.of(context, shadowThemeOnly: true)` and pass it along
/// to the class that creates a route's widgets. Material widgets that push
/// routes, like [PopupMenuButton] and [DropdownButton], do this.
final bool isMaterialAppTheme;
static final ThemeData _kFallbackTheme = new ThemeData.fallback(); static final ThemeData _kFallbackTheme = new ThemeData.fallback();
/// The data from the closest instance of this class that encloses the given context. /// The data from the closest instance of this class that encloses the given context.
/// ///
/// Defaults to the fallback theme data if none exists. /// Defaults to the fallback theme data if none exists.
static ThemeData of(BuildContext context) { ///
Theme theme = context.inheritFromWidgetOfExactType(Theme); /// If [shadowThemeOnly] is true and the closest Theme ancestor was installed by
return theme?.data ?? _kFallbackTheme; /// the [MaterialApp] - in other words if the closest Theme ancestor does not
/// shadow the app's theme - then return null. This property is specified in
/// situations where its useful to wrap a route's widgets with a Theme, but only
/// when the app's theme is being shadowed by a theme widget that is farather
/// down in the tree. See [isMaterialAppTheme].
static ThemeData of(BuildContext context, { bool shadowThemeOnly: false }) {
final Theme theme = context.inheritFromWidgetOfExactType(Theme);
final ThemeData themeData = theme?.data ?? _kFallbackTheme;
return shadowThemeOnly ? (theme.isMaterialAppTheme ? null : themeData) : themeData;
} }
@override @override
...@@ -77,6 +98,7 @@ class AnimatedTheme extends ImplicitlyAnimatedWidget { ...@@ -77,6 +98,7 @@ class AnimatedTheme extends ImplicitlyAnimatedWidget {
AnimatedTheme({ AnimatedTheme({
Key key, Key key,
@required this.data, @required this.data,
this.isMaterialAppTheme: false,
Curve curve: Curves.linear, Curve curve: Curves.linear,
Duration duration: kThemeAnimationDuration, Duration duration: kThemeAnimationDuration,
this.child this.child
...@@ -88,6 +110,9 @@ class AnimatedTheme extends ImplicitlyAnimatedWidget { ...@@ -88,6 +110,9 @@ class AnimatedTheme extends ImplicitlyAnimatedWidget {
/// Specifies the color and typography values for descendant widgets. /// Specifies the color and typography values for descendant widgets.
final ThemeData data; final ThemeData data;
/// True if this theme was created by the [MaterialApp]. See [Theme.isMaterialAppTheme].
final bool isMaterialAppTheme;
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
final Widget child; final Widget child;
...@@ -108,6 +133,7 @@ class _AnimatedThemeState extends AnimatedWidgetBaseState<AnimatedTheme> { ...@@ -108,6 +133,7 @@ class _AnimatedThemeState extends AnimatedWidgetBaseState<AnimatedTheme> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Theme( return new Theme(
isMaterialAppTheme: config.isMaterialAppTheme,
child: config.child, child: config.child,
data: _data.evaluate(animation) data: _data.evaluate(animation)
); );
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('PopupMenu inherits app theme', (WidgetTester tester) async {
final Key popupMenuButtonKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new PopupMenuButton<String>(
key: popupMenuButtonKey,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
new PopupMenuItem<String>(child: new Text('menuItem'))
];
}
),
]
)
)
)
);
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark));
});
testWidgets('PopupMenu inherits shadowed app theme', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/5572
final Key popupMenuButtonKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new PopupMenuButton<String>(
key: popupMenuButtonKey,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
new PopupMenuItem<String>(child: new Text('menuItem'))
];
}
),
]
)
)
)
)
);
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.light));
});
testWidgets('DropdownMenu inherits shadowed app theme', (WidgetTester tester) async {
final Key dropdownMenuButtonKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new DropdownButton<String>(
key: dropdownMenuButtonKey,
onChanged: (String newValue) { },
value: 'menuItem',
items: <DropdownMenuItem<String>>[
new DropdownMenuItem<String>(
value: 'menuItem',
child: new Text('menuItem'),
),
],
)
]
)
)
)
)
);
await tester.tap(find.byKey(dropdownMenuButtonKey));
await tester.pump(const Duration(seconds: 1));
for(Element item in tester.elementList(find.text('menuItem')))
expect(Theme.of(item).brightness, equals(Brightness.light));
});
testWidgets('ModalBottomSheet inherits shadowed app theme', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
body: new Center(
child: new Builder(
builder: (BuildContext context) {
return new RaisedButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) => new Text('bottomSheet'),
);
},
child: new Text('SHOW'),
);
}
)
)
)
)
)
);
await tester.tap(find.text('SHOW'));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('bottomSheet'))).brightness, equals(Brightness.light));
await tester.tap(find.text('bottomSheet')); // dismiss the bottom sheet
await tester.pump(const Duration(seconds: 1));
});
testWidgets('Dialog inherits shadowed app theme', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
key: scaffoldKey,
body: new Center(
child: new Builder(
builder: (BuildContext context) {
return new RaisedButton(
onPressed: () {
showDialog(
context: context,
child: new Text('dialog'),
);
},
child: new Text('SHOW'),
);
}
)
)
)
)
)
);
await tester.tap(find.text('SHOW'));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('dialog'))).brightness, equals(Brightness.light));
});
}
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