Unverified Commit b4058b95 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Update Popup Menu to support Material 3 (#103606)

parent 700b449d
...@@ -33,6 +33,7 @@ import 'package:gen_defaults/input_chip_template.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/input_decorator_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart'; import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/navigation_rail_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/progress_indicator_template.dart';
import 'package:gen_defaults/radio_template.dart'; import 'package:gen_defaults/radio_template.dart';
import 'package:gen_defaults/surface_tint.dart'; import 'package:gen_defaults/surface_tint.dart';
...@@ -136,6 +137,7 @@ Future<void> main(List<String> args) async { ...@@ -136,6 +137,7 @@ Future<void> main(List<String> args) async {
InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile(); InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile();
NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile(); NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile();
NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.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(); ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile(); RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile(); SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
......
// 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<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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")};
}''';
}
...@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; ...@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'color_scheme.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'divider.dart'; import 'divider.dart';
...@@ -17,6 +18,7 @@ import 'material.dart'; ...@@ -17,6 +18,7 @@ import 'material.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart'; import 'material_state.dart';
import 'popup_menu_theme.dart'; import 'popup_menu_theme.dart';
import 'text_theme.dart';
import 'theme.dart'; import 'theme.dart';
import 'tooltip.dart'; import 'tooltip.dart';
...@@ -224,6 +226,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> { ...@@ -224,6 +226,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
this.height = kMinInteractiveDimension, this.height = kMinInteractiveDimension,
this.padding, this.padding,
this.textStyle, this.textStyle,
this.labelTextStyle,
this.mouseCursor, this.mouseCursor,
required this.child, required this.child,
}) : assert(enabled != null), }) : assert(enabled != null),
...@@ -263,6 +266,16 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> { ...@@ -263,6 +266,16 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// of [ThemeData.textTheme] is used. /// of [ThemeData.textTheme] is used.
final TextStyle? textStyle; 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<TextStyle?>? labelTextStyle;
/// {@template flutter.material.popupmenu.mouseCursor} /// {@template flutter.material.popupmenu.mouseCursor}
/// The cursor for a mouse pointer when it enters or is hovering over the /// The cursor for a mouse pointer when it enters or is hovering over the
/// widget. /// widget.
...@@ -336,9 +349,20 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> { ...@@ -336,9 +349,20 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.titleMedium!; final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
final Set<MaterialState> states = <MaterialState>{
if (!widget.enabled) { 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); style = style.copyWith(color: theme.disabledColor);
} }
...@@ -537,7 +561,9 @@ class _PopupMenu<T> extends StatelessWidget { ...@@ -537,7 +561,9 @@ class _PopupMenu<T> extends StatelessWidget {
Widget build(BuildContext context) { 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 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<Widget> children = <Widget>[]; final List<Widget> children = <Widget>[];
final ThemeData theme = Theme.of(context);
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.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) { for (int i = 0; i < route.items.length; i += 1) {
final double start = (i + 1) * unit; final double start = (i + 1) * unit;
...@@ -598,11 +624,13 @@ class _PopupMenu<T> extends StatelessWidget { ...@@ -598,11 +624,13 @@ class _PopupMenu<T> extends StatelessWidget {
return FadeTransition( return FadeTransition(
opacity: opacity.animate(route.animation!), opacity: opacity.animate(route.animation!),
child: Material( child: Material(
shape: route.shape ?? popupMenuTheme.shape, shape: route.shape ?? popupMenuTheme.shape ?? defaults.shape,
color: route.color ?? popupMenuTheme.color, color: route.color ?? popupMenuTheme.color ?? defaults.color,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
type: MaterialType.card, 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( child: Align(
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
widthFactor: width.evaluate(route.animation!), widthFactor: width.evaluate(route.animation!),
...@@ -757,6 +785,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -757,6 +785,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
required this.items, required this.items,
this.initialValue, this.initialValue,
this.elevation, this.elevation,
this.surfaceTintColor,
this.shadowColor,
required this.barrierLabel, required this.barrierLabel,
this.semanticLabel, this.semanticLabel,
this.shape, this.shape,
...@@ -771,6 +801,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -771,6 +801,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final List<Size?> itemSizes; final List<Size?> itemSizes;
final T? initialValue; final T? initialValue;
final double? elevation; final double? elevation;
final Color? surfaceTintColor;
final Color? shadowColor;
final String? semanticLabel; final String? semanticLabel;
final ShapeBorder? shape; final ShapeBorder? shape;
final Color? color; final Color? color;
...@@ -911,6 +943,8 @@ Future<T?> showMenu<T>({ ...@@ -911,6 +943,8 @@ Future<T?> showMenu<T>({
required List<PopupMenuEntry<T>> items, required List<PopupMenuEntry<T>> items,
T? initialValue, T? initialValue,
double? elevation, double? elevation,
Color? shadowColor,
Color? surfaceTintColor,
String? semanticLabel, String? semanticLabel,
ShapeBorder? shape, ShapeBorder? shape,
Color? color, Color? color,
...@@ -941,6 +975,8 @@ Future<T?> showMenu<T>({ ...@@ -941,6 +975,8 @@ Future<T?> showMenu<T>({
items: items, items: items,
initialValue: initialValue, initialValue: initialValue,
elevation: elevation, elevation: elevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
semanticLabel: semanticLabel, semanticLabel: semanticLabel,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
shape: shape, shape: shape,
...@@ -1006,6 +1042,8 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1006,6 +1042,8 @@ class PopupMenuButton<T> extends StatefulWidget {
this.onCanceled, this.onCanceled,
this.tooltip, this.tooltip,
this.elevation, this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.padding = const EdgeInsets.all(8.0), this.padding = const EdgeInsets.all(8.0),
this.child, this.child,
this.splashRadius, this.splashRadius,
...@@ -1058,6 +1096,22 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1058,6 +1096,22 @@ class PopupMenuButton<T> extends StatefulWidget {
/// Defaults to 8, the appropriate elevation for popup menus. /// Defaults to 8, the appropriate elevation for popup menus.
final double? elevation; 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 /// 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 /// this button appears as the trailing element of a list item, it's useful to be able
/// to set the padding to zero. /// to set the padding to zero.
...@@ -1207,6 +1261,8 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1207,6 +1261,8 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
showMenu<T?>( showMenu<T?>(
context: context, context: context,
elevation: widget.elevation ?? popupMenuTheme.elevation, elevation: widget.elevation ?? popupMenuTheme.elevation,
shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor,
surfaceTintColor: widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor,
items: items, items: items,
initialValue: widget.initialValue, initialValue: widget.initialValue,
position: position, position: position,
...@@ -1240,6 +1296,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1240,6 +1296,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final IconThemeData iconTheme = IconTheme.of(context);
final bool enableFeedback = widget.enableFeedback final bool enableFeedback = widget.enableFeedback
?? PopupMenuTheme.of(context).enableFeedback ?? PopupMenuTheme.of(context).enableFeedback
?? true; ?? true;
...@@ -1263,7 +1320,8 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1263,7 +1320,8 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
icon: widget.icon ?? Icon(Icons.adaptive.more), icon: widget.icon ?? Icon(Icons.adaptive.more),
padding: widget.padding, padding: widget.padding,
splashRadius: widget.splashRadius, splashRadius: widget.splashRadius,
iconSize: widget.iconSize, iconSize: widget.iconSize ?? iconTheme.size,
color: widget.color ?? iconTheme.color,
tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
onPressed: widget.enabled ? showButtonMenu : null, onPressed: widget.enabled ? showButtonMenu : null,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
...@@ -1290,3 +1348,57 @@ class _EffectiveMouseCursor extends MaterialStateMouseCursor { ...@@ -1290,3 +1348,57 @@ class _EffectiveMouseCursor extends MaterialStateMouseCursor {
@override @override
String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)'; 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<TextStyle?>? get labelTextStyle {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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
...@@ -48,7 +48,10 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -48,7 +48,10 @@ class PopupMenuThemeData with Diagnosticable {
this.color, this.color,
this.shape, this.shape,
this.elevation, this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.textStyle, this.textStyle,
this.labelTextStyle,
this.enableFeedback, this.enableFeedback,
this.mouseCursor, this.mouseCursor,
this.position, this.position,
...@@ -63,9 +66,19 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -63,9 +66,19 @@ class PopupMenuThemeData with Diagnosticable {
/// The elevation of the popup menu. /// The elevation of the popup menu.
final double? elevation; 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. /// The text style of items in the popup menu.
final TextStyle? textStyle; 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<TextStyle?>? labelTextStyle;
/// If specified, defines the feedback property for [PopupMenuButton]. /// If specified, defines the feedback property for [PopupMenuButton].
/// ///
/// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored. /// If [PopupMenuButton.enableFeedback] is provided, [enableFeedback] is ignored.
...@@ -88,7 +101,10 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -88,7 +101,10 @@ class PopupMenuThemeData with Diagnosticable {
Color? color, Color? color,
ShapeBorder? shape, ShapeBorder? shape,
double? elevation, double? elevation,
Color? shadowColor,
Color? surfaceTintColor,
TextStyle? textStyle, TextStyle? textStyle,
MaterialStateProperty<TextStyle?>? labelTextStyle,
bool? enableFeedback, bool? enableFeedback,
MaterialStateProperty<MouseCursor?>? mouseCursor, MaterialStateProperty<MouseCursor?>? mouseCursor,
PopupMenuPosition? position, PopupMenuPosition? position,
...@@ -97,7 +113,10 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -97,7 +113,10 @@ class PopupMenuThemeData with Diagnosticable {
color: color ?? this.color, color: color ?? this.color,
shape: shape ?? this.shape, shape: shape ?? this.shape,
elevation: elevation ?? this.elevation, elevation: elevation ?? this.elevation,
shadowColor: shadowColor ?? this.shadowColor,
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
textStyle: textStyle ?? this.textStyle, textStyle: textStyle ?? this.textStyle,
labelTextStyle: labelTextStyle ?? this.labelTextStyle,
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor, mouseCursor: mouseCursor ?? this.mouseCursor,
position: position ?? this.position, position: position ?? this.position,
...@@ -118,7 +137,10 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -118,7 +137,10 @@ class PopupMenuThemeData with Diagnosticable {
color: Color.lerp(a?.color, b?.color, t), color: Color.lerp(a?.color, b?.color, t),
shape: ShapeBorder.lerp(a?.shape, b?.shape, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
elevation: lerpDouble(a?.elevation, b?.elevation, 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), textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
labelTextStyle: MaterialStateProperty.lerp<TextStyle?>(a?.labelTextStyle, b?.labelTextStyle, t, TextStyle.lerp),
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
position: t < 0.5 ? a?.position : b?.position, position: t < 0.5 ? a?.position : b?.position,
...@@ -130,7 +152,10 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -130,7 +152,10 @@ class PopupMenuThemeData with Diagnosticable {
color, color,
shape, shape,
elevation, elevation,
shadowColor,
surfaceTintColor,
textStyle, textStyle,
labelTextStyle,
enableFeedback, enableFeedback,
mouseCursor, mouseCursor,
position, position,
...@@ -145,10 +170,13 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -145,10 +170,13 @@ class PopupMenuThemeData with Diagnosticable {
return false; return false;
} }
return other is PopupMenuThemeData return other is PopupMenuThemeData
&& other.elevation == elevation
&& other.color == color && other.color == color
&& other.shape == shape && other.shape == shape
&& other.elevation == elevation
&& other.shadowColor == shadowColor
&& other.surfaceTintColor == surfaceTintColor
&& other.textStyle == textStyle && other.textStyle == textStyle
&& other.labelTextStyle == labelTextStyle
&& other.enableFeedback == enableFeedback && other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor && other.mouseCursor == mouseCursor
&& other.position == position; && other.position == position;
...@@ -160,7 +188,10 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -160,7 +188,10 @@ class PopupMenuThemeData with Diagnosticable {
properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(ColorProperty('color', color, defaultValue: null));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, 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<TextStyle>('text style', textStyle, defaultValue: null)); properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<TextStyle?>>('labelTextStyle', labelTextStyle, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(EnumProperty<PopupMenuPosition?>('position', position, defaultValue: null)); properties.add(EnumProperty<PopupMenuPosition?>('position', position, defaultValue: null));
......
...@@ -7,13 +7,40 @@ import 'package:flutter/material.dart'; ...@@ -7,13 +7,40 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
PopupMenuThemeData _popupMenuTheme() { PopupMenuThemeData _popupMenuThemeM2() {
return const PopupMenuThemeData( return PopupMenuThemeData(
color: Colors.orange, color: Colors.orange,
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
elevation: 12.0, elevation: 12.0,
textStyle: TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic), textStyle: const TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic),
position: PopupMenuPosition.under, mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>((Set<MaterialState> 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<MaterialState> 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<MouseCursor?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return SystemMouseCursors.contextMenu;
}
return SystemMouseCursors.alias;
}),
); );
} }
...@@ -28,9 +55,12 @@ void main() { ...@@ -28,9 +55,12 @@ void main() {
expect(popupMenuTheme.color, null); expect(popupMenuTheme.color, null);
expect(popupMenuTheme.shape, null); expect(popupMenuTheme.shape, null);
expect(popupMenuTheme.elevation, null); expect(popupMenuTheme.elevation, null);
expect(popupMenuTheme.shadowColor, null);
expect(popupMenuTheme.surfaceTintColor, null);
expect(popupMenuTheme.textStyle, null); expect(popupMenuTheme.textStyle, null);
expect(popupMenuTheme.labelTextStyle, null);
expect(popupMenuTheme.enableFeedback, null);
expect(popupMenuTheme.mouseCursor, null); expect(popupMenuTheme.mouseCursor, null);
expect(popupMenuTheme.position, null);
}); });
testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -47,13 +77,20 @@ void main() { ...@@ -47,13 +77,20 @@ void main() {
testWidgets('PopupMenuThemeData implements debugFillProperties', (WidgetTester tester) async { testWidgets('PopupMenuThemeData implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const PopupMenuThemeData( PopupMenuThemeData(
color: Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
elevation: 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<MaterialState> 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, mouseCursor: MaterialStateMouseCursor.clickable,
position: PopupMenuPosition.over,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -65,19 +102,23 @@ void main() { ...@@ -65,19 +102,23 @@ void main() {
'color: Color(0xffffffff)', 'color: Color(0xffffffff)',
'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))',
'elevation: 2.0', 'elevation: 2.0',
'shadowColor: Color(0xff00ff00)',
'surfaceTintColor: Color(0xff00ff00)',
'text style: TextStyle(inherit: true, color: Color(0xffffffff))', 'text style: TextStyle(inherit: true, color: Color(0xffffffff))',
"labelTextStyle: Instance of '_MaterialStatePropertyWith<TextStyle?>'",
'mouseCursor: MaterialStateMouseCursor(clickable)', 'mouseCursor: MaterialStateMouseCursor(clickable)',
'position: over'
]); ]);
}); });
testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async { testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey(); final Key popupButtonKey = UniqueKey();
final Key popupButtonApp = 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( await tester.pumpWidget(MaterialApp(
theme: ThemeData(), theme: theme,
key: popupButtonApp, key: popupButtonApp,
home: Material( home: Material(
child: Column( child: Column(
...@@ -91,8 +132,14 @@ void main() { ...@@ -91,8 +132,14 @@ void main() {
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuEntry<void>>[ return <PopupMenuEntry<void>>[
PopupMenuItem<void>( PopupMenuItem<void>(
key: popupItemKey, key: enabledPopupItemKey,
child: const Text('Example'), child: const Text('Enabled PopupMenuItem'),
),
const PopupMenuDivider(),
PopupMenuItem<void>(
key: disabledPopupItemKey,
enabled: false,
child: const Text('Disabled PopupMenuItem'),
), ),
]; ];
}, },
...@@ -116,37 +163,63 @@ void main() { ...@@ -116,37 +163,63 @@ void main() {
matching: find.byType(Material), matching: find.byType(Material),
).last, ).last,
); );
expect(button.color, null); expect(button.color, theme.colorScheme.surface);
expect(button.shape, null); expect(button.shadowColor, theme.colorScheme.shadow);
expect(button.elevation, 8.0); 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 /// The last DefaultTextStyle widget under popupItemKey is the
/// [PopupMenuItem] specified above, so by finding the last descendent of /// [PopupMenuItem] specified above, so by finding the last descendent of
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the /// popupItemKey that is of type DefaultTextStyle, this code retrieves the
/// built [PopupMenuItem]. /// built [PopupMenuItem].
final DefaultTextStyle text = tester.widget<DefaultTextStyle>( final DefaultTextStyle enabledText = tester.widget<DefaultTextStyle>(
find.descendant( 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<DefaultTextStyle>(
find.descendant(
of: find.byKey(disabledPopupItemKey),
matching: find.byType(DefaultTextStyle), matching: find.byType(DefaultTextStyle),
).last, ).last,
); );
expect(text.style.fontFamily, 'Roboto'); expect(disabledText.style.color, theme.colorScheme.onSurface.withOpacity(0.38));
expect(text.style.color, const Color(0xdd000000));
expect(text.style.color, const Color(0xdd000000));
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>)); final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>));
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
expect(topLeftMenu, topLeftButton); 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 { testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async {
final PopupMenuThemeData popupMenuTheme = _popupMenuTheme(); final PopupMenuThemeData popupMenuTheme = _popupMenuThemeM3();
final Key popupButtonKey = UniqueKey(); final Key popupButtonKey = UniqueKey();
final Key popupButtonApp = UniqueKey(); final Key popupButtonApp = UniqueKey();
final Key popupItemKey = UniqueKey(); final Key enabledPopupItemKey = UniqueKey();
final Key disabledPopupItemKey = UniqueKey();
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: ThemeData(popupMenuTheme: popupMenuTheme), theme: ThemeData(useMaterial3: true, popupMenuTheme: popupMenuTheme),
key: popupButtonApp, key: popupButtonApp,
home: Material( home: Material(
child: Column( child: Column(
...@@ -160,8 +233,15 @@ void main() { ...@@ -160,8 +233,15 @@ void main() {
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuEntry<Object>>[ return <PopupMenuEntry<Object>>[
PopupMenuItem<Object>( PopupMenuItem<Object>(
key: popupItemKey, key: disabledPopupItemKey,
child: const Text('Example'), enabled: false,
child: const Text('disabled'),
),
const PopupMenuDivider(),
PopupMenuItem<Object>(
key: enabledPopupItemKey,
onTap: () { },
child: const Text('enabled'),
), ),
]; ];
}, },
...@@ -184,66 +264,90 @@ void main() { ...@@ -184,66 +264,90 @@ void main() {
matching: find.byType(Material), matching: find.byType(Material),
).last, ).last,
); );
expect(button.color, popupMenuTheme.color); expect(button.color, Colors.orange);
expect(button.shape, popupMenuTheme.shape); expect(button.surfaceTintColor, const Color(0xff00ff00));
expect(button.elevation, popupMenuTheme.elevation); 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 final DefaultTextStyle enabledText = tester.widget<DefaultTextStyle>(
/// [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<DefaultTextStyle>(
find.descendant( 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<DefaultTextStyle>(
find.descendant(
of: find.byKey(disabledPopupItemKey),
matching: find.byType(DefaultTextStyle), matching: find.byType(DefaultTextStyle),
).last, ).last,
); );
expect(text.style, popupMenuTheme.textStyle); expect(
disabledText.style,
popupMenuTheme.labelTextStyle?.resolve(disabled),
);
final Offset bottomLeftButton = tester.getBottomLeft(find.byType(PopupMenuButton<void>)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); await gesture.addPointer();
expect(topLeftMenu, bottomLeftButton); 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 { 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 popupButtonKey = UniqueKey();
final Key popupButtonApp = UniqueKey(); final Key popupButtonApp = UniqueKey();
final Key popupItemKey = UniqueKey(); final Key popupItemKey = UniqueKey();
const Color color = Colors.purple; const Color color = Colors.purple;
const Color surfaceTintColor = Colors.amber;
const Color shadowColor = Colors.green;
const ShapeBorder shape = RoundedRectangleBorder( const ShapeBorder shape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(9.0)), borderRadius: BorderRadius.all(Radius.circular(9.0)),
); );
const double elevation = 7.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( await tester.pumpWidget(MaterialApp(
theme: ThemeData(popupMenuTheme: popupMenuTheme), theme: ThemeData(useMaterial3: true, popupMenuTheme: popupMenuTheme),
key: popupButtonApp, key: popupButtonApp,
home: Material( home: Material(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
Padding( PopupMenuButton<void>(
// The padding makes sure the menu has enough space around it to key: popupButtonKey,
// get properly aligned when displayed (`_kMenuScreenPadding`). elevation: elevation,
padding: const EdgeInsets.all(8.0), shadowColor: shadowColor,
child: PopupMenuButton<void>( surfaceTintColor: surfaceTintColor,
key: popupButtonKey, color: color,
elevation: elevation, shape: shape,
color: color, itemBuilder: (BuildContext context) {
shape: shape, return <PopupMenuEntry<void>>[
position: PopupMenuPosition.over, PopupMenuItem<void>(
itemBuilder: (BuildContext context) { key: popupItemKey,
return <PopupMenuEntry<void>>[ labelTextStyle: MaterialStateProperty.all<TextStyle>(textStyle),
PopupMenuItem<void>( mouseCursor: cursor,
key: popupItemKey, child: const Text('Example'),
textStyle: textStyle, ),
child: const Text('Example'), ];
), },
];
},
),
), ),
], ],
), ),
...@@ -266,6 +370,8 @@ void main() { ...@@ -266,6 +370,8 @@ void main() {
expect(button.color, color); expect(button.color, color);
expect(button.shape, shape); expect(button.shape, shape);
expect(button.elevation, elevation); expect(button.elevation, elevation);
expect(button.shadowColor, shadowColor);
expect(button.surfaceTintColor, surfaceTintColor);
/// The last DefaultTextStyle widget under popupItemKey is the /// The last DefaultTextStyle widget under popupItemKey is the
/// [PopupMenuItem] specified above, so by finding the last descendent of /// [PopupMenuItem] specified above, so by finding the last descendent of
...@@ -279,45 +385,145 @@ void main() { ...@@ -279,45 +385,145 @@ void main() {
); );
expect(text.style, textStyle); expect(text.style, textStyle);
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button)); await gesture.addPointer();
expect(topLeftMenu, topLeftButton); 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 { group('Material 2', () {
final Key popupButtonKey = UniqueKey(); // Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
final Key popupButtonApp = UniqueKey(); // is turned on by default, these tests can be removed.
final Key enabledPopupItemKey = UniqueKey();
final Key disabledPopupItemKey = UniqueKey(); testWidgets('Passing no PopupMenuThemeData returns defaults', (WidgetTester tester) async {
final Key popupButtonKey = UniqueKey();
await tester.pumpWidget(MaterialApp( final Key popupButtonApp = UniqueKey();
key: popupButtonApp, final Key enabledPopupItemKey = UniqueKey();
home: Material( final Key disabledPopupItemKey = UniqueKey();
child: Column( final ThemeData theme = ThemeData();
children: <Widget>[
PopupMenuTheme( await tester.pumpWidget(MaterialApp(
data: PopupMenuThemeData( theme: theme,
color: Colors.pink, key: popupButtonApp,
shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), home: Material(
elevation: 6.0, child: Column(
textStyle: const TextStyle(color: Color(0xfffff000), textBaseline: TextBaseline.alphabetic), children: <Widget>[
mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>((Set<MaterialState> states) { Padding(
if (states.contains(MaterialState.disabled)) { // The padding makes sure the menu has enough space around it to
return SystemMouseCursors.contextMenu; // get properly aligned when displayed (`_kMenuScreenPadding`).
} padding: const EdgeInsets.all(8.0),
return SystemMouseCursors.alias; child: PopupMenuButton<void>(
}), key: popupButtonKey,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<void>>[
PopupMenuItem<void>(
key: enabledPopupItemKey,
child: const Text('Enabled PopupMenuItem'),
),
const PopupMenuDivider(),
PopupMenuItem<void>(
key: disabledPopupItemKey,
enabled: false,
child: const Text('Disabled PopupMenuItem'),
),
];
},
),
), ),
child: PopupMenuButton<void>( ],
),
),
));
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<Material>(
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<DefaultTextStyle>(
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<DefaultTextStyle>(
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<void>));
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: <Widget>[
PopupMenuButton<void>(
// 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, key: popupButtonKey,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuEntry<void>>[ return <PopupMenuEntry<Object>>[
PopupMenuItem<void>( PopupMenuItem<Object>(
key: disabledPopupItemKey, key: disabledPopupItemKey,
enabled: false, enabled: false,
child: const Text('disabled'), child: const Text('disabled'),
), ),
PopupMenuItem<void>( const PopupMenuDivider(),
PopupMenuItem<Object>(
key: enabledPopupItemKey, key: enabledPopupItemKey,
onTap: () { }, onTap: () { },
child: const Text('enabled'), child: const Text('enabled'),
...@@ -325,44 +531,142 @@ void main() { ...@@ -325,44 +531,142 @@ void main() {
]; ];
}, },
), ),
), ],
], ),
), ),
), ));
));
await tester.tap(find.byKey(popupButtonKey));
await tester.tap(find.byKey(popupButtonKey)); await tester.pumpAndSettle();
await tester.pumpAndSettle();
/// The last Material widget under popupButtonApp is the [PopupMenuButton]
/// The last Material widget under popupButtonApp is the [PopupMenuButton] /// specified above, so by finding the last descendent of popupButtonApp
/// specified above, so by finding the last descendent of popupButtonApp /// that is of type Material, this code retrieves the built
/// that is of type Material, this code retrieves the built /// [PopupMenuButton].
/// [PopupMenuButton]. final Material button = tester.widget<Material>(
final Material button = tester.widget<Material>( find.descendant(
find.descendant( of: find.byKey(popupButtonApp),
of: find.byKey(popupButtonApp), matching: find.byType(Material),
matching: find.byType(Material), ).last,
).last, );
); expect(button.color, popupMenuTheme.color);
expect(button.color, Colors.pink); expect(button.shape, popupMenuTheme.shape);
expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10)))); expect(button.elevation, popupMenuTheme.elevation);
expect(button.elevation, 6.0);
/// The last DefaultTextStyle widget under popupItemKey is the
final DefaultTextStyle text = tester.widget<DefaultTextStyle>( /// [PopupMenuItem] specified above, so by finding the last descendent of
find.descendant( /// popupItemKey that is of type DefaultTextStyle, this code retrieves the
of: find.byKey(enabledPopupItemKey), /// built [PopupMenuItem].
matching: find.byType(DefaultTextStyle), final DefaultTextStyle text = tester.widget<DefaultTextStyle>(
), find.descendant(
); of: find.byKey(enabledPopupItemKey),
expect(text.style.color, const Color(0xfffff000)); matching: find.byType(DefaultTextStyle),
).last,
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); );
await gesture.addPointer(); expect(text.style, popupMenuTheme.textStyle);
await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey)));
await tester.pumpAndSettle(); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.contextMenu); await gesture.addPointer();
await gesture.down(tester.getCenter(find.byKey(enabledPopupItemKey))); addTearDown(gesture.removePointer);
await tester.pumpAndSettle(); await gesture.moveTo(tester.getCenter(find.byKey(disabledPopupItemKey)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.alias); 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: <Widget>[
PopupMenuButton<void>(
key: popupButtonKey,
elevation: elevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
color: color,
shape: shape,
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<void>>[
PopupMenuItem<void>(
key: popupItemKey,
labelTextStyle: MaterialStateProperty.all<TextStyle>(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<Material>(
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<DefaultTextStyle>(
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<MaterialState> enabled = <MaterialState>{};
Set<MaterialState> disabled = <MaterialState>{MaterialState.disabled};
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