From 1bc791697c9f09157cdbdf55001c115f654ed95e Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Wed, 23 Aug 2023 01:21:00 +0300 Subject: [PATCH] Update default menu text styles for Material 3 (#131930) Related https://github.com/flutter/flutter/issues/131676 ## Description #### Fix default input text style for `DropdownMenu` ![dropdown_input](https://github.com/flutter/flutter/assets/48603081/301f8243-155a-4b8f-84a8-5e6b7bebb3bc) ### Fix default text style for `MenuAnchor`'s menu items (which `DropdownMenu` uses for menu items) ![dropdown_item](https://github.com/flutter/flutter/assets/48603081/6b5be81a-72fc-4705-a577-074c7a4cad8f) ### Default `DropdownMenu` Input text style ![Screenshot 2023-08-04 at 16 48 28](https://github.com/flutter/flutter/assets/48603081/bcd9da98-e74d-491e-ae64-6268ae0b3893) ### Default `DropdownMenu` menu item text style ![Screenshot 2023-08-04 at 16 50 19](https://github.com/flutter/flutter/assets/48603081/9592ca43-2854-45b5-8648-203ab65d9745) ### Default `MenuAnchor` menu item text style ![Screenshot 2023-08-04 at 14 34 28](https://github.com/flutter/flutter/assets/48603081/e87e1073-05f8-4dc7-a435-d864e9cce6ab) ### Code sample
expand to view the code sample ```dart import 'package:flutter/material.dart'; /// Flutter code sample for [DropdownMenu]s. The first dropdown menu has an outlined border. void main() => runApp(const DropdownMenuExample()); class DropdownMenuExample extends StatefulWidget { const DropdownMenuExample({super.key}); @override State createState() => _DropdownMenuExampleState(); } class _DropdownMenuExampleState extends State { final TextEditingController colorController = TextEditingController(); final TextEditingController iconController = TextEditingController(); ColorLabel? selectedColor; IconLabel? selectedIcon; @override Widget build(BuildContext context) { final List> colorEntries = >[]; for (final ColorLabel color in ColorLabel.values) { colorEntries.add( DropdownMenuEntry( value: color, label: color.label, enabled: color.label != 'Grey'), ); } final List> iconEntries = >[]; for (final IconLabel icon in IconLabel.values) { iconEntries .add(DropdownMenuEntry(value: icon, label: icon.label)); } return MaterialApp( theme: ThemeData( useMaterial3: true, colorSchemeSeed: Colors.green, // textTheme: const TextTheme( // bodyLarge: TextStyle( // fontWeight: FontWeight.bold, // fontStyle: FontStyle.italic, // decoration: TextDecoration.underline, // ), // ), ), home: Scaffold( body: SafeArea( child: Column( children: [ const Text('DropdownMenus'), Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ DropdownMenu( controller: colorController, label: const Text('Color'), dropdownMenuEntries: colorEntries, onSelected: (ColorLabel? color) { setState(() { selectedColor = color; }); }, ), const SizedBox(width: 20), DropdownMenu( controller: iconController, enableFilter: true, leadingIcon: const Icon(Icons.search), label: const Text('Icon'), dropdownMenuEntries: iconEntries, inputDecorationTheme: const InputDecorationTheme( filled: true, contentPadding: EdgeInsets.symmetric(vertical: 5.0), ), onSelected: (IconLabel? icon) { setState(() { selectedIcon = icon; }); }, ), ], ), ), const Text('Plain TextFields'), Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 150, child: TextField( controller: TextEditingController(text: 'Blue'), decoration: const InputDecoration( suffixIcon: Icon(Icons.arrow_drop_down), labelText: 'Color', border: OutlineInputBorder(), )), ), const SizedBox(width: 20), SizedBox( width: 150, child: TextField( controller: TextEditingController(text: 'Smile'), decoration: const InputDecoration( prefixIcon: Icon(Icons.search), suffixIcon: Icon(Icons.arrow_drop_down), filled: true, labelText: 'Icon', )), ), ], ), ), if (selectedColor != null && selectedIcon != null) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'You selected a ${selectedColor?.label} ${selectedIcon?.label}'), Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: Icon( selectedIcon?.icon, color: selectedColor?.color, ), ) ], ) else const Text('Please select a color and an icon.') ], ), ), ), ); } } enum ColorLabel { blue('Blue', Colors.blue), pink('Pink', Colors.pink), green('Green', Colors.green), yellow('Yellow', Colors.yellow), grey('Grey', Colors.grey); const ColorLabel(this.label, this.color); final String label; final Color color; } enum IconLabel { smile('Smile', Icons.sentiment_satisfied_outlined), cloud( 'Cloud', Icons.cloud_outlined, ), brush('Brush', Icons.brush_outlined), heart('Heart', Icons.favorite); const IconLabel(this.label, this.icon); final String label; final IconData icon; } ```
--- dev/tools/gen_defaults/lib/menu_template.dart | 5 +- .../menu_accelerator_label.0_test.dart | 9 ++- .../menu_anchor/menu_anchor.1_test.dart | 10 ++- .../lib/src/material/dropdown_menu.dart | 4 +- .../flutter/lib/src/material/menu_anchor.dart | 6 +- .../test/material/dropdown_menu_test.dart | 75 ++++++++++++++++--- .../test/material/menu_anchor_test.dart | 71 ++++++++++++++++-- .../test/material/menu_style_test.dart | 7 +- 8 files changed, 159 insertions(+), 28 deletions(-) diff --git a/dev/tools/gen_defaults/lib/menu_template.dart b/dev/tools/gen_defaults/lib/menu_template.dart index 50dc6c4e2f..6d2d6b4430 100644 --- a/dev/tools/gen_defaults/lib/menu_template.dart +++ b/dev/tools/gen_defaults/lib/menu_template.dart @@ -65,6 +65,7 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + late final TextTheme _textTheme = Theme.of(context).textTheme; @override MaterialStateProperty? get backgroundColor { @@ -180,7 +181,9 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { @override MaterialStateProperty get textStyle { - return MaterialStatePropertyAll(${textStyle('md.comp.list.list-item.label-text')}); + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + return MaterialStatePropertyAll(_textTheme.labelLarge); } @override diff --git a/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart b/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart index 89cadb7db3..5e69f5d8c1 100644 --- a/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart +++ b/examples/api/test/material/menu_anchor/menu_accelerator_label.0_test.dart @@ -27,10 +27,11 @@ void main() { await tester.pump(); expect(find.text('About', findRichText: true), findsOneWidget); - expect( - tester.getRect(findMenu('About')), - equals(const Rect.fromLTRB(4.0, 48.0, 110.5, 208.0)), - ); + expect(tester.getRect(findMenu('About')).left, equals(4.0)); + expect(tester.getRect(findMenu('About')).top, equals(48.0)); + expect(tester.getRect(findMenu('About')).right, closeTo(98.5, 0.1)); + expect(tester.getRect(findMenu('About')).bottom, equals(208.0)); + expect(find.text('Save', findRichText: true), findsOneWidget); expect(find.text('Quit', findRichText: true), findsOneWidget); expect(find.text('Magnify', findRichText: true), findsNothing); diff --git a/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart b/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart index 0ac0e43ddb..fdb3eeea4e 100644 --- a/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart +++ b/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart @@ -21,13 +21,19 @@ void main() { await tester.tapAt(const Offset(100, 200), buttons: kSecondaryButton); await tester.pumpAndSettle(); - expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(100.0, 200.0, 433.0, 360.0))); + expect(tester.getRect(findMenu()).left, equals(100.0)); + expect(tester.getRect(findMenu()).top, equals(200.0)); + expect(tester.getRect(findMenu()).right, closeTo(389.8, 0.1)); + expect(tester.getRect(findMenu()).bottom, equals(360.0)); // Make sure tapping in a different place causes the menu to move. await tester.tapAt(const Offset(200, 100), buttons: kSecondaryButton); await tester.pump(); - expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(200.0, 100.0, 533.0, 260.0))); + expect(tester.getRect(findMenu()).left, equals(200.0)); + expect(tester.getRect(findMenu()).top, equals(100.0)); + expect(tester.getRect(findMenu()).right, closeTo(489.8, 0.1)); + expect(tester.getRect(findMenu()).bottom, equals(260.0)); expect(find.text(example.MenuEntry.about.label), findsOneWidget); expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget); diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 51a972a339..27baf5951b 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -229,7 +229,7 @@ class DropdownMenu extends StatefulWidget { /// The text style for the [TextField] of the [DropdownMenu]; /// - /// Defaults to the overall theme's [TextTheme.labelLarge] + /// Defaults to the overall theme's [TextTheme.bodyLarge] /// if the dropdown menu theme's value is null. final TextStyle? textStyle; @@ -916,7 +916,7 @@ class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { late final ThemeData _theme = Theme.of(context); @override - TextStyle? get textStyle => _theme.textTheme.labelLarge; + TextStyle? get textStyle => _theme.textTheme.bodyLarge; @override MenuStyle get menuStyle { diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart index f2dfeeea5e..3ad5543407 100644 --- a/packages/flutter/lib/src/material/menu_anchor.dart +++ b/packages/flutter/lib/src/material/menu_anchor.dart @@ -27,6 +27,7 @@ import 'menu_style.dart'; import 'menu_theme.dart'; import 'radio.dart'; import 'text_button.dart'; +import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -3676,6 +3677,7 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { final BuildContext context; late final ColorScheme _colors = Theme.of(context).colorScheme; + late final TextTheme _textTheme = Theme.of(context).textTheme; @override MaterialStateProperty? get backgroundColor { @@ -3791,7 +3793,9 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { @override MaterialStateProperty get textStyle { - return MaterialStatePropertyAll(Theme.of(context).textTheme.bodyLarge); + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + return MaterialStatePropertyAll(_textTheme.labelLarge); } @override diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 45802fdb91..5d8a2c5314 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'dart:ui'; import 'package:flutter/material.dart'; @@ -39,15 +38,19 @@ void main() { await tester.pumpWidget(buildTest(themeData, menuChildren)); final EditableText editableText = tester.widget(find.byType(EditableText)); - expect(editableText.style.color, themeData.textTheme.labelLarge!.color); - expect(editableText.style.background, themeData.textTheme.labelLarge!.background); - expect(editableText.style.shadows, themeData.textTheme.labelLarge!.shadows); - expect(editableText.style.decoration, themeData.textTheme.labelLarge!.decoration); - expect(editableText.style.locale, themeData.textTheme.labelLarge!.locale); - expect(editableText.style.wordSpacing, themeData.textTheme.labelLarge!.wordSpacing); + expect(editableText.style.color, themeData.textTheme.bodyLarge!.color); + expect(editableText.style.background, themeData.textTheme.bodyLarge!.background); + expect(editableText.style.shadows, themeData.textTheme.bodyLarge!.shadows); + expect(editableText.style.decoration, themeData.textTheme.bodyLarge!.decoration); + expect(editableText.style.locale, themeData.textTheme.bodyLarge!.locale); + expect(editableText.style.wordSpacing, themeData.textTheme.bodyLarge!.wordSpacing); + expect(editableText.style.fontSize, 16.0); + expect(editableText.style.height, 1.5); final TextField textField = tester.widget(find.byType(TextField)); expect(textField.decoration?.border, const OutlineInputBorder()); + expect(textField.style?.fontSize, 16.0); + expect(textField.style?.height, 1.5); await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first); await tester.pump(); @@ -74,6 +77,8 @@ void main() { expect(material.elevation, 0.0); expect(material.shape, const RoundedRectangleBorder()); expect(material.textStyle?.color, themeData.colorScheme.onSurface); + expect(material.textStyle?.fontSize, 14.0); + expect(material.textStyle?.height, 1.43); }); testWidgets('DropdownMenu can be disabled', (WidgetTester tester) async { @@ -177,7 +182,7 @@ void main() { final Finder textField = find.byType(TextField); final double anchorWidth = tester.getSize(textField).width; - expect(anchorWidth, 195.0); + expect(anchorWidth, closeTo(180.5, 0.1)); await tester.tap(find.byType(DropdownMenu)); await tester.pumpAndSettle(); @@ -187,7 +192,7 @@ void main() { matching: find.byType(Material), ); final double menuWidth = tester.getSize(menuMaterial).width; - expect(menuWidth, 195.0); + expect(menuWidth, closeTo(180.5, 0.1)); // The text field should have same width as the menu // when the width property is not null. @@ -391,7 +396,8 @@ void main() { matching: find.byType(Padding), ).first; final Size menuViewSize = tester.getSize(menuView); - expect(menuViewSize, const Size(195.0, 304.0)); // 304 = 288 + vertical padding(2 * 8) + expect(menuViewSize.width, closeTo(180.6, 0.1)); + expect(menuViewSize.height, equals(304.0)); // 304 = 288 + vertical padding(2 * 8) // Constrains the menu height. await tester.pumpWidget(Container()); @@ -407,7 +413,8 @@ void main() { ).first; final Size updatedMenuSize = tester.getSize(updatedMenu); - expect(updatedMenuSize, const Size(195.0, 100.0)); + expect(updatedMenuSize.width, closeTo(180.6, 0.1)); + expect(updatedMenuSize.height, equals(100.0)); }); testWidgets('The text in the menu button should be aligned with the text of ' @@ -1518,6 +1525,52 @@ void main() { expect(find.text('Item 5').hitTestable(), findsOneWidget); }); + // This is a regression test for https://github.com/flutter/flutter/issues/131676. + testWidgets('Material3 - DropdownMenu uses correct text styles', (WidgetTester tester) async { + const TextStyle inputTextThemeStyle = TextStyle( + fontSize: 18.5, + fontStyle: FontStyle.italic, + wordSpacing: 1.2, + decoration: TextDecoration.lineThrough, + ); + const TextStyle menuItemTextThemeStyle = TextStyle( + fontSize: 20.5, + fontStyle: FontStyle.italic, + wordSpacing: 2.1, + decoration: TextDecoration.underline, + ); + final ThemeData themeData = ThemeData( + useMaterial3: true, + textTheme: const TextTheme( + bodyLarge: inputTextThemeStyle, + labelLarge: menuItemTextThemeStyle, + ), + ); + await tester.pumpWidget(buildTest(themeData, menuChildren)); + + // Test input text style uses the TextTheme.bodyLarge. + final EditableText editableText = tester.widget(find.byType(EditableText)); + expect(editableText.style.fontSize, inputTextThemeStyle.fontSize); + expect(editableText.style.fontStyle, inputTextThemeStyle.fontStyle); + expect(editableText.style.wordSpacing, inputTextThemeStyle.wordSpacing); + expect(editableText.style.decoration, inputTextThemeStyle.decoration); + + // Open the menu. + await tester.tap(find.widgetWithIcon(IconButton, Icons.arrow_drop_down).first); + await tester.pump(); + + final Finder buttonMaterial = find.descendant( + of: find.byType(TextButton), + matching: find.byType(Material), + ).last; + + // Test menu item text style uses the TextTheme.labelLarge. + final Material material = tester.widget(buttonMaterial); + expect(material.textStyle?.fontSize, menuItemTextThemeStyle.fontSize); + expect(material.textStyle?.fontStyle, menuItemTextThemeStyle.fontStyle); + expect(material.textStyle?.wordSpacing, menuItemTextThemeStyle.wordSpacing); + expect(material.textStyle?.decoration, menuItemTextThemeStyle.decoration); + }); } enum TestMenu { diff --git a/packages/flutter/test/material/menu_anchor_test.dart b/packages/flutter/test/material/menu_anchor_test.dart index e7a745490f..5416882eab 100644 --- a/packages/flutter/test/material/menu_anchor_test.dart +++ b/packages/flutter/test/material/menu_anchor_test.dart @@ -237,7 +237,7 @@ void main() { ); }); - testWidgets('menu defaults colors', (WidgetTester tester) async { + testWidgets('Menu defaults', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); await tester.pumpWidget( MaterialApp( @@ -278,6 +278,8 @@ void main() { expect(material.elevation, 0.0); expect(material.shape, const RoundedRectangleBorder()); expect(material.textStyle?.color, themeData.colorScheme.onSurface); + expect(material.textStyle?.fontSize, 14.0); + expect(material.textStyle?.height, 1.43); // vertical menu await tester.tap(find.text(TestMenu.mainMenu1.label)); @@ -305,6 +307,8 @@ void main() { expect(material.elevation, 0.0); expect(material.shape, const RoundedRectangleBorder()); expect(material.textStyle?.color, themeData.colorScheme.onSurface); + expect(material.textStyle?.fontSize, 14.0); + expect(material.textStyle?.height, 1.43); await tester.tap(find.text(TestMenu.mainMenu0.label)); await tester.pump(); @@ -315,7 +319,7 @@ void main() { expect(iconRichText.text.style?.color, themeData.colorScheme.onSurfaceVariant); }); - testWidgets('menu defaults - disabled', (WidgetTester tester) async { + testWidgets('Menu defaults - disabled', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); await tester.pumpWidget( MaterialApp( @@ -3205,6 +3209,7 @@ void main() { style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)), menuChildren: [ MenuItemButton( + style: SubmenuButton.styleFrom(fixedSize: const Size(120.0, 36.0)), child: const Text('Item 0'), onPressed: () {}, ), @@ -3250,17 +3255,17 @@ void main() { ), TestSemantics( id: 6, - rect: const Rect.fromLTRB(0.0, 0.0, 123.0, 64.0), + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 64.0), children: [ TestSemantics( id: 7, - rect: const Rect.fromLTRB(0.0, 0.0, 123.0, 48.0), + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics( id: 8, label: 'Item 0', - rect: const Rect.fromLTRB(0.0, 0.0, 123.0, 48.0), + rect: const Rect.fromLTRB(0.0, 0.0, 120.0, 48.0), flags: [SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable], actions: [SemanticsAction.tap], ), @@ -3320,6 +3325,62 @@ void main() { semantics.dispose(); }); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/131676. + testWidgets('Material3 - Menu uses correct text styles', (WidgetTester tester) async { + const TextStyle menuTextStyle = TextStyle( + fontSize: 18.5, + fontStyle: FontStyle.italic, + wordSpacing: 1.2, + decoration: TextDecoration.lineThrough, + ); + final ThemeData themeData = ThemeData( + textTheme: const TextTheme( + labelLarge: menuTextStyle, + ) + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Material( + child: MenuBar( + controller: controller, + children: createTestMenus( + onPressed: onPressed, + onOpen: onOpen, + onClose: onClose, + ), + ), + ), + ), + ); + + // Test menu button text style uses the TextTheme.labelLarge. + Finder buttonMaterial = find.descendant( + of: find.byType(TextButton), + matching: find.byType(Material), + ).first; + Material material = tester.widget(buttonMaterial); + expect(material.textStyle?.fontSize, menuTextStyle.fontSize); + expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle); + expect(material.textStyle?.wordSpacing, menuTextStyle.wordSpacing); + expect(material.textStyle?.decoration, menuTextStyle.decoration); + + // Open the menu. + await tester.tap(find.text(TestMenu.mainMenu1.label)); + await tester.pump(); + + // Test menu item text style uses the TextTheme.labelLarge. + buttonMaterial = find.descendant( + of: find.widgetWithText(TextButton, TestMenu.subMenu10.label), + matching: find.byType(Material), + ).first; + material = tester.widget(buttonMaterial); + expect(material.textStyle?.fontSize, menuTextStyle.fontSize); + expect(material.textStyle?.fontStyle, menuTextStyle.fontStyle); + expect(material.textStyle?.wordSpacing, menuTextStyle.wordSpacing); + expect(material.textStyle?.decoration, menuTextStyle.decoration); + }); } List createTestMenus({ diff --git a/packages/flutter/test/material/menu_style_test.dart b/packages/flutter/test/material/menu_style_test.dart index dc501b68bc..74ce6b8887 100644 --- a/packages/flutter/test/material/menu_style_test.dart +++ b/packages/flutter/test/material/menu_style_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -81,8 +82,10 @@ void main() { expect(tester.getRect(findMenuPanels().first).size, equals(const Size(600.0, 60.0))); // MenuTheme affects menus. - expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(104.0, 54.0, 204.0, 154.0))); - expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0))); + if (!kIsWeb || isCanvasKit) { // https://github.com/flutter/flutter/issues/99933 + expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(104.0, 54.0, 204.0, 154.0))); + expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0))); + } }); testWidgets('maximumSize affects geometry', (WidgetTester tester) async {