diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index d9ec8c072a..864bc8e42a 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -83,6 +83,7 @@ export 'src/material/page.dart'; export 'src/material/page_transitions_theme.dart'; export 'src/material/paginated_data_table.dart'; export 'src/material/popup_menu.dart'; +export 'src/material/popup_menu_theme.dart'; export 'src/material/progress_indicator.dart'; export 'src/material/radio.dart'; export 'src/material/radio_list_tile.dart'; diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 8210a2f9ed..a764df677f 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -16,6 +16,7 @@ import 'ink_well.dart'; import 'list_tile.dart'; import 'material.dart'; import 'material_localizations.dart'; +import 'popup_menu_theme.dart'; import 'theme.dart'; // Examples can assume: @@ -171,6 +172,7 @@ class PopupMenuItem extends PopupMenuEntry { this.value, this.enabled = true, this.height = _kMenuItemHeight, + this.textStyle, @required this.child, }) : assert(enabled != null), assert(height != null), @@ -191,6 +193,12 @@ class PopupMenuItem extends PopupMenuEntry { @override final double height; + /// The text style of the popup menu entry. + /// + /// If this property is null, then [PopupMenuThemeData.textStyle] is used. + /// If [PopupMenuThemeData.textStyle] is also null, then [ThemeData.textTheme.subhead] is used. + final TextStyle textStyle; + /// The widget below this widget in the tree. /// /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An @@ -245,7 +253,9 @@ class PopupMenuItemState> extends State { @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); - TextStyle style = theme.textTheme.subhead; + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.subhead; + if (!widget.enabled) style = style.copyWith(color: theme.disabledColor); @@ -433,6 +443,7 @@ class _PopupMenu extends StatelessWidget { Widget build(BuildContext context) { final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. final List children = []; + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); for (int i = 0; i < route.items.length; i += 1) { final double start = (i + 1) * unit; @@ -486,8 +497,10 @@ class _PopupMenu extends StatelessWidget { return Opacity( opacity: opacity.evaluate(route.animation), child: Material( + shape: route.shape ?? popupMenuTheme.shape, + color: route.color ?? popupMenuTheme.color, type: MaterialType.card, - elevation: route.elevation, + elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0, child: Align( alignment: AlignmentDirectional.topEnd, widthFactor: width.evaluate(route.animation), @@ -589,8 +602,11 @@ class _PopupMenuRoute extends PopupRoute { this.initialValue, this.elevation, this.theme, + this.popupMenuTheme, this.barrierLabel, this.semanticLabel, + this.shape, + this.color, }); final RelativeRect position; @@ -599,6 +615,9 @@ class _PopupMenuRoute extends PopupRoute { final double elevation; final ThemeData theme; final String semanticLabel; + final ShapeBorder shape; + final Color color; + final PopupMenuThemeData popupMenuTheme; @override Animation createAnimation() { @@ -636,6 +655,8 @@ class _PopupMenuRoute extends PopupRoute { } Widget menu = _PopupMenu(route: this, semanticLabel: semanticLabel); + if (popupMenuTheme != null) + menu = PopupMenuTheme(textStyle: popupMenuTheme.textStyle, child: menu); if (theme != null) menu = Theme(data: theme, child: menu); @@ -717,8 +738,10 @@ Future showMenu({ @required RelativeRect position, @required List> items, T initialValue, - double elevation = 8.0, + double elevation, String semanticLabel, + ShapeBorder shape, + Color color, }) { assert(context != null); assert(position != null); @@ -741,7 +764,10 @@ Future showMenu({ elevation: elevation, semanticLabel: label, theme: Theme.of(context, shadowThemeOnly: true), + popupMenuTheme: PopupMenuTheme.of(context), barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, + shape: shape, + color: color, )); } @@ -826,12 +852,14 @@ class PopupMenuButton extends StatefulWidget { this.onSelected, this.onCanceled, this.tooltip, - this.elevation = 8.0, + this.elevation, this.padding = const EdgeInsets.all(8.0), this.child, this.icon, this.offset = Offset.zero, this.enabled = true, + this.shape, + this.color, }) : assert(itemBuilder != null), assert(offset != null), assert(enabled != null), @@ -898,12 +926,28 @@ class PopupMenuButton extends StatefulWidget { /// but doesn't currently have anything to show in the menu. final bool enabled; + /// If provided, the shape used for the menu. + /// + /// If this property is null, then [PopupMenuThemeData.shape] is used. + /// If [PopupMenuThemeData.shape] is also null, then the default shape for + /// [MaterialType.card] is used. This default shape is a rectangle with + /// rounded edges of BorderRadius.circular(2.0). + final ShapeBorder shape; + + /// If provided, the background color used for the menu. + /// + /// If this property is null, then [PopupMenuThemeData.color] is used. + /// If [PopupMenuThemeData.color] is also null, then + /// Theme.of(context).cardColor is used. + final Color color; + @override _PopupMenuButtonState createState() => _PopupMenuButtonState(); } class _PopupMenuButtonState extends State> { void showButtonMenu() { + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final RenderBox button = context.findRenderObject(); final RenderBox overlay = Overlay.of(context).context.findRenderObject(); final RelativeRect position = RelativeRect.fromRect( @@ -918,10 +962,12 @@ class _PopupMenuButtonState extends State> { if (items.isNotEmpty) { showMenu( context: context, - elevation: widget.elevation, + elevation: widget.elevation ?? popupMenuTheme.elevation, items: items, initialValue: widget.initialValue, position: position, + shape: widget.shape ?? popupMenuTheme.shape, + color: widget.color ?? popupMenuTheme.color, ) .then((T newValue) { if (!mounted) diff --git a/packages/flutter/lib/src/material/popup_menu_theme.dart b/packages/flutter/lib/src/material/popup_menu_theme.dart new file mode 100644 index 0000000000..ef24902bb7 --- /dev/null +++ b/packages/flutter/lib/src/material/popup_menu_theme.dart @@ -0,0 +1,161 @@ +// Copyright 2019 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 'dart:ui' show lerpDouble; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'theme.dart'; + +/// Defines the visual properties of the routes used to display popup menus +/// as well as [PopupMenuItem] and [PopupMenuDivider] widgets. +/// +/// Descendant widgets obtain the current [PopupMenuThemeData] object +/// using `PopupMenuTheme.of(context)`. Instances of +/// [PopupMenuThemeData] can be customized with +/// [PopupMenuThemeData.copyWith]. +/// +/// Typically, a [PopupMenuThemeData] is specified as part of the +/// overall [Theme] with [ThemeData.popupMenuTheme]. Otherwise, +/// [PopupMenuTheme] can be used to configure its own widget subtree. +/// +/// All [PopupMenuThemeData] properties are `null` by default. +/// If any of these properties are null, the popup menu will provide its +/// own defaults. +/// +/// See also: +/// +/// * [ThemeData], which describes the overall theme information for the +/// application. +class PopupMenuThemeData extends Diagnosticable { + /// Creates the set of properties used to configure [PopupMenuTheme]. + const PopupMenuThemeData({ + this.color, + this.shape, + this.elevation, + this.textStyle, + }); + + /// The background color of the popup menu. + final Color color; + + /// The shape of the popup menu. + final ShapeBorder shape; + + /// The elevation of the popup menu. + final double elevation; + + /// The text style of items in the popup menu. + final TextStyle textStyle; + + /// Creates a copy of this object with the given fields replaced with the + /// new values. + PopupMenuThemeData copyWith({ + Color color, + ShapeBorder shape, + double elevation, + TextStyle textStyle, + }) { + return PopupMenuThemeData( + color: color ?? this.color, + shape: shape ?? this.shape, + elevation: elevation ?? this.elevation, + textStyle: textStyle ?? this.textStyle, + ); + } + + /// Linearly interpolate between two popup menu themes. + /// + /// If both arguments are null, then null is returned. + /// + /// {@macro dart.ui.shadow.lerp} + static PopupMenuThemeData lerp(PopupMenuThemeData a, PopupMenuThemeData b, double t) { + assert(t != null); + if (a == null && b == null) + return null; + return PopupMenuThemeData( + color: Color.lerp(a?.color, b?.color, t), + shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + elevation: lerpDouble(a?.elevation, b?.elevation, t), + textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t), + ); + } + + @override + int get hashCode { + return hashValues( + color, + shape, + elevation, + textStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final PopupMenuThemeData typedOther = other; + return typedOther.elevation == elevation + && typedOther.color == color + && typedOther.shape == shape + && typedOther.textStyle == textStyle; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('color', color, defaultValue: null)); + properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); + properties.add(DiagnosticsProperty('text style', textStyle, defaultValue: null)); + } +} + +/// An inherited widget that defines the configuration for +/// popup menus in this widget's subtree. +/// +/// Values specified here are used for popup menu properties that are not +/// given an explicit non-null value. +class PopupMenuTheme extends InheritedWidget { + /// Creates a popup menu theme that controls the configurations for + /// popup menus in its widget subtree. + PopupMenuTheme({ + Key key, + Color color, + ShapeBorder shape, + double elevation, + TextStyle textStyle, + Widget child, + }) : data = PopupMenuThemeData( + color: color, + shape: shape, + elevation: elevation, + textStyle: textStyle, + ), + super(key: key, child: child); + + /// The properties for descendant popup menu widgets. + final PopupMenuThemeData data; + + /// The closest instance of this class's [data] value that encloses the given + /// context. If there is no ancestor, it returns [ThemeData.popupMenuTheme]. + /// Applications can assume that the returned value will not be null. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// PopupMenuThemeData theme = PopupMenuTheme.of(context); + /// ``` + static PopupMenuThemeData of(BuildContext context) { + final PopupMenuTheme popupMenuTheme = context.inheritFromWidgetOfExactType(PopupMenuTheme); + return popupMenuTheme?.data ?? Theme.of(context).popupMenuTheme; + } + + @override + bool updateShouldNotify(PopupMenuTheme oldWidget) => data != oldWidget.data; +} diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index c36b355877..959a7f2cc3 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -23,6 +23,7 @@ import 'ink_splash.dart'; import 'ink_well.dart' show InteractiveInkFeatureFactory; import 'input_decorator.dart'; import 'page_transitions_theme.dart'; +import 'popup_menu_theme.dart'; import 'slider_theme.dart'; import 'snack_bar_theme.dart'; import 'tab_bar_theme.dart'; @@ -174,6 +175,7 @@ class ThemeData extends Diagnosticable { CupertinoThemeData cupertinoOverrideTheme, SnackBarThemeData snackBarTheme, BottomSheetThemeData bottomSheetTheme, + PopupMenuThemeData popupMenuTheme, }) { brightness ??= Brightness.light; final bool isDark = brightness == Brightness.dark; @@ -276,6 +278,7 @@ class ThemeData extends Diagnosticable { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); snackBarTheme ??= const SnackBarThemeData(); bottomSheetTheme ??= const BottomSheetThemeData(); + popupMenuTheme ??= const PopupMenuThemeData(); return ThemeData.raw( brightness: brightness, @@ -336,6 +339,7 @@ class ThemeData extends Diagnosticable { cupertinoOverrideTheme: cupertinoOverrideTheme, snackBarTheme: snackBarTheme, bottomSheetTheme: bottomSheetTheme, + popupMenuTheme: popupMenuTheme, ); } @@ -408,6 +412,7 @@ class ThemeData extends Diagnosticable { @required this.cupertinoOverrideTheme, @required this.snackBarTheme, @required this.bottomSheetTheme, + @required this.popupMenuTheme, }) : assert(brightness != null), assert(primaryColor != null), assert(primaryColorBrightness != null), @@ -462,7 +467,8 @@ class ThemeData extends Diagnosticable { assert(floatingActionButtonTheme != null), assert(typography != null), assert(snackBarTheme != null), - assert(bottomSheetTheme != null); + assert(bottomSheetTheme != null), + assert(popupMenuTheme != null); // Warning: make sure these properties are in the exact same order as in // hashValues() and in the raw constructor and in the order of fields in @@ -788,6 +794,10 @@ class ThemeData extends Diagnosticable { /// A theme for customizing the color, elevation, and shape of a bottom sheet. final BottomSheetThemeData bottomSheetTheme; + /// A theme for customizing the color, shape, elevation, and text style of + /// popup menus. + final PopupMenuThemeData popupMenuTheme; + /// Creates a copy of this theme but with the given fields replaced with the new values. ThemeData copyWith({ Brightness brightness, @@ -848,6 +858,7 @@ class ThemeData extends Diagnosticable { CupertinoThemeData cupertinoOverrideTheme, SnackBarThemeData snackBarTheme, BottomSheetThemeData bottomSheetTheme, + PopupMenuThemeData popupMenuTheme, }) { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); return ThemeData.raw( @@ -909,6 +920,7 @@ class ThemeData extends Diagnosticable { cupertinoOverrideTheme: cupertinoOverrideTheme ?? this.cupertinoOverrideTheme, snackBarTheme: snackBarTheme ?? this.snackBarTheme, bottomSheetTheme: bottomSheetTheme ?? this.bottomSheetTheme, + popupMenuTheme: popupMenuTheme ?? this.popupMenuTheme, ); } @@ -1048,6 +1060,7 @@ class ThemeData extends Diagnosticable { cupertinoOverrideTheme: t < 0.5 ? a.cupertinoOverrideTheme : b.cupertinoOverrideTheme, snackBarTheme: SnackBarThemeData.lerp(a.snackBarTheme, b.snackBarTheme, t), bottomSheetTheme: BottomSheetThemeData.lerp(a.bottomSheetTheme, b.bottomSheetTheme, t), + popupMenuTheme: PopupMenuThemeData.lerp(a.popupMenuTheme, b.popupMenuTheme, t), ); } @@ -1114,7 +1127,8 @@ class ThemeData extends Diagnosticable { (otherData.typography == typography) && (otherData.cupertinoOverrideTheme == cupertinoOverrideTheme) && (otherData.snackBarTheme == snackBarTheme) && - (otherData.bottomSheetTheme == bottomSheetTheme); + (otherData.bottomSheetTheme == bottomSheetTheme) && + (otherData.popupMenuTheme == popupMenuTheme); } @override @@ -1181,6 +1195,7 @@ class ThemeData extends Diagnosticable { cupertinoOverrideTheme, snackBarTheme, bottomSheetTheme, + popupMenuTheme, ]; return hashList(values); } @@ -1244,6 +1259,7 @@ class ThemeData extends Diagnosticable { properties.add(DiagnosticsProperty('cupertinoOverrideTheme', cupertinoOverrideTheme, defaultValue: defaultData.cupertinoOverrideTheme)); properties.add(DiagnosticsProperty('snackBarTheme', snackBarTheme, defaultValue: defaultData.snackBarTheme)); properties.add(DiagnosticsProperty('bottomSheetTheme', bottomSheetTheme, defaultValue: defaultData.bottomSheetTheme)); + properties.add(DiagnosticsProperty('popupMenuTheme', popupMenuTheme, defaultValue: defaultData.popupMenuTheme)); } } diff --git a/packages/flutter/test/material/popup_menu_theme_test.dart b/packages/flutter/test/material/popup_menu_theme_test.dart new file mode 100644 index 0000000000..79fd151d7e --- /dev/null +++ b/packages/flutter/test/material/popup_menu_theme_test.dart @@ -0,0 +1,314 @@ +// Copyright 2019 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/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +PopupMenuThemeData _popupMenuTheme() { + return PopupMenuThemeData( + color: Colors.orange, + shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 12.0, + textStyle: const TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic), + ); +} + +void main() { + test('PopupMenuThemeData copyWith, ==, hashCode basics', () { + expect(const PopupMenuThemeData(), const PopupMenuThemeData().copyWith()); + expect(const PopupMenuThemeData().hashCode, const PopupMenuThemeData().copyWith().hashCode); + }); + + test('PopupMenuThemeData null fields by default', () { + const PopupMenuThemeData popupMenuTheme = PopupMenuThemeData(); + expect(popupMenuTheme.color, null); + expect(popupMenuTheme.shape, null); + expect(popupMenuTheme.elevation, null); + expect(popupMenuTheme.textStyle, null); + }); + + testWidgets('Default PopupMenuThemeData debugFillProperties', + (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const PopupMenuThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('PopupMenuThemeData implements debugFillProperties', + (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + PopupMenuThemeData( + color: const Color(0xFFFFFFFF), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2.0)), + elevation: 2.0, + textStyle: const TextStyle(color: Color(0xffffffff)), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'color: Color(0xffffffff)', + 'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(2.0))', + 'elevation: 2.0', + 'text style: TextStyle(inherit: true, color: Color(0xffffffff))' + ]); + }); + + testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key popupItemKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(), + key: popupButtonApp, + home: Material( + child: Column( + children: [ + PopupMenuButton( + key: popupButtonKey, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: popupItemKey, + child: const Text('Example'), + ), + ]; + }, + ), + ], + ), + ), + )); + + await tester.tap(find.byKey(popupButtonKey)); + await tester.pumpAndSettle(); + + /// The last Material widget under popupButtonApp is the [PopupMenuButton] + /// specified above, so by finding the last descendent of popupButtonApp + /// that is of type Material, this code retrieves the built + /// [PopupMenuButton]. + final Material button = tester.widget( + find.descendant( + of: find.byKey(popupButtonApp), + matching: find.byType(Material), + ).last, + ); + expect(button.color, null); + expect(button.shape, null); + expect(button.elevation, 8.0); + + /// The last DefaultTextStyle widget under popupItemKey is the + /// [PopupMenuItem] specified above, so by finding the last descendent of + /// popupItemKey that is of type DefaultTextStyle, this code retrieves the + /// built [PopupMenuItem]. + final DefaultTextStyle text = tester.widget( + find.descendant( + of: find.byKey(popupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(text.style.fontFamily, 'Roboto'); + expect(text.style.color, const Color(0xdd000000)); + }); + + testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { + final PopupMenuThemeData popupMenuTheme = _popupMenuTheme(); + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key popupItemKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(popupMenuTheme: popupMenuTheme), + key: popupButtonApp, + home: Material( + child: Column( + children: [ + PopupMenuButton( + key: popupButtonKey, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: popupItemKey, + child: const Text('Example'), + ), + ]; + }, + ), + ], + ), + ), + )); + + await tester.tap(find.byKey(popupButtonKey)); + await tester.pumpAndSettle(); + + /// The last Material widget under popupButtonApp is the [PopupMenuButton] + /// specified above, so by finding the last descendent of popupButtonApp + /// that is of type Material, this code retrieves the built + /// [PopupMenuButton]. + final Material button = tester.widget( + find.descendant( + of: find.byKey(popupButtonApp), + matching: find.byType(Material), + ).last, + ); + expect(button.color, popupMenuTheme.color); + expect(button.shape, popupMenuTheme.shape); + expect(button.elevation, popupMenuTheme.elevation); + + /// The last DefaultTextStyle widget under popupItemKey is the + /// [PopupMenuItem] specified above, so by finding the last descendent of + /// popupItemKey that is of type DefaultTextStyle, this code retrieves the + /// built [PopupMenuItem]. + final DefaultTextStyle text = tester.widget( + find.descendant( + of: find.byKey(popupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(text.style, popupMenuTheme.textStyle); + }); + + testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { + final PopupMenuThemeData popupMenuTheme = _popupMenuTheme(); + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key popupItemKey = UniqueKey(); + + const Color color = Colors.purple; + const ShapeBorder shape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(9.0)), + ); + const double elevation = 7.0; + const TextStyle textStyle = TextStyle(color: Color(0x00000000), textBaseline: TextBaseline.alphabetic); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(popupMenuTheme: popupMenuTheme), + key: popupButtonApp, + home: Material( + child: Column( + children: [ + PopupMenuButton( + key: popupButtonKey, + elevation: elevation, + color: color, + shape: shape, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: popupItemKey, + textStyle: textStyle, + child: const Text('Example'), + ), + ]; + }, + ), + ], + ), + ), + )); + + await tester.tap(find.byKey(popupButtonKey)); + await tester.pumpAndSettle(); + + /// The last Material widget under popupButtonApp is the [PopupMenuButton] + /// specified above, so by finding the last descendent of popupButtonApp + /// that is of type Material, this code retrieves the built + /// [PopupMenuButton]. + final Material button = tester.widget( + find.descendant( + of: find.byKey(popupButtonApp), + matching: find.byType(Material), + ).last, + ); + expect(button.color, color); + expect(button.shape, shape); + expect(button.elevation, elevation); + + /// The last DefaultTextStyle widget under popupItemKey is the + /// [PopupMenuItem] specified above, so by finding the last descendent of + /// popupItemKey that is of type DefaultTextStyle, this code retrieves the + /// built [PopupMenuItem]. + final DefaultTextStyle text = tester.widget( + find.descendant( + of: find.byKey(popupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(text.style, textStyle); + }); + + testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async { + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key popupItemKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + key: popupButtonApp, + home: Material( + child: Column( + children: [ + PopupMenuTheme( + color: Colors.pink, + shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(10)), + elevation: 6.0, + textStyle: const TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic), + child: PopupMenuButton( + key: popupButtonKey, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: popupItemKey, + child: const Text('Example'), + ), + ]; + }, + ), + ), + ], + ), + ), + )); + + await tester.tap(find.byKey(popupButtonKey)); + await tester.pumpAndSettle(); + + /// The last Material widget under popupButtonApp is the [PopupMenuButton] + /// specified above, so by finding the last descendent of popupButtonApp + /// that is of type Material, this code retrieves the built + /// [PopupMenuButton]. + final Material button = tester.widget( + find.descendant( + of: find.byKey(popupButtonApp), + matching: find.byType(Material), + ).last, + ); + expect(button.color, Colors.pink); + expect(button.shape, BeveledRectangleBorder(borderRadius: BorderRadius.circular(10))); + expect(button.elevation, 6.0); + + /// The last DefaultTextStyle widget under popupItemKey is the + /// [PopupMenuItem] specified above, so by finding the last descendent of + /// popupItemKey that is of type DefaultTextStyle, this code retrieves the + /// built [PopupMenuItem]. + final DefaultTextStyle text = tester.widget( + find.descendant( + of: find.byKey(popupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(text.style.color, const Color(0xfffff000)); + }); +}