Skip to content

Commit 038ec62

Browse files
authored
Add CheckedPopupMenuItem‎.labelTextStyle and update default text style for Material 3 (#131060)
fixes [Update `CheckedPopupMenuItem�` for Material 3](flutter/flutter#128576) ### Description - This adds the missing ``CheckedPopupMenuItem�.labelTextStyle` parameter - Fixes default text style for `CheckedPopupMenuItem�`. It used `ListTile`'s `bodyLarge` instead of `LabelLarge` similar to `PopupMenuItem`. ### Code sample <details> <summary>expand to view the code sample</summary> ```dart import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @OverRide Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, textTheme: const TextTheme( labelLarge: TextStyle( fontWeight: FontWeight.bold, fontStyle: FontStyle.italic, letterSpacing: 5.0, ), ), ), home: const Example(), ); } } class Example extends StatelessWidget { const Example({super.key}); @OverRide Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Sample'), actions: <Widget>[ PopupMenuButton<String>( icon: const Icon(Icons.more_vert), itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[ const CheckedPopupMenuItem<String>( // labelTextStyle: MaterialStateProperty.resolveWith( // (Set<MaterialState> states) { // if (states.contains(MaterialState.selected)) { // return const TextStyle( // color: Colors.red, // fontStyle: FontStyle.italic, // fontWeight: FontWeight.bold, // ); // } // return const TextStyle( // color: Colors.amber, // fontStyle: FontStyle.italic, // fontWeight: FontWeight.bold, // ); // }), child: Text('Mild'), ), const CheckedPopupMenuItem<String>( checked: true, // labelTextStyle: MaterialStateProperty.resolveWith( // (Set<MaterialState> states) { // if (states.contains(MaterialState.selected)) { // return const TextStyle( // color: Colors.red, // fontStyle: FontStyle.italic, // fontWeight: FontWeight.bold, // ); // } // return const TextStyle( // color: Colors.amber, // fontStyle: FontStyle.italic, // fontWeight: FontWeight.bold, // ); // }), child: Text('Spicy'), ), const PopupMenuDivider(), const PopupMenuItem<String>( value: 'Close', child: Text('Close'), ), ], ) ], ), ); } } ``` </details> ### Customized `textTheme.labelLarge` text theme. | Before | After | | --------------- | --------------- | | <img src="https://github.com/flutter/flutter/assets/48603081/2672438d-b2da-479b-a5d3-d239ef646365" /> | <img src="https://github.com/flutter/flutter/assets/48603081/b9f83719-dede-4c2f-8247-18f74e63eb29" /> | ### New `CheckedPopupMenuItem�.labelTextStyle` parameter with material states support <img src="https://github.com/flutter/flutter/assets/48603081/ef0a88aa-9811-42b1-a3aa-53b90c8d43fb" height="450" />
1 parent 79033ed commit 038ec62

3 files changed

Lines changed: 178 additions & 9 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
475475
super.enabled,
476476
super.padding,
477477
super.height,
478+
super.labelTextStyle,
478479
super.mouseCursor,
479480
super.child,
480481
});
@@ -529,9 +530,19 @@ class _CheckedPopupMenuItemState<T> extends PopupMenuItemState<T, CheckedPopupMe
529530

530531
@override
531532
Widget buildChild() {
533+
final ThemeData theme = Theme.of(context);
534+
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
535+
final PopupMenuThemeData defaults = theme.useMaterial3 ? _PopupMenuDefaultsM3(context) : _PopupMenuDefaultsM2(context);
536+
final Set<MaterialState> states = <MaterialState>{
537+
if (widget.checked) MaterialState.selected,
538+
};
539+
final MaterialStateProperty<TextStyle?>? effectiveLabelTextStyle = widget.labelTextStyle
540+
?? popupMenuTheme.labelTextStyle
541+
?? defaults.labelTextStyle;
532542
return IgnorePointer(
533543
child: ListTile(
534544
enabled: widget.enabled,
545+
titleTextStyle: effectiveLabelTextStyle?.resolve(states),
535546
leading: FadeTransition(
536547
opacity: _opacity,
537548
child: Icon(_controller.isDismissed ? null : Icons.done),

packages/flutter/test/material/popup_menu_test.dart

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3307,6 +3307,117 @@ void main() {
33073307
final Finder modalBottomSheet = find.text('ModalBottomSheet');
33083308
expect(modalBottomSheet, findsOneWidget);
33093309
});
3310+
3311+
testWidgets('Material3 - CheckedPopupMenuItem.labelTextStyle uses correct text style', (WidgetTester tester) async {
3312+
final Key popupMenuButtonKey = UniqueKey();
3313+
ThemeData theme = ThemeData(useMaterial3: true);
3314+
3315+
Widget buildMenu() {
3316+
return MaterialApp(
3317+
theme: theme,
3318+
home: Scaffold(
3319+
appBar: AppBar(
3320+
actions: <Widget>[
3321+
PopupMenuButton<void>(
3322+
key: popupMenuButtonKey,
3323+
itemBuilder: (BuildContext context) => <PopupMenuItem<void>>[
3324+
const CheckedPopupMenuItem<void>(
3325+
child: Text('Item 1'),
3326+
),
3327+
const CheckedPopupMenuItem<int>(
3328+
checked: true,
3329+
child: Text('Item 2'),
3330+
),
3331+
],
3332+
),
3333+
],
3334+
),
3335+
),
3336+
);
3337+
}
3338+
3339+
await tester.pumpWidget(buildMenu());
3340+
3341+
// Show the menu
3342+
await tester.tap(find.byKey(popupMenuButtonKey));
3343+
await tester.pumpAndSettle();
3344+
3345+
// Test default text style.
3346+
expect(_labelStyle(tester, 'Item 1')!.fontSize, 14.0);
3347+
expect(_labelStyle(tester, 'Item 1')!.color, theme.colorScheme.onSurface);
3348+
3349+
// Close the menu.
3350+
await tester.tapAt(const Offset(20.0, 20.0));
3351+
await tester.pumpAndSettle();
3352+
3353+
// Test custom text theme text style.
3354+
theme = theme.copyWith(
3355+
textTheme: theme.textTheme.copyWith(
3356+
labelLarge: const TextStyle(
3357+
fontSize: 20.0,
3358+
fontWeight: FontWeight.bold,
3359+
)
3360+
),
3361+
);
3362+
await tester.pumpWidget(buildMenu());
3363+
3364+
// Show the menu.
3365+
await tester.tap(find.byKey(popupMenuButtonKey));
3366+
await tester.pumpAndSettle();
3367+
3368+
expect(_labelStyle(tester, 'Item 1')!.fontSize, 20.0);
3369+
expect(_labelStyle(tester, 'Item 1')!.fontWeight, FontWeight.bold);
3370+
});
3371+
3372+
testWidgets('CheckedPopupMenuItem.labelTextStyle resolve material states', (WidgetTester tester) async {
3373+
final Key popupMenuButtonKey = UniqueKey();
3374+
final MaterialStateProperty<TextStyle?> labelTextStyle = MaterialStateProperty.resolveWith(
3375+
(Set<MaterialState> states) {
3376+
if (states.contains(MaterialState.selected)) {
3377+
return const TextStyle(color: Colors.red, fontSize: 24.0);
3378+
}
3379+
3380+
return const TextStyle(color: Colors.amber, fontSize: 20.0);
3381+
});
3382+
3383+
await tester.pumpWidget(
3384+
MaterialApp(
3385+
home: Scaffold(
3386+
appBar: AppBar(
3387+
actions: <Widget>[
3388+
PopupMenuButton<void>(
3389+
key: popupMenuButtonKey,
3390+
itemBuilder: (BuildContext context) => <PopupMenuItem<void>>[
3391+
CheckedPopupMenuItem<void>(
3392+
labelTextStyle: labelTextStyle,
3393+
child: const Text('Item 1'),
3394+
),
3395+
CheckedPopupMenuItem<int>(
3396+
checked: true,
3397+
labelTextStyle: labelTextStyle,
3398+
child: const Text('Item 2'),
3399+
),
3400+
],
3401+
),
3402+
],
3403+
),
3404+
),
3405+
),
3406+
);
3407+
3408+
// Show the menu.
3409+
await tester.tap(find.byKey(popupMenuButtonKey));
3410+
await tester.pumpAndSettle();
3411+
3412+
expect(
3413+
_labelStyle(tester, 'Item 1'),
3414+
labelTextStyle.resolve(<MaterialState>{})
3415+
);
3416+
expect(
3417+
_labelStyle(tester, 'Item 2'),
3418+
labelTextStyle.resolve(<MaterialState>{MaterialState.selected})
3419+
);
3420+
});
33103421
}
33113422

33123423
class TestApp extends StatelessWidget {
@@ -3377,3 +3488,10 @@ class _ClosureNavigatorObserver extends NavigatorObserver {
33773488
@override
33783489
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => onDidChange(newRoute!);
33793490
}
3491+
3492+
TextStyle? _labelStyle(WidgetTester tester, String label) {
3493+
return tester.widget<RichText>(find.descendant(
3494+
of: find.text(label),
3495+
matching: find.byType(RichText),
3496+
)).text.style;
3497+
}

packages/flutter/test/material/popup_menu_theme_test.dart

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ void main() {
149149
enabled: false,
150150
child: const Text('Disabled PopupMenuItem'),
151151
),
152+
const CheckedPopupMenuItem<void>(
153+
child: Text('Unchecked item'),
154+
),
155+
const CheckedPopupMenuItem<void>(
156+
checked: true,
157+
child: Text('Checked item'),
158+
),
152159
];
153160
},
154161
),
@@ -181,22 +188,23 @@ void main() {
181188
/// [PopupMenuItem] specified above, so by finding the last descendent of
182189
/// popupItemKey that is of type DefaultTextStyle, this code retrieves the
183190
/// built [PopupMenuItem].
184-
final DefaultTextStyle enabledText = tester.widget<DefaultTextStyle>(
191+
DefaultTextStyle popupMenuItemLabel = tester.widget<DefaultTextStyle>(
185192
find.descendant(
186193
of: find.byKey(enabledPopupItemKey),
187194
matching: find.byType(DefaultTextStyle),
188195
).last,
189196
);
190-
expect(enabledText.style.fontFamily, 'Roboto');
191-
expect(enabledText.style.color, theme.colorScheme.onSurface);
197+
expect(popupMenuItemLabel.style.fontFamily, 'Roboto');
198+
expect(popupMenuItemLabel.style.color, theme.colorScheme.onSurface);
199+
192200
/// Test disabled text color
193-
final DefaultTextStyle disabledText = tester.widget<DefaultTextStyle>(
201+
popupMenuItemLabel = tester.widget<DefaultTextStyle>(
194202
find.descendant(
195203
of: find.byKey(disabledPopupItemKey),
196204
matching: find.byType(DefaultTextStyle),
197205
).last,
198206
);
199-
expect(disabledText.style.color, theme.colorScheme.onSurface.withOpacity(0.38));
207+
expect(popupMenuItemLabel.style.color, theme.colorScheme.onSurface.withOpacity(0.38));
200208

201209
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>));
202210
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
@@ -217,6 +225,14 @@ void main() {
217225
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
218226
SystemMouseCursors.click,
219227
);
228+
229+
// Test unchecked CheckedPopupMenuItem label.
230+
ListTile listTile = tester.widget<ListTile>(find.byType(ListTile).first);
231+
expect(listTile.titleTextStyle?.color, theme.colorScheme.onSurface);
232+
233+
// Test checked CheckedPopupMenuItem label.
234+
listTile = tester.widget<ListTile>(find.byType(ListTile).last);
235+
expect(listTile.titleTextStyle?.color, theme.colorScheme.onSurface);
220236
});
221237

222238
testWidgetsWithLeakTracking('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async {
@@ -251,6 +267,13 @@ void main() {
251267
onTap: () { },
252268
child: const Text('enabled'),
253269
),
270+
const CheckedPopupMenuItem<Object>(
271+
child: Text('Unchecked item'),
272+
),
273+
const CheckedPopupMenuItem<Object>(
274+
checked: true,
275+
child: Text('Checked item'),
276+
),
254277
];
255278
},
256279
),
@@ -278,25 +301,25 @@ void main() {
278301
expect(button.shape, const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))));
279302
expect(button.elevation, 12.0);
280303

281-
final DefaultTextStyle enabledText = tester.widget<DefaultTextStyle>(
304+
DefaultTextStyle popupMenuItemLabel = tester.widget<DefaultTextStyle>(
282305
find.descendant(
283306
of: find.byKey(enabledPopupItemKey),
284307
matching: find.byType(DefaultTextStyle),
285308
).last,
286309
);
287310
expect(
288-
enabledText.style,
311+
popupMenuItemLabel.style,
289312
popupMenuTheme.labelTextStyle?.resolve(enabled),
290313
);
291314
/// Test disabled text color
292-
final DefaultTextStyle disabledText = tester.widget<DefaultTextStyle>(
315+
popupMenuItemLabel = tester.widget<DefaultTextStyle>(
293316
find.descendant(
294317
of: find.byKey(disabledPopupItemKey),
295318
matching: find.byType(DefaultTextStyle),
296319
).last,
297320
);
298321
expect(
299-
disabledText.style,
322+
popupMenuItemLabel.style,
300323
popupMenuTheme.labelTextStyle?.resolve(disabled),
301324
);
302325

@@ -315,6 +338,14 @@ void main() {
315338
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
316339
popupMenuTheme.mouseCursor?.resolve(enabled),
317340
);
341+
342+
// Test unchecked CheckedPopupMenuItem label.
343+
ListTile listTile = tester.widget<ListTile>(find.byType(ListTile).first);
344+
expect(listTile.titleTextStyle, popupMenuTheme.labelTextStyle?.resolve(enabled));
345+
346+
// Test checked CheckedPopupMenuItem label.
347+
listTile = tester.widget<ListTile>(find.byType(ListTile).last);
348+
expect(listTile.titleTextStyle, popupMenuTheme.labelTextStyle?.resolve(enabled));
318349
});
319350

320351
testWidgetsWithLeakTracking('Popup menu widget properties take priority over theme', (WidgetTester tester) async {
@@ -354,6 +385,11 @@ void main() {
354385
mouseCursor: cursor,
355386
child: const Text('Example'),
356387
),
388+
CheckedPopupMenuItem<void>(
389+
checked: true,
390+
labelTextStyle: MaterialStateProperty.all<TextStyle>(textStyle),
391+
child: const Text('Checked item'),
392+
)
357393
];
358394
},
359395
),
@@ -399,6 +435,10 @@ void main() {
399435
await gesture.moveTo(tester.getCenter(find.byKey(popupItemKey)));
400436
await tester.pumpAndSettle();
401437
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), cursor);
438+
439+
// Test CheckedPopupMenuItem label.
440+
final ListTile listTile = tester.widget<ListTile>(find.byType(ListTile).first);
441+
expect(listTile.titleTextStyle, textStyle);
402442
});
403443

404444
group('Material 2', () {

0 commit comments

Comments
 (0)