Skip to content

Commit b4058b9

Browse files
authored
Update Popup Menu to support Material 3 (#103606)
1 parent 700b449 commit b4058b9

File tree

5 files changed

+636
-141
lines changed

5 files changed

+636
-141
lines changed

dev/tools/gen_defaults/bin/gen_defaults.dart

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import 'package:gen_defaults/input_chip_template.dart';
3333
import 'package:gen_defaults/input_decorator_template.dart';
3434
import 'package:gen_defaults/navigation_bar_template.dart';
3535
import 'package:gen_defaults/navigation_rail_template.dart';
36+
import 'package:gen_defaults/popup_menu_template.dart';
3637
import 'package:gen_defaults/progress_indicator_template.dart';
3738
import 'package:gen_defaults/radio_template.dart';
3839
import 'package:gen_defaults/surface_tint.dart';
@@ -136,6 +137,7 @@ Future<void> main(List<String> args) async {
136137
InputDecoratorTemplate('InputDecorator', '$materialLib/input_decorator.dart', tokens).updateFile();
137138
NavigationBarTemplate('NavigationBar', '$materialLib/navigation_bar.dart', tokens).updateFile();
138139
NavigationRailTemplate('NavigationRail', '$materialLib/navigation_rail.dart', tokens).updateFile();
140+
PopupMenuTemplate('PopupMenu', '$materialLib/popup_menu.dart', tokens).updateFile();
139141
ProgressIndicatorTemplate('ProgressIndicator', '$materialLib/progress_indicator.dart', tokens).updateFile();
140142
RadioTemplate('Radio<T>', '$materialLib/radio.dart', tokens).updateFile();
141143
SurfaceTintTemplate('SurfaceTint', '$materialLib/elevation_overlay.dart', tokens).updateFile();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'template.dart';
6+
7+
class PopupMenuTemplate extends TokenTemplate {
8+
const PopupMenuTemplate(super.blockName, super.fileName, super.tokens, {
9+
super.colorSchemePrefix = '_colors.',
10+
super.textThemePrefix = '_textTheme.',
11+
});
12+
13+
@override
14+
String generate() => '''
15+
class _${blockName}DefaultsM3 extends PopupMenuThemeData {
16+
_${blockName}DefaultsM3(this.context)
17+
: super(elevation: ${elevation('md.comp.menu.container')});
18+
19+
final BuildContext context;
20+
late final ThemeData _theme = Theme.of(context);
21+
late final ColorScheme _colors = _theme.colorScheme;
22+
late final TextTheme _textTheme = _theme.textTheme;
23+
24+
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
25+
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
26+
final TextStyle style = _textTheme.labelLarge!;
27+
if (states.contains(MaterialState.disabled)) {
28+
return style.apply(color: ${componentColor('md.comp.menu.list-item.disabled.label-text')});
29+
}
30+
return style.apply(color: ${componentColor('md.comp.menu.list-item.label-text')});
31+
});
32+
}
33+
34+
@override
35+
Color? get color => ${componentColor('md.comp.menu.container')};
36+
37+
@override
38+
Color? get shadowColor => ${color("md.comp.menu.container.shadow-color")};
39+
40+
@override
41+
Color? get surfaceTintColor => ${color("md.comp.menu.container.surface-tint-layer.color")};
42+
43+
@override
44+
ShapeBorder? get shape => ${shape("md.comp.menu.container")};
45+
}''';
46+
}

packages/flutter/lib/src/material/popup_menu.dart

+119-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
66
import 'package:flutter/rendering.dart';
77
import 'package:flutter/widgets.dart';
88

9+
import 'color_scheme.dart';
910
import 'constants.dart';
1011
import 'debug.dart';
1112
import 'divider.dart';
@@ -17,6 +18,7 @@ import 'material.dart';
1718
import 'material_localizations.dart';
1819
import 'material_state.dart';
1920
import 'popup_menu_theme.dart';
21+
import 'text_theme.dart';
2022
import 'theme.dart';
2123
import 'tooltip.dart';
2224

@@ -224,6 +226,7 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
224226
this.height = kMinInteractiveDimension,
225227
this.padding,
226228
this.textStyle,
229+
this.labelTextStyle,
227230
this.mouseCursor,
228231
required this.child,
229232
}) : assert(enabled != null),
@@ -263,6 +266,16 @@ class PopupMenuItem<T> extends PopupMenuEntry<T> {
263266
/// of [ThemeData.textTheme] is used.
264267
final TextStyle? textStyle;
265268

269+
/// The label style of the popup menu item.
270+
///
271+
/// When [ThemeData.useMaterial3] is true, this styles the text of the popup menu item.
272+
///
273+
/// If this property is null, then [PopupMenuThemeData.labelTextStyle] is used.
274+
/// If [PopupMenuThemeData.labelTextStyle] is also null, then [TextTheme.labelLarge]
275+
/// is used with the [ColorScheme.onSurface] color when popup menu item is enabled and
276+
/// the [ColorScheme.onSurface] color with 0.38 opacity when the popup menu item is disabled.
277+
final MaterialStateProperty<TextStyle?>? labelTextStyle;
278+
266279
/// {@template flutter.material.popupmenu.mouseCursor}
267280
/// The cursor for a mouse pointer when it enters or is hovering over the
268281
/// widget.
@@ -336,9 +349,20 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
336349
Widget build(BuildContext context) {
337350
final ThemeData theme = Theme.of(context);
338351
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
339-
TextStyle style = widget.textStyle ?? popupMenuTheme.textStyle ?? theme.textTheme.titleMedium!;
340-
341-
if (!widget.enabled) {
352+
final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
353+
final Set<MaterialState> states = <MaterialState>{
354+
if (!widget.enabled) MaterialState.disabled,
355+
};
356+
357+
TextStyle style = theme.useMaterial3
358+
? (widget.labelTextStyle?.resolve(states)
359+
?? popupMenuTheme.labelTextStyle?.resolve(states)!
360+
?? defaults.labelTextStyle!.resolve(states)!)
361+
: (widget.textStyle
362+
?? popupMenuTheme.textStyle
363+
?? defaults.textStyle!);
364+
365+
if (!widget.enabled && !theme.useMaterial3) {
342366
style = style.copyWith(color: theme.disabledColor);
343367
}
344368

@@ -537,7 +561,9 @@ class _PopupMenu<T> extends StatelessWidget {
537561
Widget build(BuildContext context) {
538562
final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
539563
final List<Widget> children = <Widget>[];
564+
final ThemeData theme = Theme.of(context);
540565
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
566+
final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
541567

542568
for (int i = 0; i < route.items.length; i += 1) {
543569
final double start = (i + 1) * unit;
@@ -598,11 +624,13 @@ class _PopupMenu<T> extends StatelessWidget {
598624
return FadeTransition(
599625
opacity: opacity.animate(route.animation!),
600626
child: Material(
601-
shape: route.shape ?? popupMenuTheme.shape,
602-
color: route.color ?? popupMenuTheme.color,
627+
shape: route.shape ?? popupMenuTheme.shape ?? defaults.shape,
628+
color: route.color ?? popupMenuTheme.color ?? defaults.color,
603629
clipBehavior: clipBehavior,
604630
type: MaterialType.card,
605-
elevation: route.elevation ?? popupMenuTheme.elevation ?? 8.0,
631+
elevation: route.elevation ?? popupMenuTheme.elevation ?? defaults.elevation!,
632+
shadowColor: route.shadowColor ?? popupMenuTheme.shadowColor ?? defaults.shadowColor,
633+
surfaceTintColor: route.surfaceTintColor ?? popupMenuTheme.surfaceTintColor ?? defaults.surfaceTintColor,
606634
child: Align(
607635
alignment: AlignmentDirectional.topEnd,
608636
widthFactor: width.evaluate(route.animation!),
@@ -757,6 +785,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
757785
required this.items,
758786
this.initialValue,
759787
this.elevation,
788+
this.surfaceTintColor,
789+
this.shadowColor,
760790
required this.barrierLabel,
761791
this.semanticLabel,
762792
this.shape,
@@ -771,6 +801,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
771801
final List<Size?> itemSizes;
772802
final T? initialValue;
773803
final double? elevation;
804+
final Color? surfaceTintColor;
805+
final Color? shadowColor;
774806
final String? semanticLabel;
775807
final ShapeBorder? shape;
776808
final Color? color;
@@ -911,6 +943,8 @@ Future<T?> showMenu<T>({
911943
required List<PopupMenuEntry<T>> items,
912944
T? initialValue,
913945
double? elevation,
946+
Color? shadowColor,
947+
Color? surfaceTintColor,
914948
String? semanticLabel,
915949
ShapeBorder? shape,
916950
Color? color,
@@ -941,6 +975,8 @@ Future<T?> showMenu<T>({
941975
items: items,
942976
initialValue: initialValue,
943977
elevation: elevation,
978+
shadowColor: shadowColor,
979+
surfaceTintColor: surfaceTintColor,
944980
semanticLabel: semanticLabel,
945981
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
946982
shape: shape,
@@ -1006,6 +1042,8 @@ class PopupMenuButton<T> extends StatefulWidget {
10061042
this.onCanceled,
10071043
this.tooltip,
10081044
this.elevation,
1045+
this.shadowColor,
1046+
this.surfaceTintColor,
10091047
this.padding = const EdgeInsets.all(8.0),
10101048
this.child,
10111049
this.splashRadius,
@@ -1058,6 +1096,22 @@ class PopupMenuButton<T> extends StatefulWidget {
10581096
/// Defaults to 8, the appropriate elevation for popup menus.
10591097
final double? elevation;
10601098

1099+
/// The color used to paint the shadow below the menu.
1100+
///
1101+
/// If null then the ambient [PopupMenuThemeData.shadowColor] is used.
1102+
/// If that is null too, then the overall theme's [ThemeData.shadowColor]
1103+
/// (default black) is used.
1104+
final Color? shadowColor;
1105+
1106+
/// The color used as an overlay on [color] to indicate elevation.
1107+
///
1108+
/// If null, [PopupMenuThemeData.surfaceTintColor] is used. If that
1109+
/// is also null, the default value is [ColorScheme.surfaceTint].
1110+
///
1111+
/// See [Material.surfaceTintColor] for more details on how this
1112+
/// overlay is applied.
1113+
final Color? surfaceTintColor;
1114+
10611115
/// Matches IconButton's 8 dps padding by default. In some cases, notably where
10621116
/// this button appears as the trailing element of a list item, it's useful to be able
10631117
/// to set the padding to zero.
@@ -1207,6 +1261,8 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
12071261
showMenu<T?>(
12081262
context: context,
12091263
elevation: widget.elevation ?? popupMenuTheme.elevation,
1264+
shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor,
1265+
surfaceTintColor: widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor,
12101266
items: items,
12111267
initialValue: widget.initialValue,
12121268
position: position,
@@ -1240,6 +1296,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
12401296

12411297
@override
12421298
Widget build(BuildContext context) {
1299+
final IconThemeData iconTheme = IconTheme.of(context);
12431300
final bool enableFeedback = widget.enableFeedback
12441301
?? PopupMenuTheme.of(context).enableFeedback
12451302
?? true;
@@ -1263,7 +1320,8 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
12631320
icon: widget.icon ?? Icon(Icons.adaptive.more),
12641321
padding: widget.padding,
12651322
splashRadius: widget.splashRadius,
1266-
iconSize: widget.iconSize,
1323+
iconSize: widget.iconSize ?? iconTheme.size,
1324+
color: widget.color ?? iconTheme.color,
12671325
tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
12681326
onPressed: widget.enabled ? showButtonMenu : null,
12691327
enableFeedback: enableFeedback,
@@ -1290,3 +1348,57 @@ class _EffectiveMouseCursor extends MaterialStateMouseCursor {
12901348
@override
12911349
String get debugDescription => 'MaterialStateMouseCursor(PopupMenuItemState)';
12921350
}
1351+
1352+
class _PopupMenuDefaultsM2 extends PopupMenuThemeData {
1353+
_PopupMenuDefaultsM2(this.context)
1354+
: super(elevation: 8.0);
1355+
1356+
final BuildContext context;
1357+
late final ThemeData _theme = Theme.of(context);
1358+
late final TextTheme _textTheme = _theme.textTheme;
1359+
1360+
@override
1361+
TextStyle? get textStyle => _textTheme.subtitle1;
1362+
}
1363+
1364+
// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu
1365+
1366+
// Do not edit by hand. The code between the "BEGIN GENERATED" and
1367+
// "END GENERATED" comments are generated from data in the Material
1368+
// Design token database by the script:
1369+
// dev/tools/gen_defaults/bin/gen_defaults.dart.
1370+
1371+
// Token database version: v0_132
1372+
1373+
class _PopupMenuDefaultsM3 extends PopupMenuThemeData {
1374+
_PopupMenuDefaultsM3(this.context)
1375+
: super(elevation: 3.0);
1376+
1377+
final BuildContext context;
1378+
late final ThemeData _theme = Theme.of(context);
1379+
late final ColorScheme _colors = _theme.colorScheme;
1380+
late final TextTheme _textTheme = _theme.textTheme;
1381+
1382+
@override MaterialStateProperty<TextStyle?>? get labelTextStyle {
1383+
return MaterialStateProperty.resolveWith((Set<MaterialState> states) {
1384+
final TextStyle style = _textTheme.labelLarge!;
1385+
if (states.contains(MaterialState.disabled)) {
1386+
return style.apply(color: _colors.onSurface.withOpacity(0.38));
1387+
}
1388+
return style.apply(color: _colors.onSurface);
1389+
});
1390+
}
1391+
1392+
@override
1393+
Color? get color => _colors.surface;
1394+
1395+
@override
1396+
Color? get shadowColor => _colors.shadow;
1397+
1398+
@override
1399+
Color? get surfaceTintColor => _colors.surfaceTint;
1400+
1401+
@override
1402+
ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
1403+
}
1404+
// END GENERATED TOKEN PROPERTIES - PopupMenu

0 commit comments

Comments
 (0)