diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index ed9e100889ed3..167b95a8d4025 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -33,6 +33,7 @@ import 'package:gen_defaults/input_chip_template.dart'; import 'package:gen_defaults/input_decorator_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_rail_template.dart'; +import 'package:gen_defaults/popup_menu_template.dart'; import 'package:gen_defaults/progress_indicator_template.dart'; import 'package:gen_defaults/radio_template.dart'; import 'package:gen_defaults/surface_tint.dart'; @@ -136,6 +137,7 @@ Future main(List args) async { InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile(); NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile(); + PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile(); ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile(); RadioTemplate('Radio', '$materialLib/radio.dart', tokens).updateFile(); SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/popup_menu_template.dart b/dev/tools/gen_defaults/lib/popup_menu_template.dart new file mode 100644 index 0000000000000..136e00ff373c9 --- /dev/null +++ b/dev/tools/gen_defaults/lib/popup_menu_template.dart @@ -0,0 +1,46 @@ +// Copyright 2014 The Flutter 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 'template.dart'; + +class PopupMenuTemplate extends TokenTemplate { + const PopupMenuTemplate(super.blockName, super.fileName, super.tokens, { + super.colorSchemePrefix = '_colors.', + super.textThemePrefix = '_textTheme.', + }); + + @override + String generate() => ''' +class _${blockName}DefaultsM3 extends PopupMenuThemeData { + _${blockName}DefaultsM3(this.context) + : super(elevation: ${elevation('md.comp.menu.container')}); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + late final TextTheme _textTheme = _theme.textTheme; + + @override MaterialStateProperty? get labelTextStyle { + return MaterialStateProperty.resolveWith((Set states) { + final TextStyle style = _textTheme.labelLarge!; + if (states.contains(MaterialState.disabled)) { + return style.apply(color: ${componentColor('md.comp.menu.list-item.disabled.label-text')}); + } + return style.apply(color: ${componentColor('md.comp.menu.list-item.label-text')}); + }); + } + + @override + Color? get color => ${componentColor('md.comp.menu.container')}; + + @override + Color? get shadowColor => ${color("md.comp.menu.container.shadow-color")}; + + @override + Color? get surfaceTintColor => ${color("md.comp.menu.container.surface-tint-layer.color")}; + + @override + ShapeBorder? get shape => ${shape("md.comp.menu.container")}; +}'''; +} diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 1518652b4d67b..35bf531df478c 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'color_scheme.dart'; import 'constants.dart'; import 'debug.dart'; import 'divider.dart'; @@ -17,6 +18,7 @@ import 'material.dart'; import 'material_localizations.dart'; import 'material_state.dart'; import 'popup_menu_theme.dart'; +import 'text_theme.dart'; import 'theme.dart'; import 'tooltip.dart'; @@ -224,6 +226,7 @@ class PopupMenuItem extends PopupMenuEntry { this.height = kMinInteractiveDimension, this.padding, this.textStyle, + this.labelTextStyle, this.mouseCursor, required this.child, }) : assert(enabled != null), @@ -263,6 +266,16 @@ class PopupMenuItem extends PopupMenuEntry { /// of [ThemeData.textTheme] is used. final TextStyle? textStyle; + /// The label style of the popup menu item. + /// + /// When [ThemeData.useMaterial3] is true, this styles the text of the popup menu item. + /// + /// If this property is null, then [PopupMenuThemeData.labelTextStyle] is used. + /// If [PopupMenuThemeData.labelTextStyle] is also null, then [TextTheme.labelLarge] + /// is used with the [ColorScheme.onSurface] color when popup menu item is enabled and + /// the [ColorScheme.onSurface] color with 0.38 opacity when the popup menu item is disabled. + final MaterialStateProperty? labelTextStyle; + /// {@template flutter.material.popupmenu.mouseCursor} /// The cursor for a mouse pointer when it enters or is hovering over the /// widget. @@ -336,9 +349,20 @@ class PopupMenuItemState> extends State { Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); - TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.titleMedium!; - - if (!widget.enabled) { + final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context); + final Set states = { + if (!widget.enabled) MaterialState.disabled, + }; + + TextStyle style = theme.useMaterial3 + ? (widget.labelTextStyle?.resolve(states) + ?? popupMenuTheme.labelTextStyle?.resolve(states)! + ?? defaults.labelTextStyle!.resolve(states)!) + : (widget.textStyle + ?? popupMenuTheme.textStyle + ?? defaults.textStyle!); + + if (!widget.enabled && !theme.useMaterial3) { style = style.copyWith(color: theme.disabledColor); } @@ -537,7 +561,9 @@ 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 ThemeData theme = Theme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context); for (int i = 0; i < route.items.length; i += 1) { final double start = (i + 1) * unit; @@ -598,11 +624,13 @@ class _PopupMenu extends StatelessWidget { return FadeTransition( opacity: opacity.animate(route.animation!), child: Material( - shape: route.shape ?? popupMenuTheme.shape, - color: route.color ?? popupMenuTheme.color, + shape: route.shape ?? popupMenuTheme.shape ?? defaults.shape, + color: route.color ?? popupMenuTheme.color ?? defaults.color, clipBehavior: clipBehavior, type: MaterialType.card, - elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0, + elevation: route.elevation ?? popupMenuTheme.elevation ?? defaults.elevation!, + shadowColor: route.shadowColor ?? popupMenuTheme.shadowColor ?? defaults.shadowColor, + surfaceTintColor: route.surfaceTintColor ?? popupMenuTheme.surfaceTintColor ?? defaults.surfaceTintColor, child: Align( alignment: AlignmentDirectional.topEnd, widthFactor: width.evaluate(route.animation!), @@ -757,6 +785,8 @@ class _PopupMenuRoute extends PopupRoute { required this.items, this.initialValue, this.elevation, + this.surfaceTintColor, + this.shadowColor, required this.barrierLabel, this.semanticLabel, this.shape, @@ -771,6 +801,8 @@ class _PopupMenuRoute extends PopupRoute { final List itemSizes; final T? initialValue; final double? elevation; + final Color? surfaceTintColor; + final Color? shadowColor; final String? semanticLabel; final ShapeBorder? shape; final Color? color; @@ -911,6 +943,8 @@ Future showMenu({ required List> items, T? initialValue, double? elevation, + Color? shadowColor, + Color? surfaceTintColor, String? semanticLabel, ShapeBorder? shape, Color? color, @@ -941,6 +975,8 @@ Future showMenu({ items: items, initialValue: initialValue, elevation: elevation, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, semanticLabel: semanticLabel, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, shape: shape, @@ -1006,6 +1042,8 @@ class PopupMenuButton extends StatefulWidget { this.onCanceled, this.tooltip, this.elevation, + this.shadowColor, + this.surfaceTintColor, this.padding = const EdgeInsets.all(8.0), this.child, this.splashRadius, @@ -1058,6 +1096,22 @@ class PopupMenuButton extends StatefulWidget { /// Defaults to 8, the appropriate elevation for popup menus. final double? elevation; + /// The color used to paint the shadow below the menu. + /// + /// If null then the ambient [PopupMenuThemeData.shadowColor] is used. + /// If that is null too, then the overall theme's [ThemeData.shadowColor] + /// (default black) is used. + final Color? shadowColor; + + /// The color used as an overlay on [color] to indicate elevation. + /// + /// If null, [PopupMenuThemeData.surfaceTintColor] is used. If that + /// is also null, the default value is [ColorScheme.surfaceTint]. + /// + /// See [Material.surfaceTintColor] for more details on how this + /// overlay is applied. + final Color? surfaceTintColor; + /// Matches IconButton's 8 dps padding by default. In some cases, notably where /// this button appears as the trailing element of a list item, it's useful to be able /// to set the padding to zero. @@ -1207,6 +1261,8 @@ class PopupMenuButtonState extends State> { showMenu( context: context, elevation: widget.elevation ?? popupMenuTheme.elevation, + shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor, + surfaceTintColor: widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor, items: items, initialValue: widget.initialValue, position: position, @@ -1240,6 +1296,7 @@ class PopupMenuButtonState extends State> { @override Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); final bool enableFeedback = widget.enableFeedback ?? PopupMenuTheme.of(context).enableFeedback ?? true; @@ -1263,7 +1320,8 @@ class PopupMenuButtonState extends State> { icon: widget.icon ?? Icon(Icons.adaptive.more), padding: widget.padding, splashRadius: widget.splashRadius, - iconSize: widget.iconSize, + iconSize: widget.iconSize ?? iconTheme.size, + color: widget.color ?? iconTheme.color, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, enableFeedback: enableFeedback, @@ -1290,3 +1348,57 @@ class _EffectiveMouseCursor extends MaterialStateMouseCursor { @override String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)'; } + +class _PopupMenuDefaultsM2 extends PopupMenuThemeData { + _PopupMenuDefaultsM2(this.context) + : super(elevation: 8.0); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final TextTheme _textTheme = _theme.textTheme; + + @override + TextStyle? get textStyle => _textTheme.subtitle1; +} + +// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_132 + +class _PopupMenuDefaultsM3 extends PopupMenuThemeData { + _PopupMenuDefaultsM3(this.context) + : super(elevation: 3.0); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + late final TextTheme _textTheme = _theme.textTheme; + + @override MaterialStateProperty? get labelTextStyle { + return MaterialStateProperty.resolveWith((Set states) { + final TextStyle style = _textTheme.labelLarge!; + if (states.contains(MaterialState.disabled)) { + return style.apply(color: _colors.onSurface.withOpacity(0.38)); + } + return style.apply(color: _colors.onSurface); + }); + } + + @override + Color? get color => _colors.surface; + + @override + Color? get shadowColor => _colors.shadow; + + @override + Color? get surfaceTintColor => _colors.surfaceTint; + + @override + ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); +} +// END GENERATED TOKEN PROPERTIES - PopupMenu diff --git a/packages/flutter/lib/src/material/popup_menu_theme.dart b/packages/flutter/lib/src/material/popup_menu_theme.dart index 3ace02516c4ef..a232afded3572 100644 --- a/packages/flutter/lib/src/material/popup_menu_theme.dart +++ b/packages/flutter/lib/src/material/popup_menu_theme.dart @@ -48,7 +48,10 @@ class PopupMenuThemeData with Diagnosticable { this.color, this.shape, this.elevation, + this.shadowColor, + this.surfaceTintColor, this.textStyle, + this.labelTextStyle, this.enableFeedback, this.mouseCursor, this.position, @@ -63,9 +66,19 @@ class PopupMenuThemeData with Diagnosticable { /// The elevation of the popup menu. final double? elevation; + /// The color used to paint shadow below the popup menu. + final Color? shadowColor; + + /// The color used as an overlay on [color] of the popup menu. + final Color? surfaceTintColor; + /// The text style of items in the popup menu. final TextStyle? textStyle; + /// You can use this to specify a different style of the label + /// when the popup menu item is enabled and disabled. + final MaterialStateProperty? labelTextStyle; + /// If specified, defines the feedback property for [PopupMenuButton]. /// /// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored. @@ -88,7 +101,10 @@ class PopupMenuThemeData with Diagnosticable { Color? color, ShapeBorder? shape, double? elevation, + Color? shadowColor, + Color? surfaceTintColor, TextStyle? textStyle, + MaterialStateProperty? labelTextStyle, bool? enableFeedback, MaterialStateProperty? mouseCursor, PopupMenuPosition? position, @@ -97,7 +113,10 @@ class PopupMenuThemeData with Diagnosticable { color: color ?? this.color, shape: shape ?? this.shape, elevation: elevation ?? this.elevation, + shadowColor: shadowColor ?? this.shadowColor, + surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, textStyle: textStyle ?? this.textStyle, + labelTextStyle: labelTextStyle ?? this.labelTextStyle, enableFeedback: enableFeedback ?? this.enableFeedback, mouseCursor: mouseCursor ?? this.mouseCursor, position: position ?? this.position, @@ -118,7 +137,10 @@ class PopupMenuThemeData with Diagnosticable { color: Color.lerp(a?.color, b?.color, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t), elevation: lerpDouble(a?.elevation, b?.elevation, t), + shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), + surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t), textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t), + labelTextStyle: MaterialStateProperty.lerp(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp), enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, position: t < 0.5 ? a?.position : b?.position, @@ -130,7 +152,10 @@ class PopupMenuThemeData with Diagnosticable { color, shape, elevation, + shadowColor, + surfaceTintColor, textStyle, + labelTextStyle, enableFeedback, mouseCursor, position, @@ -145,10 +170,13 @@ class PopupMenuThemeData with Diagnosticable { return false; } return other is PopupMenuThemeData - && other.elevation == elevation && other.color == color && other.shape == shape + && other.elevation == elevation + && other.shadowColor == shadowColor + && other.surfaceTintColor == surfaceTintColor && other.textStyle == textStyle + && other.labelTextStyle == labelTextStyle && other.enableFeedback == enableFeedback && other.mouseCursor == mouseCursor && other.position == position; @@ -160,7 +188,10 @@ class PopupMenuThemeData with Diagnosticable { properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); + properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null)); + properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); properties.add(DiagnosticsProperty('text style', textStyle, defaultValue: null)); + properties.add(DiagnosticsProperty>('labelTextStyle', labelTextStyle, defaultValue: null)); properties.add(DiagnosticsProperty('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(EnumProperty('position', position, defaultValue: null)); diff --git a/packages/flutter/test/material/popup_menu_theme_test.dart b/packages/flutter/test/material/popup_menu_theme_test.dart index bbddd55053265..f8f64e8adc2a5 100644 --- a/packages/flutter/test/material/popup_menu_theme_test.dart +++ b/packages/flutter/test/material/popup_menu_theme_test.dart @@ -7,13 +7,40 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -PopupMenuThemeData _popupMenuTheme() { - return const PopupMenuThemeData( +PopupMenuThemeData _popupMenuThemeM2() { + return PopupMenuThemeData( color: Colors.orange, - shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), elevation: 12.0, - textStyle: TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic), - position: PopupMenuPosition.under, + textStyle: const TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic), + mouseCursor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return SystemMouseCursors.contextMenu; + } + return SystemMouseCursors.alias; + }), + ); +} + +PopupMenuThemeData _popupMenuThemeM3() { + return PopupMenuThemeData( + color: Colors.orange, + shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + elevation: 12.0, + shadowColor: const Color(0xff00ff00), + surfaceTintColor: const Color(0xff00ff00), + labelTextStyle: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return const TextStyle(color: Color(0xfff99ff0), fontSize: 12.0); + } + return const TextStyle(color: Color(0xfff12099), fontSize: 17.0); + }), + mouseCursor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return SystemMouseCursors.contextMenu; + } + return SystemMouseCursors.alias; + }), ); } @@ -28,9 +55,12 @@ void main() { expect(popupMenuTheme.color, null); expect(popupMenuTheme.shape, null); expect(popupMenuTheme.elevation, null); + expect(popupMenuTheme.shadowColor, null); + expect(popupMenuTheme.surfaceTintColor, null); expect(popupMenuTheme.textStyle, null); + expect(popupMenuTheme.labelTextStyle, null); + expect(popupMenuTheme.enableFeedback, null); expect(popupMenuTheme.mouseCursor, null); - expect(popupMenuTheme.position, null); }); testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async { @@ -47,13 +77,20 @@ void main() { testWidgets('PopupMenuThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); - const PopupMenuThemeData( - color: Color(0xFFFFFFFF), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), + PopupMenuThemeData( + color: const Color(0xFFFFFFFF), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), elevation: 2.0, - textStyle: TextStyle(color: Color(0xffffffff)), + shadowColor: const Color(0xff00ff00), + surfaceTintColor: const Color(0xff00ff00), + textStyle: const TextStyle(color: Color(0xffffffff)), + labelTextStyle: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return const TextStyle(color: Color(0xfff99ff0), fontSize: 12.0); + } + return const TextStyle(color: Color(0xfff12099), fontSize: 17.0); + }), mouseCursor: MaterialStateMouseCursor.clickable, - position: PopupMenuPosition.over, ).debugFillProperties(builder); final List description = builder.properties @@ -65,19 +102,23 @@ void main() { 'color: Color(0xffffffff)', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', 'elevation: 2.0', + 'shadowColor: Color(0xff00ff00)', + 'surfaceTintColor: Color(0xff00ff00)', 'text style: TextStyle(inherit: true, color: Color(0xffffffff))', + "labelTextStyle: Instance of '_MaterialStatePropertyWith'", 'mouseCursor: MaterialStateMouseCursor(clickable)', - 'position: over' ]); }); testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); - final Key popupItemKey = UniqueKey(); + final Key enabledPopupItemKey = UniqueKey(); + final Key disabledPopupItemKey = UniqueKey(); + final ThemeData theme = ThemeData(useMaterial3: true); await tester.pumpWidget(MaterialApp( - theme: ThemeData(), + theme: theme, key: popupButtonApp, home: Material( child: Column( @@ -91,8 +132,14 @@ void main() { itemBuilder: (BuildContext context) { return >[ PopupMenuItem( - key: popupItemKey, - child: const Text('Example'), + key: enabledPopupItemKey, + child: const Text('Enabled PopupMenuItem'), + ), + const PopupMenuDivider(), + PopupMenuItem( + key: disabledPopupItemKey, + enabled: false, + child: const Text('Disabled PopupMenuItem'), ), ]; }, @@ -116,37 +163,63 @@ void main() { matching: find.byType(Material), ).last, ); - expect(button.color, null); - expect(button.shape, null); - expect(button.elevation, 8.0); + expect(button.color, theme.colorScheme.surface); + expect(button.shadowColor, theme.colorScheme.shadow); + expect(button.surfaceTintColor, theme.colorScheme.surfaceTint); + expect(button.shape, RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0))); + expect(button.elevation, 3.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( + final DefaultTextStyle enabledText = tester.widget( find.descendant( - of: find.byKey(popupItemKey), + of: find.byKey(enabledPopupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(enabledText.style.fontFamily, 'Roboto'); + expect(enabledText.style.color, const Color(0xff000000)); + /// Test disabled text color + final DefaultTextStyle disabledText = tester.widget( + find.descendant( + of: find.byKey(disabledPopupItemKey), matching: find.byType(DefaultTextStyle), ).last, ); - expect(text.style.fontFamily, 'Roboto'); - expect(text.style.color, const Color(0xdd000000)); - expect(text.style.color, const Color(0xdd000000)); + expect(disabledText.style.color, theme.colorScheme.onSurface.withOpacity(0.38)); final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton)); final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); expect(topLeftMenu, topLeftButton); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.click, + ); }); testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { - final PopupMenuThemeData popupMenuTheme = _popupMenuTheme(); + final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM3(); final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); - final Key popupItemKey = UniqueKey(); + final Key enabledPopupItemKey = UniqueKey(); + final Key disabledPopupItemKey = UniqueKey(); await tester.pumpWidget(MaterialApp( - theme: ThemeData(popupMenuTheme: popupMenuTheme), + theme: ThemeData(useMaterial3: true, popupMenuTheme: popupMenuTheme), key: popupButtonApp, home: Material( child: Column( @@ -160,8 +233,15 @@ void main() { itemBuilder: (BuildContext context) { return >[ PopupMenuItem( - key: popupItemKey, - child: const Text('Example'), + key: disabledPopupItemKey, + enabled: false, + child: const Text('disabled'), + ), + const PopupMenuDivider(), + PopupMenuItem( + key: enabledPopupItemKey, + onTap: () { }, + child: const Text('enabled'), ), ]; }, @@ -184,66 +264,90 @@ void main() { matching: find.byType(Material), ).last, ); - expect(button.color, popupMenuTheme.color); - expect(button.shape, popupMenuTheme.shape); - expect(button.elevation, popupMenuTheme.elevation); + expect(button.color, Colors.orange); + expect(button.surfaceTintColor, const Color(0xff00ff00)); + expect(button.shadowColor, const Color(0xff00ff00)); + expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12)))); + expect(button.elevation, 12.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( + final DefaultTextStyle enabledText = tester.widget( find.descendant( - of: find.byKey(popupItemKey), + of: find.byKey(enabledPopupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect( + enabledText.style, + popupMenuTheme.labelTextStyle?.resolve(enabled), + ); + /// Test disabled text color + final DefaultTextStyle disabledText = tester.widget( + find.descendant( + of: find.byKey(disabledPopupItemKey), matching: find.byType(DefaultTextStyle), ).last, ); - expect(text.style, popupMenuTheme.textStyle); + expect( + disabledText.style, + popupMenuTheme.labelTextStyle?.resolve(disabled), + ); - final Offset bottomLeftButton = tester.getBottomLeft(find.byType(PopupMenuButton)); - final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); - expect(topLeftMenu, bottomLeftButton); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + popupMenuTheme.mouseCursor?.resolve(disabled), + ); + await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + popupMenuTheme.mouseCursor?.resolve(enabled), + ); }); testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { - final PopupMenuThemeData popupMenuTheme = _popupMenuTheme(); + final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM3(); final Key popupButtonKey = UniqueKey(); final Key popupButtonApp = UniqueKey(); final Key popupItemKey = UniqueKey(); const Color color = Colors.purple; + const Color surfaceTintColor = Colors.amber; + const Color shadowColor = Colors.green; 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); + const TextStyle textStyle = TextStyle(color: Color(0xffffffef), fontSize: 19.0); + const MouseCursor cursor = SystemMouseCursors.forbidden; await tester.pumpWidget(MaterialApp( - theme: ThemeData(popupMenuTheme: popupMenuTheme), + theme: ThemeData(useMaterial3: true, popupMenuTheme: popupMenuTheme), key: popupButtonApp, home: Material( child: Column( children: [ - Padding( - // The padding makes sure the menu has enough space around it to - // get properly aligned when displayed (`_kMenuScreenPadding`). - padding: const EdgeInsets.all(8.0), - child: PopupMenuButton( - key: popupButtonKey, - elevation: elevation, - color: color, - shape: shape, - position: PopupMenuPosition.over, - itemBuilder: (BuildContext context) { - return >[ - PopupMenuItem( - key: popupItemKey, - textStyle: textStyle, - child: const Text('Example'), - ), - ]; - }, - ), + PopupMenuButton( + key: popupButtonKey, + elevation: elevation, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + color: color, + shape: shape, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: popupItemKey, + labelTextStyle: MaterialStateProperty.all(textStyle), + mouseCursor: cursor, + child: const Text('Example'), + ), + ]; + }, ), ], ), @@ -266,6 +370,8 @@ void main() { expect(button.color, color); expect(button.shape, shape); expect(button.elevation, elevation); + expect(button.shadowColor, shadowColor); + expect(button.surfaceTintColor, surfaceTintColor); /// The last DefaultTextStyle widget under popupItemKey is the /// [PopupMenuItem] specified above, so by finding the last descendent of @@ -279,45 +385,145 @@ void main() { ); expect(text.style, textStyle); - final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton)); - final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); - expect(topLeftMenu, topLeftButton); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byKey(popupItemKey))); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), cursor); }); - testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async { - final Key popupButtonKey = UniqueKey(); - final Key popupButtonApp = UniqueKey(); - final Key enabledPopupItemKey = UniqueKey(); - final Key disabledPopupItemKey = UniqueKey(); - - await tester.pumpWidget(MaterialApp( - key: popupButtonApp, - home: Material( - child: Column( - children: [ - PopupMenuTheme( - data: PopupMenuThemeData( - color: Colors.pink, - shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 6.0, - textStyle: const TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic), - mouseCursor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { - return SystemMouseCursors.contextMenu; - } - return SystemMouseCursors.alias; - }), + group('Material 2', () { + // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3 + // is turned on by default, these tests can be removed. + + testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key enabledPopupItemKey = UniqueKey(); + final Key disabledPopupItemKey = UniqueKey(); + final ThemeData theme = ThemeData(); + + await tester.pumpWidget(MaterialApp( + theme: theme, + key: popupButtonApp, + home: Material( + child: Column( + children: [ + Padding( + // The padding makes sure the menu has enough space around it to + // get properly aligned when displayed (`_kMenuScreenPadding`). + padding: const EdgeInsets.all(8.0), + child: PopupMenuButton( + key: popupButtonKey, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: enabledPopupItemKey, + child: const Text('Enabled PopupMenuItem'), + ), + const PopupMenuDivider(), + PopupMenuItem( + key: disabledPopupItemKey, + enabled: false, + child: const Text('Disabled PopupMenuItem'), + ), + ]; + }, + ), ), - child: PopupMenuButton( + ], + ), + ), + )); + + 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 enabledText = tester.widget( + find.descendant( + of: find.byKey(enabledPopupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(enabledText.style.fontFamily, 'Roboto'); + expect(enabledText.style.color, const Color(0xdd000000)); + /// Test disabled text color + final DefaultTextStyle disabledText = tester.widget( + find.descendant( + of: find.byKey(disabledPopupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(disabledText.style.color, theme.disabledColor); + + final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton)); + final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); + expect(topLeftMenu, topLeftButton); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.click, + ); + }); + + testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { + final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM2(); + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key enabledPopupItemKey = UniqueKey(); + final Key disabledPopupItemKey = UniqueKey(); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(popupMenuTheme: popupMenuTheme), + key: popupButtonApp, + home: Material( + child: Column( + children: [ + PopupMenuButton( + // The padding is used in the positioning of the menu when the + // position is `PopupMenuPosition.under`. Setting it to zero makes + // it easier to test. + padding: EdgeInsets.zero, key: popupButtonKey, itemBuilder: (BuildContext context) { - return >[ - PopupMenuItem( + return >[ + PopupMenuItem( key: disabledPopupItemKey, enabled: false, child: const Text('disabled'), ), - PopupMenuItem( + const PopupMenuDivider(), + PopupMenuItem( key: enabledPopupItemKey, onTap: () { }, child: const Text('enabled'), @@ -325,44 +531,142 @@ void main() { ]; }, ), - ), - ], + ], + ), ), - ), - )); - - 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, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10)))); - expect(button.elevation, 6.0); - - final DefaultTextStyle text = tester.widget( - find.descendant( - of: find.byKey(enabledPopupItemKey), - matching: find.byType(DefaultTextStyle), - ), - ); - expect(text.style.color, const Color(0xfffff000)); - - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(); - await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey))); - await tester.pumpAndSettle(); - expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.contextMenu); - await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey))); - await tester.pumpAndSettle(); - expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.alias); + )); + + 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(enabledPopupItemKey), + matching: find.byType(DefaultTextStyle), + ).last, + ); + expect(text.style, popupMenuTheme.textStyle); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + popupMenuTheme.mouseCursor?.resolve(disabled), + ); + await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey))); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + popupMenuTheme.mouseCursor?.resolve(enabled), + ); + }); + + testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { + final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM2(); + final Key popupButtonKey = UniqueKey(); + final Key popupButtonApp = UniqueKey(); + final Key popupItemKey = UniqueKey(); + + const Color color = Colors.purple; + const Color surfaceTintColor = Colors.amber; + const Color shadowColor = Colors.green; + const ShapeBorder shape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(9.0)), + ); + const double elevation = 7.0; + const TextStyle textStyle = TextStyle(color: Color(0xffffffef), fontSize: 19.0); + const MouseCursor cursor = SystemMouseCursors.forbidden; + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true, popupMenuTheme: popupMenuTheme), + key: popupButtonApp, + home: Material( + child: Column( + children: [ + PopupMenuButton( + key: popupButtonKey, + elevation: elevation, + shadowColor: shadowColor, + surfaceTintColor: surfaceTintColor, + color: color, + shape: shape, + itemBuilder: (BuildContext context) { + return >[ + PopupMenuItem( + key: popupItemKey, + labelTextStyle: MaterialStateProperty.all(textStyle), + mouseCursor: cursor, + 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); + expect(button.shadowColor, shadowColor); + expect(button.surfaceTintColor, surfaceTintColor); + + /// 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); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byKey(popupItemKey))); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), cursor); + }); }); } + +Set enabled = {}; +Set disabled = {MaterialState.disabled};