Skip to content

Commit

Permalink
Update default menu text styles for Material 3 (#131930)
Browse files Browse the repository at this point in the history
Related flutter/flutter#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

<details> 
<summary>expand to view the code sample</summary> 

```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<DropdownMenuExample> createState() => _DropdownMenuExampleState();
}

class _DropdownMenuExampleState extends State<DropdownMenuExample> {
  final TextEditingController colorController = TextEditingController();
  final TextEditingController iconController = TextEditingController();
  ColorLabel? selectedColor;
  IconLabel? selectedIcon;

  @OverRide
  Widget build(BuildContext context) {
    final List<DropdownMenuEntry<ColorLabel>> colorEntries =
        <DropdownMenuEntry<ColorLabel>>[];
    for (final ColorLabel color in ColorLabel.values) {
      colorEntries.add(
        DropdownMenuEntry<ColorLabel>(
            value: color, label: color.label, enabled: color.label != 'Grey'),
      );
    }

    final List<DropdownMenuEntry<IconLabel>> iconEntries =
        <DropdownMenuEntry<IconLabel>>[];
    for (final IconLabel icon in IconLabel.values) {
      iconEntries
          .add(DropdownMenuEntry<IconLabel>(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: <Widget>[
              const Text('DropdownMenus'),
              Padding(
                padding: const EdgeInsets.symmetric(vertical: 20),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    DropdownMenu<ColorLabel>(
                      controller: colorController,
                      label: const Text('Color'),
                      dropdownMenuEntries: colorEntries,
                      onSelected: (ColorLabel? color) {
                        setState(() {
                          selectedColor = color;
                        });
                      },
                    ),
                    const SizedBox(width: 20),
                    DropdownMenu<IconLabel>(
                      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: <Widget>[
                    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: <Widget>[
                    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;
}

``` 
	
</details>
  • Loading branch information
TahaTesser authored Aug 22, 2023
1 parent 469c6c3 commit 1bc7916
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 28 deletions.
5 changes: 4 additions & 1 deletion dev/tools/gen_defaults/lib/menu_template.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Color?>? get backgroundColor {
Expand Down Expand Up @@ -180,7 +181,9 @@ class _MenuButtonDefaultsM3 extends ButtonStyle {
@override
MaterialStateProperty<TextStyle?> get textStyle {
return MaterialStatePropertyAll<TextStyle?>(${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<TextStyle?>(_textTheme.labelLarge);
}
@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions examples/api/test/material/menu_anchor/menu_anchor.1_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions packages/flutter/lib/src/material/dropdown_menu.dart
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ class DropdownMenu<T> 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;

Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion packages/flutter/lib/src/material/menu_anchor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<Color?>? get backgroundColor {
Expand Down Expand Up @@ -3791,7 +3793,9 @@ class _MenuButtonDefaultsM3 extends ButtonStyle {

@override
MaterialStateProperty<TextStyle?> get textStyle {
return MaterialStatePropertyAll<TextStyle?>(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<TextStyle?>(_textTheme.labelLarge);
}

@override
Expand Down
75 changes: 64 additions & 11 deletions packages/flutter/test/material/dropdown_menu_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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 {
Expand Down Expand Up @@ -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<TestMenu>));
await tester.pumpAndSettle();
Expand All @@ -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.
Expand Down Expand Up @@ -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());
Expand All @@ -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 '
Expand Down Expand Up @@ -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<Material>(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 {
Expand Down
71 changes: 66 additions & 5 deletions packages/flutter/test/material/menu_anchor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -3205,6 +3209,7 @@ void main() {
style: SubmenuButton.styleFrom(fixedSize: const Size(88.0, 36.0)),
menuChildren: <Widget>[
MenuItemButton(
style: SubmenuButton.styleFrom(fixedSize: const Size(120.0, 36.0)),
child: const Text('Item 0'),
onPressed: () {},
),
Expand Down Expand Up @@ -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> [
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> [SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics> [
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>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
),
Expand Down Expand Up @@ -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<Material>(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<Material>(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<Widget> createTestMenus({
Expand Down
7 changes: 5 additions & 2 deletions packages/flutter/test/material/menu_style_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 1bc7916

Please sign in to comment.